Skip to main content

Metadata Resolvers

Metadata resolvers are responsible for automatically attaching metadata to ledger entries.

Resolvers allow applications to:

  • Add request information
  • Add tenant identifiers
  • Attach environment details
  • Include job or queue metadata
  • Enrich entries without modifying logging code

Resolvers are executed automatically every time an entry is logged.


When to Use a Resolver

Use a metadata resolver when:

  • Metadata should be added automatically
  • The value is available globally or from the runtime
  • You want to avoid repeating metadata in every log call

Examples:

  • tenant_id
  • request_id
  • service name
  • region
  • feature flag environment

If metadata applies only to a specific operation, use explicit metadata or context instead.


Creating a Resolver

A resolver is a simple class that returns an array.

Example:

namespace App\Ledgerly\Resolvers;

class TenantMetadataResolver
{
public function resolve(): array
{
return [
'tenant_id' => tenant()->id,
];
}
}

Resolvers should return small, structured arrays.


Registering a Resolver

Resolvers are configured in:

config/ledgerly.php

Example:

'metadata_resolvers' => [
App\Ledgerly\Resolvers\TenantMetadataResolver::class,
],

Resolvers are executed in order.


Resolver Execution Order

Resolvers run sequentially. If multiple resolvers return the same key, later resolvers override earlier ones.

Example:

EnvironmentResolver
TenantResolver
RequestResolver

If both return:

region

The value from RequestResolver will be used.


Writing Safe Resolvers

Resolvers should:

  • Be fast
  • Be deterministic
  • Avoid heavy queries
  • Avoid network calls
  • Never throw exceptions

Resolvers should not:

  • Modify database state
  • Trigger events
  • Perform long-running operations

Resolvers should only gather context.


Example: Request Metadata Resolver

Example resolver that adds request data:

class RequestMetadataResolver
{
public function resolve(): array
{
if (!request()) {
return [];
}

return [
'ip' => request()->ip(),
'method' => request()->method(),
'url' => request()->fullUrl(),
];
}
}

This metadata will be attached automatically.


Example: Environment Metadata Resolver

class EnvironmentMetadataResolver
{
public function resolve(): array
{
return [
'environment' => app()->environment(),
'service' => config('app.name'),
];
}
}

Useful for multi-service or multi-environment deployments.


Example: Feature Flag Resolver

class FeatureFlagResolver
{
public function resolve(): array
{
return [
'feature_flags' => app(FeatureManager::class)->activeFlags(),
];
}
}

Useful for experiments and rollouts.


Conditional Metadata

Resolvers can return an empty array when metadata is not applicable:

if (!auth()->check()) {
return [];
}

This ensures entries remain clean.


Resolver vs Context

Use a resolver when:

  • Metadata should always be attached automatically
  • Value is derived from runtime environment

Use context when:

  • Value applies to a specific request or workflow
  • Value is known in application code

Example:

Resolver:

environment
request_id
source

Context:

tenant_id
batch_id
workflow_id

Resolver vs Explicit Metadata

Use explicit metadata when:

  • Metadata applies to only one entry

Example:

ledgerly()
->withMetadata(['channel' => 'email'])
->log();

Testing Resolvers

Resolvers should be tested independently.

Example:

$resolver = new TenantMetadataResolver();

$this->assertEquals(
['tenant_id' => 42],
$resolver->resolve()
);

Testing ensures metadata remains consistent across changes.


Best Practices

Recommended:

  • Keep resolvers small
  • Return only relevant keys
  • Use consistent naming
  • Document custom metadata keys

Avoid:

  • Returning large payloads
  • Storing sensitive data
  • Making external API calls