Claude Skill

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

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download ollygarden-opentelemetry-agent-skills-skills_otel-span-events-to-logs-migration-9671795.zip · 13 KB
Part of ollygarden/opentelemetry-agent-skills — 19 skills

Install

skills CLI npx skills add https://github.com/ollygarden/opentelemetry-agent-skills/tree/main/skills/otel-span-events-to-logs-migration
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install ollygarden-opentelemetry-agent-skills@llmmart
Git 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

  1. 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
  1. 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
  1. 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)
  1. 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
  1. 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.
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.

No comments yet.

Reviews (0)

No reviews yet.

Related