Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/Instrumentation/Laravel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,27 @@ The extension can be disabled via [runtime configuration](https://opentelemetry.
```shell
OTEL_PHP_DISABLED_INSTRUMENTATIONS=laravel
```

### Log Context Attributes

By default, log context is JSON-encoded into a single `context` attribute. This can make it difficult to search for specific context values in observability backends like SigNoz.

To flatten log context into individual, searchable attributes, enable:

```shell
OTEL_PHP_LARAVEL_LOG_ATTRIBUTES_FLATTEN=true
```

**Default behavior (off):**
```
context: {"http":{"method":"GET","path":"/users"},"user_id":"123"}
```

**With flattening enabled:**
```
http.method: GET
http.path: /users
user_id: 123
```

Nested arrays are flattened using dot notation, making each value individually searchable in your observability backend.
68 changes: 63 additions & 5 deletions src/Instrumentation/Laravel/src/Watchers/LogWatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,30 @@
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Log\Events\MessageLogged;
use Illuminate\Log\LogManager;
use Illuminate\Support\Arr;
use OpenTelemetry\API\Instrumentation\CachedInstrumentation;
use OpenTelemetry\API\Instrumentation\ConfigurationResolver;
use OpenTelemetry\API\Logs\Severity;
use Stringable;
use Throwable;
use TypeError;

class LogWatcher extends Watcher
{
/**
* When enabled, log context attributes are spread as individual OTLP attributes
* instead of being JSON-encoded into a single 'context' attribute.
* This improves searchability in observability backends like SigNoz.
*/
public const OTEL_PHP_LARAVEL_LOG_ATTRIBUTES_FLATTEN = 'OTEL_PHP_LARAVEL_LOG_ATTRIBUTES_FLATTEN';

private LogManager $logger;
private bool $flattenAttributes;

public function __construct(
private CachedInstrumentation $instrumentation,
) {
$this->flattenAttributes = $this->shouldFlattenAttributes();
}

/** @psalm-suppress UndefinedInterfaceMethod */
Expand Down Expand Up @@ -56,20 +69,28 @@ public function recordLog(MessageLogged $log): void
->logger()
->logRecordBuilder();

$context = array_filter($log->context, static fn ($value) => $value !== null);
$contextToProcess = array_filter($log->context, static fn ($value) => $value !== null);
$exception = $this->getExceptionFromContext($log->context);

if ($exception !== null) {
$logBuilder->setException($exception);

unset($context['exception']);
unset($contextToProcess['exception']);
}

$logBuilder->setBody($log->message)
->setSeverityText($log->level)
->setSeverityNumber(Severity::fromPsr3($log->level))
->setAttribute('context', $context)
->emit();
->setSeverityNumber(Severity::fromPsr3($log->level));

if ($this->flattenAttributes) {
foreach ($this->buildFlattenedAttributes($contextToProcess) as $key => $value) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation is much cleaner with that array method :)

$logBuilder->setAttribute($key, $value);
}
} else {
$logBuilder->setAttribute('context', json_encode($contextToProcess) ?: '{}');
}

$logBuilder->emit();
}

private function getExceptionFromContext(array $context): ?Throwable
Expand All @@ -83,4 +104,41 @@ private function getExceptionFromContext(array $context): ?Throwable

return $context['exception'];
}

private function shouldFlattenAttributes(): bool
{
$resolver = new ConfigurationResolver();

return $resolver->has(self::OTEL_PHP_LARAVEL_LOG_ATTRIBUTES_FLATTEN)
&& $resolver->getBoolean(self::OTEL_PHP_LARAVEL_LOG_ATTRIBUTES_FLATTEN);
}

/**
* Build flattened attributes from context array.
* Nested arrays are flattened with dot notation for better searchability.
*
* @return array<string, mixed>
*/
private function buildFlattenedAttributes(array $context): array
{
return array_map(fn ($value) => $this->normalizeValue($value), Arr::dot($context));
}

/**
* Normalize a value for OTLP attributes.
* OTLP attributes support: string, bool, int, float, and arrays of these.
*/
private function normalizeValue(mixed $value): string|bool|int|float|null
{
if ($value === null || is_scalar($value)) {
return $value;
}

if ($value instanceof Stringable) {
return (string) $value;
}

// For objects that can't be stringified, JSON encode them
return json_encode($value) ?: null;
}
}
Loading
Loading