Lines 100.00% 37 / 37
Methods 100.00% 6 / 6
Classes 100.00% 1 / 1
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 forApp 100.00% 1 / 1 100.00% 1 / 1 1
 forScope 100.00% 1 / 1 100.00% 1 / 1 1
 log 100.00% 18 / 18 100.00% 1 / 1 7
 serializableContext 100.00% 4 / 4 100.00% 1 / 1 2
 serializableValue 100.00% 12 / 12 100.00% 1 / 1 6
19final class WorkerLogger extends AbstractLogger
20{
21    public function __construct(
22        private readonly Transport $transport,
23        private readonly StderrFallback $stderr,
24        private readonly ResourceScope $resourceScope,
25        private readonly WorkerSettings $workerSettings,
26    ) {}
27
28    public function forApp(AppId $appId): self
29    {
30        return $this->forScope(ResourceScope::forApp($appId));
31    }
32
33    public function forScope(ResourceScope $resourceScope): self
34    {
35        return new self($this->transport, $this->stderr, $resourceScope, $this->workerSettings);
36    }
37
38    /**
39     * @param mixed $level
40     * @param array<mixed> $context
41     * @throws InvalidArgumentException
42     */
43    public function log($level, string|Stringable $message, array $context = []): void
44    {
45        $severity = \is_string($level) ? LogLevel::tryFrom($level) : null;
46
47        if ($severity === null) {
48            throw new InvalidArgumentException(\sprintf(
49                'Unknown log level "%s"; use one of %s.',
50                \is_scalar($level) ? (string) $level : get_debug_type($level),
51                implode(', ', array_map(static fn(LogLevel $known): string => $known->value, LogLevel::cases())),
52            ));
53        }
54
55        if ($severity->isBelow($this->workerSettings->logLevel)) {
56            return;
57        }
58
59        try {
60            $this->transport->send(new LogRecord(
61                scope: $this->resourceScope,
62                level: $severity,
63                message: (string) $message,
64                context: self::serializableContext($context),
65                exception: ($context['exception'] ?? null) instanceof Throwable ? ExceptionDetails::fromThrowable($context['exception']) : null,
66            ));
67        } catch (Throwable) {
68            $this->stderr->writeLine($this->resourceScope, \sprintf('%s %s', $severity->value, $message));
69        }
70    }
71
72    /**
73     * @param array<array-key, mixed> $context
74     * @return array<string, mixed>
75     */
76    private static function serializableContext(array $context): array
77    {
78        $out = [];
79
80        foreach ($context as $key => $value) {
81            $out[(string) $key] = self::serializableValue($value);
82        }
83
84        return $out;
85    }
86
87    private static function serializableValue(mixed $value): mixed
88    {
89        return match (true) {
90            $value === null, \is_scalar($value) => $value,
91            $value instanceof Throwable => \sprintf(
92                "%s: %s in %s:%d\n%s",
93                $value::class,
94                $value->getMessage(),
95                $value->getFile(),
96                $value->getLine(),
97                $value->getTraceAsString(),
98            ),
99            \is_array($value) => array_map(self::serializableValue(...), $value),
100            $value instanceof Stringable => (string) $value,
101            default => get_debug_type($value),
102        };
103    }
104}