otel-span-events-to-logs-migration
Migrate OpenTelemetry Span Events (AddEvent, RecordException, and language equivalents) to the Logs API following the accepted OTEP 4430 migration plan. Use when migrating instrumentation from span events to log-based events, reviewing code that still uses AddEvent or RecordExcep
Install
npx skills add https://github.com/ollygarden/opentelemetry-agent-skills/tree/main/skills/otel-span-events-to-logs-migration
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install ollygarden-opentelemetry-agent-skills@llmmart
git clone https://github.com/ollygarden/opentelemetry-agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole ollygarden/opentelemetry-agent-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Span Events to Logs Migration
Use this skill to migrate instrumentation from the Span Event API (AddEvent, RecordException, and language equivalents) to the Logs API, following the accepted OTEP 4430 deprecation plan.
Background
The OpenTelemetry project accepted a plan to deprecate Span.AddEvent and Span.RecordException in favor of emitting events and exceptions through the Logs API. Span Events as a concept remain valid -- they can be emitted via logs that correlate to the active span, and optionally bridged back into the span proto.
Status as of 2026-10-03: OTEP 4430 is accepted, log-based event/exception emission is specified in the Logs API, and the SDK "event to span event bridge" is specified with Development status. The trace API methods AddEvent/RecordException are not yet formally marked Deprecated in the specification -- that step is still pending. Treat existing span-event calls as migration candidates, not automatically invalid code; some SDK-specific equivalents have already changed status (for example OpenTelemetry .NET's Activity.RecordException extension is [Obsolete] in favor of Activity.AddException, which is still a span-event API).
See references/deprecation-plan.md for the full context.
Workflow
- Prepare before migrating.
- check the project's OpenTelemetry SDK version supports log-based events (check the
otel-sdk-versionsskill's version index if available) - identify whether the project has a LoggerProvider configured; if not, one must be set up
- determine if downstream consumers (backends, dashboards, alerts) depend on span events appearing in the span proto envelope
- Scan the codebase for span event usage.
- search for
AddEvent,add_event,addEvent,RecordException,record_exception,recordException,RecordError,record_error,AddException, and language-specific variants - categorize each call site: general event, exception recording, or informational annotation
- note the span context, attributes, and timestamp usage at each site
- Classify each call site using the decision tree.
- see
references/decision-tree.mdfor the full classification logic - the three outcomes are: migrate to log-based event, convert to span attributes, or remove
- Apply the migration for each call site.
- see
references/migration-patterns.mdfor language-specific before/after patterns - ensure the replacement log record carries the correct span context, event name, attributes, and timestamp; for exceptions use the applicable semantic-convention event name (normally an operation-specific
.exceptionname), reservingexceptionfor generic handlers - in Go (1.47.0+, Logs API/SDK stable), get loggers with
otel.Loggerfrom the rootgo.opentelemetry.io/otelpackage (otel/log/globalis deprecated) - in Java, the contrib
io.opentelemetry.contrib.eventbridge.EventToSpanEventBridgewas removed in opentelemetry-java-contrib v1.60.0; useio.opentelemetry.sdk.extension.incubator.logs.EventToSpanEventBridgefromopentelemetry-sdk-extension-incubator(seereferences/backward-compat.md) - in Go,
log.Record.AddAttributesacceptsattribute.KeyValue: useattribute.String(...), notlog.String(...); set the event name withRecord.SetEventName(...) - for exceptions, preserve the applicable semconv attributes:
exception.typeandexception.message(at least one is required), plusexception.stacktracewhen the language/error type makes it meaningful (in Go, omit it unless an error library preserves the origin stack -- do not callruntime.Stackat the emit site)
- If backward compatibility is needed, configure the SDK bridge.
- see
references/backward-compat.md - this is an SDK-level log processor that converts log-based events back to span events
- only needed when downstream systems require span events in the same proto envelope as the span
- Verify the migration.
Required Completion Loop
Follow this loop every time:
- scan and classify all span event call sites
- migrate each call site following the decision tree and patterns
- review the changed code against the checklist below
- re-open the changed files and confirm each checklist item with codebase evidence
- if any item is unresolved, patch the code or mark it not applicable with a reason, then repeat the review
- do not finish until every checklist item is completed or explicitly marked not applicable
Do not mark a checklist item complete based on intent alone. Mark it complete only after confirming it in the current codebase.
Migration Checklist
For every item, report one of these statuses in the final answer:
[x]completed[~]not applicable, with a reason[ ]unresolved
Include file references as evidence for every completed item.
[ ]All general span-event call sites identified and classified, includingAddEvent/add_event/addEventand language equivalents such as .NETActivityEvent.[ ]All exception span-event call sites identified and classified, includingRecordException/record_exception/recordExceptionand language equivalents such as GoRecordError, Rustrecord_error, and .NETAddException.[ ]A LoggerProvider is configured in the SDK setup (or already existed).[ ]Each migrated event uses the Logs API with the correct event name and attributes.[ ]Each migrated exception preserves the applicable semconv attributes:exception.typeandexception.message(at least one is required), plusexception.stacktracewhen the language/error type makes it meaningful.[ ]Migrated log records carry the active span context for trace correlation.[ ]Call sites classified as "convert to span attributes" now use span attributes instead.[ ]Call sites classified as "remove" have been removed with justification.[ ]Backward compatibility bridge is configured if downstream systems require span events in the span envelope.[ ]No remaining span-event API call sites are left unintentionally; any retainedAddEvent/RecordException/RecordError/AddException/ActivityEvent/ equivalent call is justified for current-version compatibility.[ ]The changed files were re-read after implementation to verify the final state.[ ]The final answer includes this checklist, file evidence, and any remaining risks or gaps.
Final Review Format
In the final answer, include the checklist in this format:
[x]LoggerProvider configured. Evidence:src/telemetry/setup.go:42-- added OTLP log exporter with batch processor.[~]Backward compatibility bridge. Reason: no downstream systems depend on span events in the proto envelope.[ ]Exception migration. Missing evidence; re-check required.
Files (opentelemetry-agent-skills)
-
references
-
backward-compat.md 4.9 KB
# Backward Compatibility: The SDK Bridge When downstream systems (backends, dashboards, alerting rules) depend on span events appearing in the same proto envelope as the span, the SDK bridge preserves this behavior after migration. ## What It Is An SDK-based log processor that: 1. Intercepts log records that represent events (identified by a non-empty `event_name` LogRecord field) 2. Converts them to span events on the current span, when the record's `TraceId`/`SpanId` match the current recording span 3. Copies the record's timestamp and attributes onto the span event This means the log-based event appears as a traditional span event in the exported span data, while also being available as a log record if a log exporter is configured. Bridging does not remove the record from the normal log pipeline. The bridge is now specified in the OpenTelemetry Specification as the "Event to span event bridge" [LogRecordProcessor](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.61.0/specification/logs/sdk.md#event-to-span-event-bridge) (Status: Development), which defines these exact bridging conditions. ## When to Use It Use the bridge when: - a backend requires span events in the span proto (e.g., for timeline visualization) - alerting rules query span events directly - dashboards depend on span event fields - migrating incrementally and some consumers have not adapted yet Do NOT use the bridge when: - the backend already supports log-based events correlated to traces - you are starting fresh with no legacy consumers - you want a clean break from span events ## How to Configure It ### Via Declarative Configuration When available for the language SDK, add the bridge processor to the log pipeline in the declarative config. The processor key is defined in the [opentelemetry-configuration v1.2.0 schema](https://github.com/open-telemetry/opentelemetry-configuration/blob/v1.2.0/schema/logger_provider.yaml) as `event_to_span_event_bridge/development` (the `/development` suffix marks it experimental): ```yaml # OpenTelemetry SDK declarative configuration logger_provider: processors: - event_to_span_event_bridge/development: {} - batch: exporter: otlp_http: endpoint: "http://collector:4318" ``` A schema key does not imply SDK support. As of 2026-10-03, Java 1.66.0 declarative config maps this key to its incubator bridge. The JavaScript Node SDK (experimental 0.222.0) accepts the key when parsing, but processor construction fails (`unknown LogRecordProcessor name`), so `startNodeSDK()` logs the error and returns a no-op SDK for that declarative initialization (programmatic setups such as `new NodeSDK()` are unaffected). Check the target SDK's processor-construction path, not just its parser, before relying on it. ### Via Code The bridge is a log record processor. Add a bridge implementation provided by the target language SDK to the LoggerProvider alongside any export processors. Do not infer support from the specification alone: the specification says SDKs SHOULD provide the processor, and implementation availability varies by language. For example, OpenTelemetry Java 1.66.0 provides the bridge in the incubator SDK extension: ```java // Java example -- opentelemetry-sdk-extension-incubator SdkLoggerProvider loggerProvider = SdkLoggerProvider.builder() .addLogRecordProcessor(EventToSpanEventBridge.create()) .addLogRecordProcessor(BatchLogRecordProcessor.builder(otlpExporter).build()) .build(); ``` The specification does not require a particular ordering between the bridge and other processors. Follow the target SDK's processor-composition semantics. If you only want span events and do not want separate log export, configure only the bridge where the SDK permits that pipeline. ## Migration Path 1. Start with the bridge enabled 2. Update downstream consumers to read from logs instead of span events 3. Once all consumers are migrated, remove the bridge 4. The bridge is a transitional tool, not a permanent fixture ## Reference Implementations - Specification: [Event to span event bridge](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.61.0/specification/logs/sdk.md#event-to-span-event-bridge) - Java: `io.opentelemetry.sdk.extension.incubator.logs.EventToSpanEventBridge` in `opentelemetry-sdk-extension-incubator` - Java bridge source: [`EventToSpanEventBridge`](https://github.com/open-telemetry/opentelemetry-java/blob/v1.66.0/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/logs/EventToSpanEventBridge.java). The contrib predecessor, removed in opentelemetry-java-contrib v1.60.0, was `io.opentelemetry.contrib:opentelemetry-processors`'s `io.opentelemetry.contrib.eventbridge.EventToSpanEventBridge`; its [v1.57.0 release documentation](https://github.com/open-telemetry/opentelemetry-java-contrib/tree/v1.57.0/processors#event-to-spanevent-bridge) directs users to this SDK incubator implementation. -
decision-tree.md 2.3 KB
# Decision Tree: Classifying Span Event Call Sites For each `AddEvent` or `RecordException` call site, follow this tree to determine the migration action. ## Step 1: Is It an Exception? If the call uses `RecordException` (or `AddEvent` with `exception.*` attributes): - **Action: Migrate to log-based exception** - Use the Logs API to emit the applicable semantic-convention event name, normally an operation-specific name with a `.exception` suffix; use `exception` for generic handlers not tied to an operation or domain - Preserve the applicable exception semantic-convention attributes. Use SDK exception support when available; otherwise set `exception.type` and `exception.message` when known, plus `exception.stacktrace` when the language/error object carries a meaningful stack. - The log record must carry the active span context ## Step 2: Is It a Timestamped Diagnostic Event? If the event represents a discrete occurrence with its own timestamp that aids debugging: - Examples: retry attempt, state transition, circuit breaker trip, cache miss, fallback activation - **Action: Migrate to log-based event** - Use the Logs API to emit an event with the same name and attributes - The log record must carry the active span context ## Step 3: Is It Really Just Extra Span Detail? If the event records additional details about the span that do not need their own timestamp: - Examples: request/response body snippets, configuration values, resolved parameters, feature flag states - **Action: Convert to span attributes** - Move the data to attributes on the span itself - Use semantic conventions where applicable - See [semantic-conventions#2010](https://github.com/open-telemetry/semantic-conventions/issues/2010) ## Step 4: Is It Noise? If the event provides no diagnostic value or duplicates information already on the span or in metrics: - Examples: "processing started", "step 1 complete", happy-path confirmations - **Action: Remove** - Document the removal reason in the commit or PR ## Summary Table | Pattern | Action | Target | |---|---|---| | `RecordException` / exception event | Migrate | Log-based exception via Logs API | | Timestamped diagnostic event | Migrate | Log-based event via Logs API | | Detail without own timestamp | Convert | Span attributes | | Noise or duplication | Remove | Delete the call | -
deprecation-plan.md 6.4 KB
# OTEP 4430: Span Event API Deprecation Plan This reference summarizes the [OTEP 4430](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.61.0/oteps/4430-span-event-api-deprecation-plan.md) deprecation plan. ## Planned Deprecation Targets - `Span.AddEvent` -- the API method for attaching events to spans - `Span.RecordException` -- the API method for recording exceptions on spans As of 2026-10-03, these methods are planned deprecation targets from the accepted OTEP. They are not yet marked Deprecated in `specification/trace/api.md`. ## What Is NOT Being Deprecated - Span Events as a concept -- they can still be emitted via the Logs API - The ability to have events correlated with spans in the same proto envelope (via the SDK bridge) ## Why - Provide a single consistent guidance: use the Logs API for events - Reduce API surface duplication between spans and logs - Events emitted via logs are more flexible (can be exported as logs, as span events, or both) ## Phased Rollout ### In the proto Stabilize log-based Events. (The `event_name` LogRecord field is part of the stable proto.) ### Specification 1. Stabilize emitting exceptions and events via the Logs API 2. Mark `Span.RecordException` as deprecated 3. Mark `Span.AddEvent` as deprecated (can happen in parallel with 2) ### Per API and SDK 1. Implement and stabilize log-based exception and event emission 2. Implement the backward-compatibility SDK bridge (log processor that converts log-based events to span events) 3. Mark `Span.RecordException` as deprecated 4. Mark `Span.AddEvent` as deprecated (can happen in parallel with 3) ### Per Instrumentation - Current major version: continue using existing span event methods - Next major version: migrate to the Logs API; for span-detail-without-a-timestamp cases, record span attributes instead ([semantic-conventions#2010](https://github.com/open-telemetry/semantic-conventions/issues/2010), [opentelemetry-specification#4446](https://github.com/open-telemetry/opentelemetry-specification/issues/4446)) - Users opt into the SDK bridge if they need span events in the proto envelope ## Current Status (2026-10-03) - **Proto 1.11.1**: log-based Events are stable; `event_name` is a stable LogRecord field. - **Specification 1.61.0**: the Logs API is Stable, including the `event_name` field and the optional `Exception` parameter to Emit, so log-based exception/event emission is specified. The "event to span event bridge" `LogRecordProcessor` is specified in `specification/logs/sdk.md` (Status: Development), with a matching `event_to_span_event_bridge/development` declarative-config key. - **Not yet done in spec**: `Span.AddEvent` and `Span.RecordException` are **not** yet marked Deprecated in `specification/trace/api.md`. That step remains pending. - **Semantic conventions 1.44.0**: exception events normally use an operation-specific name with a `.exception` suffix; the generic `exception` name is for handlers not tied to an operation or domain. Existing instrumentations introducing log-based exceptions should use `OTEL_SEMCONV_EXCEPTION_SIGNAL_OPT_IN` with `logs` or `logs/dup` for a phased rollout while keeping span events as the default in the current major version. - **SDKs**: implementation status varies. Do not claim all `AddEvent`/`RecordException` equivalents are deprecated unless that language SDK marks them so. ## Language Status Snapshot Use current released source for the target language before editing user code. The snapshot below was verified against synced upstream checkouts and exact released tags on 2026-10-03: | Language | Current migration-relevant status | |---|---| | Go 1.47.0 (trace, log, sdk/log) | `trace.Span.AddEvent` / `RecordError` are present and not marked deprecated. The Logs API and SDK (`go.opentelemetry.io/otel/log`, `go.opentelemetry.io/otel/sdk/log`) are stable as of 1.47.0; get loggers with `otel.Logger` from the root `go.opentelemetry.io/otel` package (`go.opentelemetry.io/otel/log/global` is deprecated). `log.Record.SetEventName` and `SetErr` are available; the log SDK derives `exception.type` and `exception.message` from the error. No event-to-span-event bridge implementation was found in these releases. | | Java 1.66.0 | `Span.addEvent` / `recordException` are present and not marked deprecated; `LogRecordBuilder.setEventName` is available since 1.50.0 and `setException(Throwable)` since 1.60.0. The bridge is available in `opentelemetry-sdk-extension-incubator`; declarative config wires the `event_to_span_event_bridge/development` key to it. The older Java contrib bridge was removed in opentelemetry-java-contrib v1.60.0. | | JavaScript / TypeScript 2.11.0 / experimental 0.222.0 | `Span.addEvent` / `recordException` are present and not marked deprecated; `@opentelemetry/api-logs` / `@opentelemetry/sdk-logs` support `eventName`; the `exception` log-record field is marked experimental. No event-to-span-event bridge implementation was found in these releases. | | Python 1.45.0 / 0.66b0 | `span.add_event` / `record_exception` are present and not marked deprecated; `opentelemetry._logs.Logger.emit` accepts `event_name` and `exception`, and the SDK derives exception attributes. The deprecated standalone Events API/SDK was removed in favor of `LogRecord` with `event_name`. No event-to-span-event bridge implementation was found in this release. | | .NET 1.19.1 | `ActivityExtensions.RecordException` is `[Obsolete]` and points to `Activity.AddException`; both record span events. `TelemetrySpan.AddEvent` / `RecordException` are not marked obsolete. `ILogger` supplies an event name through `EventId.Name`; the lower-level Logs API is pre-release. No event-to-span-event bridge implementation was found in this release. | | Ruby API 1.11.1 / Logs API 0.5.0 / Logs SDK 0.7.0 | `Span#add_event` / `record_exception` are present and not marked deprecated. `Logger#on_emit` accepts `event_name` and inherits the current span context, but has no exception parameter; set `exception.*` attributes explicitly. No event-to-span-event bridge implementation was found in these releases. | Treat the deprecation as the accepted direction, not a completed spec change; verify the current status of each step against the linked sources before making hard claims. ## Key Principle The migration is about WHERE events are emitted (Logs API instead of Span API), not about removing event data from spans. The SDK bridge ensures span events can still appear in the span proto when needed. -
migration-patterns.md 9.6 KB
# Migration Patterns: Before and After Language-specific patterns for migrating from `AddEvent` / `RecordException` to the Logs API. When applying these patterns, always check the project's actual SDK imports and version. The examples below use idiomatic API calls; adapt to the project's style. Do not assume the span-event method is formally deprecated in the target SDK. The migration target comes from OTEP 4430 and per-language release status. In exception examples, `exceptionEventName` / `exception_event_name` is the event name defined by the applicable semantic convention, normally an operation-specific name with a `.exception` suffix. Use the generic `exception` name only for handlers not tied to an operation or domain. ## Go The examples import `go.opentelemetry.io/otel` (for `otel.Logger`) and `go.opentelemetry.io/otel/log`. ### Exception Recording Before: ```go span.RecordError(err) span.SetStatus(codes.Error, err.Error()) ``` After: ```go // Use the event logger to record the exception as a log-based event logger := otel.Logger("my-package") record := log.Record{} record.SetTimestamp(time.Now()) record.SetEventName(exceptionEventName) record.SetSeverity(log.SeverityError) record.SetErr(err) // the SDK derives exception.type and exception.message // exception.stacktrace is intentionally omitted: the Go error value does // not carry its origin stack, and capturing runtime.Stack here would // point at the emit site, not where the error arose. Per semconv, the // attribute is Recommended, not Required. If the project uses an error // library that preserves the origin stack (e.g., github.com/pkg/errors, // github.com/cockroachdb/errors), see the note below. Do not call // runtime.Stack here. logger.Emit(ctx, record) span.SetStatus(codes.Error, err.Error()) ``` Go log SDK 1.47.0 derives `exception.type` and `exception.message` from `SetErr`. An explicitly supplied exception attribute is preserved, while each missing `exception.type` or `exception.message` attribute is derived independently. When an error library preserves the origin stack, add `exception.stacktrace` and let the SDK derive whichever of type or message you did not set explicitly. Prefer `record.SetEventName(name)` over adding an `event.name` attribute in Go: it is a first-class field on `log.Record` and produces cleaner output in backends that special-case event records. ### General Event Before: ```go span.AddEvent("cache.miss", trace.WithAttributes( attribute.String("cache.key", key), )) ``` After: ```go logger := otel.Logger("my-package") record := log.Record{} record.SetTimestamp(time.Now()) record.SetEventName("cache.miss") record.AddAttributes( attribute.String("cache.key", key), ) logger.Emit(ctx, record) ``` ### Convert to Span Attributes Before: ```go span.AddEvent("request.details", trace.WithAttributes( attribute.String("http.request.body.summary", summary), )) ``` After: ```go span.SetAttributes( attribute.String("http.request.body.summary", summary), ) ``` ## Python ### Exception Recording Before: ```python span.record_exception(exc) span.set_status(Status(StatusCode.ERROR, str(exc))) ``` After: ```python from opentelemetry._logs import SeverityNumber, get_logger logger = get_logger(__name__) logger.emit( event_name=exception_event_name, severity_number=SeverityNumber.ERROR, body="exception", exception=exc, ) span.set_status(Status(StatusCode.ERROR, str(exc))) ``` ### General Event Before: ```python span.add_event("retry.attempt", attributes={"retry.count": count}) ``` After: ```python from opentelemetry._logs import get_logger logger = get_logger(__name__) logger.emit( event_name="retry.attempt", body="retry.attempt", attributes={"retry.count": count}, ) ``` The Python SDK derives exception semantic-convention attributes when `exception=` is provided. If the project already uses stdlib `logging` with `LoggingHandler`, verify how that bridge maps event names before relying on it for `event_name`. ## Java ### Exception Recording Before: ```java span.recordException(exception); span.setStatus(StatusCode.ERROR, exception.getMessage()); ``` After: ```java Logger logger = GlobalOpenTelemetry.get().getLogsBridge().loggerBuilder("my-class").build(); logger.logRecordBuilder() .setSeverity(Severity.ERROR) .setEventName(exceptionEventName) .setException(exception) .emit(); span.setStatus(StatusCode.ERROR, exception.getMessage()); ``` `setEventName(...)` is a first-class method on the stable `LogRecordBuilder` (since 1.50.0); `setException(Throwable)` is available since 1.60.0. Prefer these over setting an `event.name` attribute or manually maintaining `exception.*` attributes when the project is on a current Java SDK. ### General Event Before: ```java span.addEvent("state.transition", Attributes.of( AttributeKey.stringKey("state.from"), oldState, AttributeKey.stringKey("state.to"), newState )); ``` After: ```java logger.logRecordBuilder() .setEventName("state.transition") .setAttribute(AttributeKey.stringKey("state.from"), oldState) .setAttribute(AttributeKey.stringKey("state.to"), newState) .emit(); ``` ## JavaScript / TypeScript ### Exception Recording Before: ```typescript span.recordException(error); span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); ``` After: ```typescript const logger = logs.getLogger('my-module'); logger.emit({ severityNumber: SeverityNumber.ERROR, eventName: exceptionEventName, body: 'exception', exception: error, }); span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); ``` The JS log-record `exception` field is present in current `@opentelemetry/api-logs` / `@opentelemetry/sdk-logs` source and marked experimental. If the project avoids experimental fields, set `exception.type`, `exception.message`, and `exception.stacktrace` attributes manually. ### General Event Before: ```typescript span.addEvent('queue.enqueue', { 'queue.name': queueName, 'queue.size': size }); ``` After: ```typescript logger.emit({ eventName: 'queue.enqueue', attributes: { 'queue.name': queueName, 'queue.size': size, }, }); ``` ## .NET Note: OpenTelemetry .NET's `Activity.RecordException(...)` extension is `[Obsolete]` (it points callers to the runtime's `Activity.AddException(ex)`). Both `RecordException` and `AddException` record an exception *span event*, so either one is a migration source for this skill -- the target is still the Logs API as shown below. ### Exception Recording Before: ```csharp activity?.RecordException(ex); // or the newer activity?.AddException(ex); activity?.SetStatus(ActivityStatusCode.Error, ex.Message); ``` After: ```csharp var logger = loggerFactory.CreateLogger("MyClass"); logger.LogError(new EventId(0, exceptionEventName), ex, "{ExceptionEventName}", exceptionEventName); activity?.SetStatus(ActivityStatusCode.Error, ex.Message); ``` ### General Event Before: ```csharp activity?.AddEvent(new ActivityEvent("validation.failure", default, new ActivityTagsCollection { { "validation.field", fieldName }, { "validation.rule", rule }, })); ``` After: ```csharp logger.LogWarning( new EventId(0, "validation.failure"), "validation.failure for field {validation.field} rule {validation.rule}", fieldName, rule); ``` With the OpenTelemetry .NET OTLP exporter, `EventId.Name` is exported as the OTLP log `event_name` field. The lower-level `LogRecordData.EventName` / `Logger.EmitLog` bridge API exists in source but is experimental/pre-release-gated, so prefer stable `ILogger` patterns unless the project has opted into the bridge API. ## Ruby ### Exception Recording Before: ```ruby span.record_exception(exception) span.status = OpenTelemetry::Trace::Status.error(exception.message) ``` After: ```ruby logger = OpenTelemetry.logger_provider.logger(name: 'my-gem') logger.on_emit( event_name: exception_event_name, severity_number: OpenTelemetry::Logs::SeverityNumber::SEVERITY_NUMBER_ERROR, body: 'exception', attributes: { 'exception.type' => exception.class.name, 'exception.message' => exception.message, 'exception.stacktrace' => exception.full_message } ) span.status = OpenTelemetry::Trace::Status.error(exception.message) ``` ### General Event Before: ```ruby span.add_event('retry.attempt', attributes: { 'retry.count' => count }) ``` After: ```ruby logger.on_emit( event_name: 'retry.attempt', body: 'retry.attempt', attributes: { 'retry.count' => count } ) ``` Ruby Logs API 0.5.0 and Logs SDK 0.7.0 expose `Logger#on_emit` with `event_name:` and use the current context by default. They do not expose an exception convenience parameter, so populate the applicable `exception.*` attributes explicitly. ## Key Rules Across All Languages 1. The event name replaces the name from `AddEvent`. Use the dedicated event-name API where available -- `record.SetEventName(...)` (Go), `.setEventName(...)` (Java `LogRecordBuilder`), `eventName:` (JS `logger.emit`), `event_name=` (Python `Logger.emit`), `EventId.Name` (.NET `ILogger`), or `event_name:` (Ruby `Logger#on_emit`) -- which maps to the `event_name` LogRecord field. Only set an `event.name` attribute when the target SDK/exporter has no dedicated event-name path. 2. All original attributes transfer to the log record attributes, preserving the original attribute keys unless an intentional mapping is documented and tested. 3. The log record automatically inherits the active span context from `ctx` / the current context -- this is how trace correlation is maintained. 4. `span.SetStatus` (or equivalent) is still set on the span for error cases -- the migration only moves event emission, not status. 5. Timestamps are set automatically by the SDK if not specified explicitly.
-
-
scripts
-
scan-span-events.sh 2.5 KB
#!/usr/bin/env bash set -euo pipefail # Scans a directory for Span Event API usage targeted by the OTEP 4430 migration plan. # Outputs each match with file, line number, matched pattern, and a classification hint. usage() { cat <<'EOF' >&2 Usage: ./scripts/scan-span-events.sh <directory> ./scripts/scan-span-events.sh . Scans for Span Event API calls (AddEvent, RecordException, RecordError, AddException, ActivityEvent, and language-specific variants) and prints matches grouped by category. EOF } require_bin() { command -v "$1" >/dev/null 2>&1 || { echo "missing dependency: $1" >&2 exit 1 } } scan_pattern() { local dir="$1" local label="$2" local pattern="$3" local results results="$(grep -rn --include='*.go' --include='*.java' --include='*.py' \ --include='*.js' --include='*.ts' --include='*.tsx' --include='*.cs' \ --include='*.rb' --include='*.rs' --include='*.kt' --include='*.scala' \ -E "$pattern" "$dir" 2>/dev/null || true)" if [[ -n "$results" ]]; then printf '\n## %s\n\n' "$label" printf '%s\n' "$results" | while IFS= read -r line; do printf ' %s\n' "$line" done return 0 fi return 1 } main() { require_bin grep local dir="${1:-}" if [[ -z "$dir" || "$dir" == "-h" || "$dir" == "--help" ]]; then usage [[ -n "$dir" ]] && return 0 return 1 fi if [[ ! -d "$dir" ]]; then echo "error: '${dir}' is not a directory" >&2 return 1 fi local found=0 printf '# Span Event API Usage Scan\n' printf 'directory: %s\n' "$dir" # RecordException / AddException variants # AddException is the current .NET exception span-event API (the OTel # RecordException extension is obsolete); both record exception span events. if scan_pattern "$dir" "RecordException / AddException (exception recording)" \ '(RecordException|record_exception|recordException|RecordError|record_error|RecordErr|AddException)'; then found=1 fi # AddEvent variants if scan_pattern "$dir" "AddEvent (general events)" \ '\.(AddEvent|add_event|addEvent)\b'; then found=1 fi # Activity-based (.NET) if scan_pattern "$dir" "Activity span events (.NET)" \ '(activity\??\.(AddEvent|RecordException|AddException)|ActivityEvent)'; then found=1 fi if [[ "$found" -eq 0 ]]; then printf '\nNo Span Event API usage found.\n' else printf '\n---\n' printf 'total categories with matches: see sections above\n' printf 'classify each match using references/decision-tree.md\n' fi } main "$@"
-
-
SKILL.md 7.1 KB
--- name: otel-span-events-to-logs-migration description: Migrate OpenTelemetry Span Events (AddEvent, RecordException, and language equivalents) to the Logs API following the accepted OTEP 4430 migration plan. Use when migrating instrumentation from span events to log-based events, reviewing code that still uses AddEvent or RecordException, or planning a codebase migration while preserving trace correlation. --- # Span Events to Logs Migration Use this skill to migrate instrumentation from the Span Event API (`AddEvent`, `RecordException`, and language equivalents) to the Logs API, following the accepted [OTEP 4430 deprecation plan](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.61.0/oteps/4430-span-event-api-deprecation-plan.md). ## Background The OpenTelemetry project accepted a plan to deprecate `Span.AddEvent` and `Span.RecordException` in favor of emitting events and exceptions through the Logs API. Span Events as a concept remain valid -- they can be emitted via logs that correlate to the active span, and optionally bridged back into the span proto. Status as of 2026-10-03: OTEP 4430 is accepted, log-based event/exception emission is specified in the Logs API, and the SDK "event to span event bridge" is specified with Development status. The trace API methods `AddEvent`/`RecordException` are not yet formally marked Deprecated in the specification -- that step is still pending. Treat existing span-event calls as migration candidates, not automatically invalid code; some SDK-specific equivalents have already changed status (for example OpenTelemetry .NET's `Activity.RecordException` extension is `[Obsolete]` in favor of `Activity.AddException`, which is still a span-event API). See `references/deprecation-plan.md` for the full context. ## Workflow 0. Prepare before migrating. - check the project's OpenTelemetry SDK version supports log-based events (check the `otel-sdk-versions` skill's version index if available) - identify whether the project has a LoggerProvider configured; if not, one must be set up - determine if downstream consumers (backends, dashboards, alerts) depend on span events appearing in the span proto envelope 1. Scan the codebase for span event usage. - search for `AddEvent`, `add_event`, `addEvent`, `RecordException`, `record_exception`, `recordException`, `RecordError`, `record_error`, `AddException`, and language-specific variants - categorize each call site: general event, exception recording, or informational annotation - note the span context, attributes, and timestamp usage at each site 2. Classify each call site using the decision tree. - see `references/decision-tree.md` for the full classification logic - the three outcomes are: migrate to log-based event, convert to span attributes, or remove 3. Apply the migration for each call site. - see `references/migration-patterns.md` for language-specific before/after patterns - ensure the replacement log record carries the correct span context, event name, attributes, and timestamp; for exceptions use the applicable semantic-convention event name (normally an operation-specific `.exception` name), reserving `exception` for generic handlers - in Go (1.47.0+, Logs API/SDK stable), get loggers with `otel.Logger` from the root `go.opentelemetry.io/otel` package (`otel/log/global` is deprecated) - in Java, the contrib `io.opentelemetry.contrib.eventbridge.EventToSpanEventBridge` was removed in opentelemetry-java-contrib v1.60.0; use `io.opentelemetry.sdk.extension.incubator.logs.EventToSpanEventBridge` from `opentelemetry-sdk-extension-incubator` (see `references/backward-compat.md`) - in Go, `log.Record.AddAttributes` accepts `attribute.KeyValue`: use `attribute.String(...)`, not `log.String(...)`; set the event name with `Record.SetEventName(...)` - for exceptions, preserve the applicable semconv attributes: `exception.type` and `exception.message` (at least one is required), plus `exception.stacktrace` when the language/error type makes it meaningful (in Go, omit it unless an error library preserves the origin stack -- do not call `runtime.Stack` at the emit site) 4. If backward compatibility is needed, configure the SDK bridge. - see `references/backward-compat.md` - this is an SDK-level log processor that converts log-based events back to span events - only needed when downstream systems require span events in the same proto envelope as the span 5. Verify the migration. ## Required Completion Loop Follow this loop every time: 1. scan and classify all span event call sites 2. migrate each call site following the decision tree and patterns 3. review the changed code against the checklist below 4. re-open the changed files and confirm each checklist item with codebase evidence 5. if any item is unresolved, patch the code or mark it not applicable with a reason, then repeat the review 6. do not finish until every checklist item is completed or explicitly marked not applicable Do not mark a checklist item complete based on intent alone. Mark it complete only after confirming it in the current codebase. ## Migration Checklist For every item, report one of these statuses in the final answer: - `[x]` completed - `[~]` not applicable, with a reason - `[ ]` unresolved Include file references as evidence for every completed item. - `[ ]` All general span-event call sites identified and classified, including `AddEvent` / `add_event` / `addEvent` and language equivalents such as .NET `ActivityEvent`. - `[ ]` All exception span-event call sites identified and classified, including `RecordException` / `record_exception` / `recordException` and language equivalents such as Go `RecordError`, Rust `record_error`, and .NET `AddException`. - `[ ]` A LoggerProvider is configured in the SDK setup (or already existed). - `[ ]` Each migrated event uses the Logs API with the correct event name and attributes. - `[ ]` Each migrated exception preserves the applicable semconv attributes: `exception.type` and `exception.message` (at least one is required), plus `exception.stacktrace` when the language/error type makes it meaningful. - `[ ]` Migrated log records carry the active span context for trace correlation. - `[ ]` Call sites classified as "convert to span attributes" now use span attributes instead. - `[ ]` Call sites classified as "remove" have been removed with justification. - `[ ]` Backward compatibility bridge is configured if downstream systems require span events in the span envelope. - `[ ]` No remaining span-event API call sites are left unintentionally; any retained `AddEvent` / `RecordException` / `RecordError` / `AddException` / `ActivityEvent` / equivalent call is justified for current-version compatibility. - `[ ]` The changed files were re-read after implementation to verify the final state. - `[ ]` The final answer includes this checklist, file evidence, and any remaining risks or gaps. ## Final Review Format In the final answer, include the checklist in this format: - `[x]` LoggerProvider configured. Evidence: `src/telemetry/setup.go:42` -- added OTLP log exporter with batch processor. - `[~]` Backward compatibility bridge. Reason: no downstream systems depend on span events in the proto envelope. - `[ ]` Exception migration. Missing evidence; re-check required.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.