Skip to main content
Version: 1.3.x

Observability

Metrics Module

The namastack-outbox-metrics module provides automatic integration with Spring Boot Actuator and Micrometer:

dependencies {
implementation("io.namastack:namastack-outbox-starter-jpa:1.3.x")
implementation("io.namastack:namastack-outbox-metrics:1.3.x")
// For Prometheus endpoint (optional)
implementation("io.micrometer:micrometer-registry-prometheus")
}

Built-in Metrics

MetricDescriptionTags
outbox.records.countNumber of outbox recordsstatus=new|failed|completed
outbox.partitions.assigned.countPartitions assigned to this instance-
outbox.partitions.pending.records.totalTotal pending records across partitions-
outbox.partitions.pending.records.maxMaximum pending records in any partition-
outbox.cluster.instances.totalTotal active instances in cluster-
Endpoints
  • /actuator/metrics/outbox.records.count
  • /actuator/metrics/outbox.partitions.assigned.count
  • /actuator/prometheus (if Prometheus enabled)

Programmatic Monitoring

@Service
class OutboxMonitoringService(
private val outboxRepository: OutboxRecordRepository,
private val partitionMetricsProvider: OutboxPartitionMetricsProvider
) {
fun getPendingRecordCount(): Long =
outboxRepository.countByStatus(OutboxRecordStatus.NEW)
fun getFailedRecordCount(): Long =
outboxRepository.countByStatus(OutboxRecordStatus.FAILED)
fun getPartitionStats(): PartitionProcessingStats =
partitionMetricsProvider.getProcessingStats()
fun getClusterStats(): PartitionStats =
partitionMetricsProvider.getPartitionStats()
}

Observations & Tracing

The namastack-outbox-tracing module wraps every handler and fallback handler invocation in a Micrometer Observation, giving you distributed traces, metrics, and structured log correlation out of the box.

Setup

dependencies {
implementation("io.namastack:namastack-outbox-starter-jpa:1.3.x")
implementation("io.namastack:namastack-outbox-tracing:1.3.x")
// Tracing bridge of your choice, e.g. OpenTelemetry
implementation("org.springframework.boot:spring-boot-starter-opentelemetry")
}

The module auto-configures when both a Tracer and a Propagator bean are present (provided by any Spring Boot tracing bridge such as OpenTelemetry or Brave).


How It Works

The module handles tracing across both sides of the async boundary:

At scheduling time, OutboxTracingContextProvider serializes the active span context into the outbox record's context map using the configured Micrometer Propagator (W3C Trace Context format by default, producing traceparent, tracestate, and optional baggage headers). These headers are persisted alongside the record in the database. If no active span exists or serialization fails, an empty map is stored and scheduling continues unblocked.

At processing time, each polled record passes through an AOP advice that:

  1. Reads the propagation headers stored in record.context (e.g. traceparent, tracestate).
  2. Creates a child span under the original producer trace so the whole flow is visible end-to-end.
  3. Attaches observation tags (see table below) and stops the observation when the handler returns.

This applies to both the primary handler and the fallback handler, each producing its own span.


Observation

PropertyValue
Nameoutbox.record.process
Contextual nameoutbox process

Tags

Low-cardinality (safe to use as metric/trace dimensions):

Tag keyValuesDescription
outbox.handler.kindprimary / fallbackWhether the primary or fallback handler processed the record
outbox.handler.idhandler nameIdentifier of the handler that processed the record

High-cardinality (for traces and log correlation only, not metric dimensions):

Tag keyExample valueDescription
outbox.record.id3fa85f64-5717-4562-b3fc-2c963f66afa6Unique identifier (UUID) of the outbox record
outbox.record.keyorder-42Business key of the outbox record
outbox.delivery.attempt2Current attempt number (failureCount + 1)
See also

For details on how trace headers are stored in and read from record.context, how to add your own context alongside tracing (e.g. tenant ID, correlation ID), or how to manually override context at scheduling time, see Context Propagation.

Custom Observation

You can override the default observation naming and tag convention by providing your own implementation of OutboxProcessObservationConvention and registering it as a Spring bean.

When the tracing auto-configuration is active the AOP advice (OutboxInvokerObservationAdvice) will use a custom convention bean if present; otherwise it falls back to OutboxObservationDocumentation.DefaultOutboxProcessObservationConvention.

Example: a minimal custom convention that changes the observation name and adds a tenant tag when present in the record context.

@Configuration
class CustomOutboxObservationConfig {
@Bean
fun customOutboxConvention(): OutboxProcessObservationConvention =
object : OutboxProcessObservationConvention {
override fun getName(): String = "myapp.outbox.process"

override fun getContextualName(context: OutboxProcessObservationContext): String = "outbox process"

override fun getLowCardinalityKeyValues(context: OutboxProcessObservationContext) =
KeyValues.of(
OutboxObservationDocumentation.LowCardinalityKeyNames.HANDLER_KIND.withValue(context.getHandlerKind().toString()),
OutboxObservationDocumentation.LowCardinalityKeyNames.HANDLER_ID.withValue(context.getHandlerId()),
// optional tenant tag (low-cardinality if you control cardinality)
KeyName.of("tenant").withValue(context.record.context["tenant"] ?: "")
)

override fun getHighCardinalityKeyValues(context: OutboxProcessObservationContext) =
KeyValues.of(
OutboxObservationDocumentation.HighCardinalityKeyNames.RECORD_ID.withValue(context.getRecordId()),
OutboxObservationDocumentation.HighCardinalityKeyNames.RECORD_KEY.withValue(context.getRecordKey()),
OutboxObservationDocumentation.HighCardinalityKeyNames.DELIVERY_ATTEMPT.withValue(context.getDeliveryAttempt().toString()),
)
}
}

Important notes:

  • The OutboxProcessObservationConvention implementation must implement ObservationConvention<OutboxProcessObservationContext> (the API exposes the OutboxProcessObservationConvention interface to simplify this).
  • Keep high-cardinality values out of low-cardinality keys to avoid cardinality explosion in metrics backends.
  • Register the bean in any auto-config or @Configuration class; the library will pick it up automatically.

If you need a completely custom observation lifecycle (e.g., additional spans around multiple handlers or custom error handling), consider writing a custom Advisor using OutboxInvokerObservationAdvice as a reference or extending the auto-configuration with your own @Bean of type Advisor.