webhookUrl === null) { return; } try { $response = Http::asJson() ->connectTimeout($this->connectTimeout) ->timeout($this->timeout) ->post($this->webhookUrl, [ 'content' => $this->formatContent($record), ]); if ($response->failed()) { error_log(sprintf( '[discord logger] webhook failed with status %d: %s', $response->status(), $this->truncateForDiscord($response->body()) )); } } catch (Throwable $exception) { error_log(sprintf('[discord logger] %s', $exception->getMessage())); } } private function formatContent(LogRecord $record): string { $lines = []; if ($this->mention !== null) { $lines[] = $this->mention; } $lines[] = $this->title; $lines[] = sprintf('- App: %s (%s)', (string) config('app.name'), (string) config('app.env')); $lines[] = sprintf('- Level: %s', $record->level->getName()); $lines[] = sprintf('- Time: %s', $record->datetime->format(DATE_ATOM)); $lines[] = sprintf('- Message: %s', $record->message); /** @var Throwable|null $exception */ $exception = Arr::get($record->context, 'exception'); if ($exception instanceof Throwable) { $lines[] = ''; $lines[] = '**Exception**'; $lines[] = sprintf('> %s', $exception->getMessage()); $lines[] = sprintf('> %s', $exception::class); $lines[] = sprintf('> %s:%d', $exception->getFile(), $exception->getLine()); $trace = $this->normalizeTrace($exception); if ($trace !== []) { $lines[] = '```text'; $lines[] = implode("\n", $trace); $lines[] = '```'; } } $context = Arr::except($record->context, ['exception']); if ($context !== []) { $lines[] = ''; $lines[] = '**Context**'; $lines[] = '```json'; $lines[] = $this->truncateContext($this->jsonEncode($this->normalizeValue($context))); $lines[] = '```'; } return $this->truncateForDiscord(implode("\n", $lines)); } /** * @return array */ private function normalizeTrace(Throwable $exception): array { return array_map( fn (string $line): string => $this->truncateLine($line, 240), array_slice(explode("\n", $exception->getTraceAsString()), 0, $this->maxTraceLines), ); } private function truncateLine(string $value, int $limit): string { if (mb_strlen($value) <= $limit) { return $value; } return mb_substr($value, 0, max(0, $limit - 1)).'…'; } private function truncateContext(string $value): string { if (mb_strlen($value) <= $this->maxContextLength) { return $value; } return mb_substr($value, 0, max(0, $this->maxContextLength - 1)).'…'; } private function truncateForDiscord(string $value): string { if (mb_strlen($value) <= $this->maxContentLength) { return $value; } $limit = max(0, $this->maxContentLength - mb_strlen(self::TRUNCATION_SUFFIX)); return mb_substr($value, 0, $limit).self::TRUNCATION_SUFFIX; } private function jsonEncode(mixed $value): string { $encoded = json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR); return is_string($encoded) ? $encoded : '{}'; } private function normalizeValue(mixed $value): mixed { if (is_array($value)) { return array_map(fn (mixed $item): mixed => $this->normalizeValue($item), $value); } if ($value instanceof Throwable) { return [ 'class' => $value::class, 'message' => $value->getMessage(), 'file' => $value->getFile(), 'line' => $value->getLine(), ]; } if ($value instanceof Stringable) { return (string) $value; } if (is_object($value)) { return ['class' => $value::class]; } if (is_resource($value)) { return sprintf('resource(%s)', get_resource_type($value)); } return $value; } }