You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Defines the Functorium framework's Observability field/tag specification, Meter definition rules, and message template patterns. For Pipeline execution order, OpenTelemetryOptions settings, and custom extension points, see the Pipeline Specification.
Summary
Key Concepts
Concept
Description
Service Attributes
service.namespace, service.name, etc. for OpenTelemetry standard service identification
Namespace of service.name. Helps distinguish service groups (e.g., by team or environment).
mycompany.production
service.name
Logical name of the service. Must be identical across all horizontally scaled instances.
orderservice
service.version
Version string of the service API or implementation.
2.0.0
service.instance.id
Unique ID of the service instance. Must be globally unique per service.namespace,service.name pair. Uses HOSTNAME environment variable when available, otherwise falls back to Environment.MachineName.
Recommended: Use lowercase values for service.name and service.namespace (e.g., mycompany.production, orderservice).
This ensures consistency with OpenTelemetry conventions and prevents case-sensitivity issues in downstream systems (dashboards, queries, alerts).
Error Classification
Error Type Tag Values
The following table summarizes how error.type and error.code tag values are determined based on the error cause.
# Correct examples
request.event.count # Static .count
response.event.success_count # Adjective combination _count
request.params.orders_count # Dynamic parameter _count
# Incorrect examples
response.event.success.count # Do not use .count for combination count
request.params.orders.count # Do not use .count for dynamic fields
Metrics uses a dedicated durationHistogram instrument to capture processing time, which is the OpenTelemetry recommended approach for latency measurement.
Using elapsed time as a tag causes high cardinality explosion (each unique duration value creates a new time series, degrading metric storage and query performance).
Histogram provides statistical aggregation (percentiles, averages, counts) that is more useful for monitoring than individual elapsed values.
DomainEventHandler is classified as the Application layer, with request.layer as "application", request.category.name as "usecase", and request.category.type as "event".
Field/Tag
Logging
Metrics
Tracing
Description
request.layer
✅
✅
✅
Architecture layer ("application")
request.category.name
✅
✅
✅
Request category ("usecase")
request.category.type
✅
✅
✅
CQRS type ("event")
request.handler.name
✅
✅
✅
Handler class name
request.handler.method
✅
✅
✅
Method name ("Handle")
request.event.type
✅
-
✅
Event type name
request.event.id
✅
-
✅
Event unique ID
@request.message
✅
-
-
Event object (on request)
response.status
✅
✅
✅
Response status ("success", "failure")
response.elapsed
✅
-*
-
Elapsed time (seconds)
error.type
✅
✅
✅
Error classification ("expected", "exceptional")
error.code
✅
✅
✅
Domain-specific error code
Note: DomainEventHandler's response.elapsed is not set on Tracing Span tags (Logging only). Since Spans inherently have their own start/end times (duration), a separate elapsed field would be redundant.
DomainEventHandler's ErrorResponse logs the Exception object directly (instead of @error).
DomainEventHandler returns ValueTask, so @response.message is not recorded.
ctx.* User-Defined Context Fields (3-Pillar)
Overview
ctx.* fields are user-defined context fields that simultaneously propagate business context to Logging, Tracing, and Metrics. Source Generator automatically detects public properties of Request/Response/DomainEvent and generates IUsecaseCtxEnricher<TRequest, TResponse> or IDomainEventCtxEnricher<TEvent> implementations. CtxEnricherPipeline runs as the first Pipeline, making ctx.* data accessible to subsequent Metrics/Tracing/Logging Pipelines.
Item
Description
Target Pillar
Logging + Tracing (default). Metrics requires explicit opt-in via [CtxTarget]
Generator (Usecase)
CtxEnricherGenerator — detects records implementing ICommandRequest<T> / IQueryRequest<T>
Generator (DomainEvent)
DomainEventCtxEnricherGenerator — detects T from classes implementing IDomainEventHandler<T>
Runtime Mechanism
CtxEnricherContext.Push(name, value, pillars) — simultaneous propagation to Logging/Tracing/Metrics
Target Properties
Public scalar and collection properties (complex types are excluded)
All names are converted from PascalCase to snake_case. The I prefix is removed from interface names (ICustomerRequest → customer_request, IX → x).
Field names are identical across all Pillars -- the same ctx.* names are used in Logging, Tracing, and Metrics.
DomainEvent Enricher automatically excludes the default properties of the IDomainEvent interface. These properties are already output as standard fields (such as request.event.id).
Excluded Property
Reason
OccurredAt
Timestamp is output separately as @timestamp
EventId
Output separately as request.event.id
CorrelationId
Managed separately as standard correlation ID field
CausationId
Managed separately as standard causation ID field
Safety Net
PascalCase properties pushed via LogContext.PushProperty without the ctx. prefix are automatically converted to ctx.snake_case by OpenSearchJsonFormatter.
Code generated by the Source Generator already includes the ctx. prefix and is therefore not subject to safety net conversion. This conversion only applies when LogContext.PushProperty is called manually.
Extension Points (Partial Methods)
Generated Enricher classes are partial class and provide the following extension points.
# Request - Single event
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} requesting with {@request.message}
# Request - Aggregate multiple events
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} requesting with {request.event.count} events
# Response - Success
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} responded {response.status} in {response.elapsed:0.0000} s
# Response - Success (Aggregate)
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} responded {response.status} in {response.elapsed:0.0000} s with {request.event.count} events
# Response - Warning/Error
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} responded {response.status} in {response.elapsed:0.0000} s with {error.type}:{error.code} {@error}
# Response - Warning/Error (Aggregate)
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} responded {response.status} in {response.elapsed:0.0000} s with {request.event.count} events with {error.type}:{error.code} {@error}
# Response - Partial Failure (Aggregate)
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} responded {response.status} in {response.elapsed:0.0000} s with {request.event.count} events partial failure: {response.event.success_count} succeeded, {response.event.failure_count} failed
Publisher Event IDs
DomainEvent Publisher is classified as the Adapter layer, so it uses the same Event IDs as the Adapter layer.
Event
ID
Name
Request
2001
adapter.request
Success
2002
adapter.response.success
Warning
2003
adapter.response.warning
Error
2004
adapter.response.error
Message Templates (Handler)
DomainEventHandler logging is from the Handler perspective, processing events published by the Publisher. request.layer is "application", request.category.name is "usecase", and request.category.type is "event".
# Request
{request.layer} {request.category.name}.{request.category.type} {request.handler.name}.{request.handler.method} {request.event.type} {request.event.id} requesting with {@request.message}
# Response - Success
{request.layer} {request.category.name}.{request.category.type} {request.handler.name}.{request.handler.method} {request.event.type} {request.event.id} responded {response.status} in {response.elapsed:0.0000} s
# Response - Warning/Error
{request.layer} {request.category.name}.{request.category.type} {request.handler.name}.{request.handler.method} {request.event.type} {request.event.id} responded {response.status} in {response.elapsed:0.0000} s with {error.type}:{error.code} {@error}
Handler Event IDs
DomainEventHandler is classified as a usecase in the Application Layer, so it uses the same Event IDs as the Application Layer.
Tags excluded from DomainEvent Metrics:request.event.count, response.event.success_count, response.event.failure_count are not used as Metrics tags.
These values each have unique numeric values, so using them as tags would cause high cardinality explosion.
This follows the same principle as not using response.elapsed as a Metrics tag.
Span Name format difference: Application Layer includes the .{type} segment (command/query/event), but the Adapter layer omits the .{type} segment since there is no CQRS type distinction.
ObservableSignal -- Internal Developer Logging API for Adapter Implementations
Overview
ObservableSignal is a static API for developers to directly emit operational logs within Adapter implementation code. The common context set by the Observable wrapper (request.layer, request.category.name, request.handler.name, request.handler.method) is automatically included.
Pillar Scope
✅ = Supported, X = Not supported
Level
Logging
Tracing (Activity Event)
Metrics
Debug
✅
X (high frequency -> noise)
X
Warning
✅
✅ (track degradation cause within span)
X
Error
✅
✅ (track failure cause within span)
X
Metrics excluded: The Observable wrapper already auto-generates request/response/duration metrics. Use IMeterFactory directly for custom metrics.
Tracing excluded at Debug level: Adding high-frequency events like cache misses to spans creates trace noise.
// Warning on Polly retryObservableSignal.Warning("Retry attempt {Attempt}/{MaxRetry} after {Delay}s delay",("adapter.retry.attempt",attempt),("adapter.retry.delay_ms",delay.TotalMilliseconds));// Debug on cache miss (high frequency)ObservableSignal.Debug("Cache miss",("adapter.cache.key",cacheKey));// Error when retries exhaustedObservableSignal.Error(ex,"Database operation failed after exhausting retries",("adapter.db.retry.attempt",maxRetries));
How It Works
[GenerateObservablePort] Source Generator calls ObservableSignalScope.Begin() within ExecuteWithSpan
ObservableSignalScope sets current context (logger, layer, category, handler, method) via AsyncLocal
When ObservableSignal.Debug/Warning/Error is called in Adapter code, common fields are obtained from ObservableSignalScope.Current
ObservableSignalFactory outputs via ILogger + Activity Event