§ reference

OpenTelemetry tracing

Ekbatan instruments its action execution pipeline with the **OpenTelemetry API** (`opentelemetry-api`). The library depends only on the API — no SDK. When no OTel SDK is registered at runtime, all tracing calls are no-ops with zero overhead. Consumers bring their own `opentelemetry-sdk` and exporters.

The instrumentation scope is io.ekbatan.core version 1.0.0, obtained from GlobalOpenTelemetry.get().getTracer(...).

Span hierarchy

Each executor.execute(...) call produces a span tree of the following shape:

[ekbatan.action.execute]                    ActionExecutor.execute()
├── [ekbatan.action.perform]                Action.perform()
└── [ekbatan.action.persist]                ActionExecutor.persistChanges()
    └── [ekbatan.transaction]               TransactionManager.inTransactionChecked() (per shard)
        ├── [ekbatan.repository]            AbstractRepository.addAllNoResult / updateAllNoResult
        └── [ekbatan.event.persist]         EventPersister.persistActionEvents()

A single action that touches one shard produces one of each span. A cross-shard action produces one transaction + nested repo + event-persist spans per involved shard.

Attributes

AttributeTypeSpanDescription
ekbatan.action.namestringaction.executeSimple class name of the action
ekbatan.action.principalstringaction.executePrincipal name
ekbatan.action.outcomestringaction.execute"success" or "error"
ekbatan.action.retry.countlongaction.executeTotal retries (0 if none)
ekbatan.shard.cross_shardbooleanaction.persistPresent (and true) when changes span multiple shards
ekbatan.shard.grouplongtransactionShard group identifier
ekbatan.shard.memberlongtransactionShard member identifier
db.operation.namestringrepository"INSERT" or "UPDATE". Follows OTel semantic conventions.
ekbatan.entity.typestringrepositorySimple class name of the domain object
ekbatan.batch.sizelongrepositoryNumber of records in the batch
ekbatan.event.countlongevent.persistNumber of model events persisted

db.operation.name follows OTel semantic conventions. All others use the ekbatan.* namespace.

Retry events

Each retry attempt adds a span event named "retry" to the action span with attributes retry.attempt (int) and retry.exception (exception class name). Retries are not their own spans — each retry re-executes perform + persist, so the child spans of each attempt already appear under the action span; a separate retry span would just add a redundant level.

Errors

On failure, spans are marked with StatusCode.ERROR and the exception is recorded via span.recordException().

Context propagation

Since actions execute single-threaded (ScopedValue-based transactions), context flows naturally via Span.makeCurrent() / Scope. No async context passing is needed. Each TransactionManager instance knows its own ShardIdentifier (set at construction time, defaults to ShardIdentifier.DEFAULT), so inTransactionChecked() automatically sets shard attributes on the transaction span without requiring the shard to be passed per-call.

Bringing your own SDK

The framework only declares the opentelemetry-api dependency. To actually export traces, your application brings the SDK and exporters:

implementation("io.opentelemetry:opentelemetry-sdk")
implementation("io.opentelemetry:opentelemetry-exporter-otlp")
implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")

Then configure via standard OTel system properties or environment variables — otel.exporter.otlp.endpoint, otel.service.name, etc. The framework picks up whatever GlobalOpenTelemetry returns.

In tests, register an in-memory exporter via opentelemetry-sdk-testing to assert on emitted spans:

testImplementation("io.opentelemetry:opentelemetry-sdk-testing")

The :ekbatan-core:tracingTest Gradle task forks a separate JVM specifically because the OTel SDK must be registered before any instrumented class loads its static Tracer field via GlobalOpenTelemetry.

Read-path tracing is not instrumented

AbstractRepository’s read methods (findById, findAllWhere, count, etc.) don’t produce their own spans. They run inside Action.perform() which already has a span, and instrumenting every read would be noisy for actions that do many lookups. If you need fine-grained read tracing, instrument those calls in your repository subclass.

See also