Add OpenTelemetry logging backend - #143
Conversation
Build on the Phase 0/1 backend with the log-based events surface, an OTLP bootstrap module, trace correlation, documentation, and review fixes. - Events: the `EVENT_NAME` metadata key and `WithLogging.logEvent(...)`. The backend forwards the name via `Logger.emit(eventName = …)` and omits it from the `spine.*` attributes. - `backends/otel-backend-bootstrap` (jvm-module): `OtelLogging.installOtlpHttp()` and `fromEnvironment()` build a native OTLP/HTTP SDK, install it through `OtelBackendSettings`, and return an `AutoCloseable` that uninstalls and shuts the SDK down. Adds the `exporters-core` and `exporters-otlp` coordinates. - Add a trace-correlation test (the record carries the active span's ids) and the module `README.md` (usage, mapping, events, and the consumer-side domain-event recipe). - Review fixes: restore the no-op holder after each test, safe-cast in `close()`, null-message guard in `handleError`, a widening KDoc note, README line length, and a resolved KDoc link. Tests: otel-backend 13, bootstrap 2. detekt and Dokka clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address findings from spine-code-review, kotlin-engineer, dependency-audit, and review-docs: - Revert an unintended `rootProject.name` rename (`logging` -> `spine-logging`) that a formatter slipped into the Phase 0 commit. - Add tests for `handleError`, `Tags` expansion (label-only / single / list), and boolean and double attribute values (otel-backend now has 16 tests). - `OpenTelemetryKotlin`: add the project URL comment. - Use `requireNotNull` instead of `!!` in the `StubLogData` test fixture. - README: fix an example comment; capitalize a `@return` description. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- `OtelLoggerBackend` no longer captures the OpenTelemetry logger at creation. It resolves from `OtelBackendSettings.current()` on every access (a `logger` property), so a backend created and cached per logging class before the SDK is installed picks it up once `use()` runs, and follows a later replacement (for example, the bootstrap restoring the no-op instance on shutdown). - Publishing: add `otel-backend-bootstrap` to the root `spinePublishing.modules`, and configure `otel-backend` (a KMP module) with `kmp-publish` + `customPublishing` so it publishes as `spine-logging-otel-backend`. Verified via `publishToMavenLocal`. - Update the backend tests for the lazy-resolution change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Raise patch coverage for the OpenTelemetry backend: exercise the single and repeated attribute value-type branches (numeric widening; boolean/double/string lists; non-primitive `toString` fallback), the unknown line-number / source-file log site, the factory `toString()`, and the bootstrap default-endpoint install. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the `spine.` prefix on metadata-derived attributes. Spine labels are dot-free identifiers and cannot collide with OpenTelemetry's dotted semantic conventions, so the prefix was convention, not collision-prevention — metadata keys now map to bare attributes named after their label. Keep a prefix for `Tags`, de-`spine`d to `tag.`: tag names and metadata labels share the same identifier namespace, so a same-named tag and metadata key would otherwise overwrite each other. The KDoc now states that actual rationale. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
otel-backend-bootstrap: - Document DEFAULT_OTLP_HTTP_ENDPOINT as the OpenTelemetry-standard port and its override paths; resolve the endpoint through a testable endpointFromEnvironment(). - Build the lifecycle test against a free ephemeral port instead of 4318 so it cannot collide with a real collector or a parallel suite; verify default resolution as pure logic. - Report a failed shutdown() (OperationResultCode.Failure) via JUL instead of discarding it silently. backends: - Add BackendFactory.loggerName() as the single home for the `$`-to-`.` logger-name convention and adopt it in the JUL, Log4j2, and OTel factories. JUL now derives the name in its factory; the backend no longer re-converts. - Fix stale KDoc: "Spine Logging backends", and drop the spine.* attribute mention removed in 4e467c5. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds an OpenTelemetry (OTel) logging backend to Spine Logging, mapping LogData to OTel log records (including severity, attributes, log-site code.*, tags, and span correlation) and providing a small JVM bootstrap module for turnkey OTLP/HTTP wiring.
Changes:
- Introduces
otel-backend(KMP module, JVM target) withLogData→Logger.emit(...)mapping and a log-based events API (logEvent/EVENT_NAME). - Adds
otel-backend-bootstrap(JVM module) to install an OTLP/HTTP pipeline and manage SDK lifecycle viaAutoCloseable. - Updates build/publishing wiring, docs, and aligns existing backends on a shared logger-name convention (
$→.).
Reviewed changes
Copilot reviewed 29 out of 31 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| settings.gradle.kts | Renames root project and includes the new OTel backend modules. |
| README.md | Updates project status messaging and documents the OTel backend entry. |
| logging/src/commonMain/kotlin/io/spine/logging/backend/BackendFactory.kt | Adds shared loggerName() helper for consistent backend logger naming. |
| docs/otel-backend.md | New guide describing setup, mapping, and events for the OTel backend. |
| docs/dependencies/pom.xml | Refreshes dependency report to include OTel Kotlin artifacts and related deps. |
| buildSrc/src/main/kotlin/io/spine/dependency/lib/OpenTelemetryKotlin.kt | Adds centralized dependency coordinates for opentelemetry-kotlin (0.4.0). |
| build.gradle.kts | Adds otel-backend-bootstrap to published modules list. |
| backends/otel-backend/src/jvmTest/kotlin/io/spine/logging/backend/otel/OtelLoggerBackendSpec.kt | Adds JVM tests covering mapping behavior, events, and span correlation. |
| backends/otel-backend/src/jvmTest/kotlin/io/spine/logging/backend/otel/given/StubMetadata.kt | Test fixture for mutable Metadata. |
| backends/otel-backend/src/jvmTest/kotlin/io/spine/logging/backend/otel/given/StubLogSite.kt | Test fixture for LogSite. |
| backends/otel-backend/src/jvmTest/kotlin/io/spine/logging/backend/otel/given/StubLogData.kt | Test fixture for mutable LogData. |
| backends/otel-backend/src/jvmTest/kotlin/io/spine/logging/backend/otel/given/RecordingLogRecordProcessor.kt | Test processor to capture emitted OTel log records. |
| backends/otel-backend/src/jvmTest/kotlin/io/spine/logging/backend/otel/given/NoOpSpanProcessor.kt | Test span processor to enable valid span contexts for correlation tests. |
| backends/otel-backend/src/jvmMain/kotlin/io/spine/logging/backend/otel/OtelBackendFactory.kt | JVM BackendFactory registered via @AutoService / KSP. |
| backends/otel-backend/src/commonMain/kotlin/io/spine/logging/backend/otel/SeverityMapping.kt | Implements Level → SeverityNumber threshold mapping. |
| backends/otel-backend/src/commonMain/kotlin/io/spine/logging/backend/otel/OtelLoggerBackend.kt | Implements the core LoggerBackend mapping and error handling. |
| backends/otel-backend/src/commonMain/kotlin/io/spine/logging/backend/otel/OtelEvents.kt | Adds EVENT_NAME metadata key and WithLogging.logEvent(...) helper. |
| backends/otel-backend/src/commonMain/kotlin/io/spine/logging/backend/otel/OtelBackendSettings.kt | Adds injectable global OpenTelemetry instance holder (defaults to noop). |
| backends/otel-backend/src/commonMain/kotlin/io/spine/logging/backend/otel/AttributeMapping.kt | Implements metadata/tags/log-site → OTel attributes mapping. |
| backends/otel-backend/README.md | Module README linking to the full OTel backend guide. |
| backends/otel-backend/build.gradle.kts | Configures KMP module deps, publishing, and JVM test task setup (JUnit Platform). |
| backends/otel-backend-bootstrap/src/test/kotlin/io/spine/logging/backend/otel/bootstrap/OtelLoggingSpec.kt | Adds tests for endpoint resolution and install/shutdown path. |
| backends/otel-backend-bootstrap/src/main/kotlin/io/spine/logging/backend/otel/bootstrap/OtelLogging.kt | Adds OTLP/HTTP bootstrap installer with shutdown handling and warning-on-failure. |
| backends/otel-backend-bootstrap/build.gradle.kts | Configures bootstrap module dependencies (SDK + exporters + coroutines). |
| backends/log4j2-backend/src/main/kotlin/io/spine/logging/backend/log4j2/Log4j2BackendFactory.kt | Switches to shared BackendFactory.loggerName() convention. |
| backends/jul-backend/src/main/kotlin/io/spine/logging/backend/jul/JulBackendFactory.kt | Switches to shared BackendFactory.loggerName() convention. |
| backends/jul-backend/src/main/kotlin/io/spine/logging/backend/jul/JulBackend.kt | Updates KDoc to reflect logger-name derivation now handled in the factory. |
| backends/jul-backend/src/main/kotlin/io/spine/logging/backend/jul/AbstractJulBackend.kt | Removes redundant $ → . conversion (now done by factories). |
| .agents/tasks/otel-backend-report.md | Adds internal research/spec document for the OTel backend work. |
| .agents/tasks/otel-backend-implementation.md | Adds internal implementation plan / execution log for the OTel backend work. |
`WithLogging.logEvent` now takes `message: () -> String` instead of a `String`, so
the body is built only when the event is enabled — matching the fluent `log {}` API
and the "a disabled event costs nothing" contract. The default still resolves to the
event name, and the trailing-lambda form reads naturally: `logEvent(name) { "body" }`.
Adds a test covering the supplied-message path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4c59160d53
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #143 +/- ##
============================================
+ Coverage 14.20% 17.67% +3.46%
- Complexity 120 149 +29
============================================
Files 102 109 +7
Lines 2443 2558 +115
Branches 379 404 +25
============================================
+ Hits 347 452 +105
- Misses 2065 2068 +3
- Partials 31 38 +7 🚀 New features to boost your workflow:
|
- AttributeMapping.putValues: dispatch on the whole list, not just its first element, so a repeated<Any> key holding mixed types falls back to a string list instead of risking a ClassCastException. Adds a regression test. - OtelLogging.close(): also warn (via JUL) when the SDK is not a TelemetryCloseable, so a skipped shutdown is never silent — previously only OperationResultCode.Failure warned. - docs/otel-backend.md: note that programmatic setup (OtelBackendSettings.use / logEvent) needs the backend as an `implementation` dependency, not `runtimeOnly`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address Codex review: `endpointFromEnvironment()` now prefers the signal-specific `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` over the generic `OTEL_EXPORTER_OTLP_ENDPOINT` before the localhost default, so logs reach the configured collector. The value is a base URL — the bundled `otlpHttpLogRecordExporter` always appends `/v1/logs` — kept consistent with the default and the generic variable. Resolution is split into a `getenv`-injected overload so the precedence is covered by deterministic unit tests without mutating the process environment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract SDK shutdown reporting into a testable `internal reportShutdown(sdk, result, logger)` so both warning paths (non-closeable SDK, OperationResultCode.Failure) and the silent-success path are unit-tested via a capturing JUL handler. Add smoke tests for the no-arg installOtlpHttp() and fromEnvironment() wiring. Raises otel-backend-bootstrap src/main line coverage from 75% to ~97%, clearing the codecov/project threshold; the new module's uncovered warning branches were dragging the src/main aggregate below base. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
The OpenTelemetry logging backend — the OTel half of the branch split. The config
sync and version bump live in #142 (
update-config → master). The original 9 OTelcommits are preserved here.
Base: this PR is stacked on
update-config(notmaster).Contents
otel-backend(KMP) — maps SpineLogDatato OTel log records: severitymapping, attribute mapping (incl. repeated values and the
tag.namespace),log-site
code.*attributes, event-name routing, and span correlation.otel-backend-bootstrap(JVM) —OtelLogginginstall helpers (OTLP/HTTP andfrom-environment) returning an
AutoCloseable.OpenTelemetryKotlindependency declaration (opentelemetry-kotlin0.4.0).docs/otel-backend.md, the module README, and a README backends entry.Verification
./gradlew build dokkaGenerateis green;otel-backend:jvmTestandotel-backend-bootstrap:testpass. The dependency report includes the OTel deps.🤖 Generated with Claude Code