From 20f4dddba7a763b135172ec91c9dd533302d46bc Mon Sep 17 00:00:00 2001 From: Jack Berg Date: Mon, 18 Nov 2024 13:36:29 -0600 Subject: [PATCH 1/5] Add EventToSpanEventBridge --- processors/README.md | 30 +++- processors/build.gradle.kts | 8 + .../eventbridge/EventToSpanEventBridge.java | 130 ++++++++++++++ ...entToSpanEventBridgeComponentProvider.java | 36 ++++ ...toconfigure.spi.internal.ComponentProvider | 1 + .../EventToSpanEventBridgeTest.java | 161 ++++++++++++++++++ ...ventToSpanBridgeComponentProviderTest.java | 35 ++++ samplers/README.md | 2 +- 8 files changed, 401 insertions(+), 2 deletions(-) create mode 100644 processors/src/main/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridge.java create mode 100644 processors/src/main/java/io/opentelemetry/contrib/eventbridge/internal/EventToSpanEventBridgeComponentProvider.java create mode 100644 processors/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider create mode 100644 processors/src/test/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridgeTest.java create mode 100644 processors/src/test/java/io/opentelemetry/contrib/eventbridge/internal/EventToSpanBridgeComponentProviderTest.java diff --git a/processors/README.md b/processors/README.md index eda5acee6..5ae81cab0 100644 --- a/processors/README.md +++ b/processors/README.md @@ -1,6 +1,34 @@ # Processors -This module provides tools to intercept and process signals globally. +## Interceptable exporters + +This module provides tools to intercept and process signals globally: + +* `InterceptableSpanExporter` +* `InterceptableMetricExporter` +* `InterceptableLogRecordExporter` + +## Event to SpanEvent Bridge + +`EventToSpanEventBridge` is a `LogRecordProcessor` which records events (i.e. log records with an `event.name` attribute) as span events for the current span if: + +* The log record has a valid span context +* `Span.current()` returns a span where `Span.isRecording()` is true + +For details of how the event log record is translated to span event, see [EventToSpanEventBridge Javadoc](./src/main/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridge.java). + +`EventToSpanEventBridge` can be referenced in [declarative configuration](https://opentelemetry.io/docs/languages/java/configuration/#declarative-configuration) as follows: + +```yaml +// Configure tracer provider as usual, omitted for brevity +tracer_provider: ... + +logger_provider: + processors: + - event_to_span_event_bridge: {} +``` + +// TODO(jack-berg): remove "{}" after merging / rle https://github.com/open-telemetry/opentelemetry-java/pull/6891/files ## Component owners diff --git a/processors/build.gradle.kts b/processors/build.gradle.kts index c24118550..de3575593 100644 --- a/processors/build.gradle.kts +++ b/processors/build.gradle.kts @@ -13,5 +13,13 @@ java { dependencies { api("io.opentelemetry:opentelemetry-sdk") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") + + // For EventToSpanEventBridge + implementation("io.opentelemetry:opentelemetry-exporter-otlp-common") + implementation("com.fasterxml.jackson.core:jackson-core") + testImplementation("io.opentelemetry:opentelemetry-sdk-testing") + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator") } diff --git a/processors/src/main/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridge.java b/processors/src/main/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridge.java new file mode 100644 index 000000000..86b1cb82c --- /dev/null +++ b/processors/src/main/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridge.java @@ -0,0 +1,130 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.eventbridge; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.exporter.internal.marshal.MarshalerWithSize; +import io.opentelemetry.exporter.internal.otlp.AnyValueMarshaler; +import io.opentelemetry.sdk.logs.LogRecordProcessor; +import io.opentelemetry.sdk.logs.ReadWriteLogRecord; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A processor that records events (i.e. log records with an {@code event.name} attribute) as span + * events for the current span if: + * + * + * + *

The event {@link LogRecordData} is converted to attributes on the span event as follows: + * + *

+ */ +public final class EventToSpanEventBridge implements LogRecordProcessor { + + private static final Logger LOGGER = Logger.getLogger(EventToSpanEventBridge.class.getName()); + + private static final AttributeKey EVENT_NAME = AttributeKey.stringKey("event.name"); + private static final AttributeKey LOG_RECORD_OBSERVED_TIME_UNIX_NANO = + AttributeKey.longKey("log.record.observed_time_unix_nano"); + private static final AttributeKey LOG_RECORD_SEVERITY_NUMBER = + AttributeKey.longKey("log.record.severity_number"); + private static final AttributeKey LOG_RECORD_BODY = + AttributeKey.stringKey("log.record.body"); + private static final AttributeKey LOG_RECORD_DROPPED_ATTRIBUTES_COUNT = + AttributeKey.longKey("log.record.dropped_attributes_count"); + + private EventToSpanEventBridge() {} + + /** Create an instance. */ + public static EventToSpanEventBridge create() { + return new EventToSpanEventBridge(); + } + + @Override + public void onEmit(Context context, ReadWriteLogRecord logRecord) { + LogRecordData logRecordData = logRecord.toLogRecordData(); + String eventName = logRecordData.getAttributes().get(EVENT_NAME); + if (eventName == null) { + return; + } + if (!logRecordData.getSpanContext().isValid()) { + return; + } + Span currentSpan = Span.current(); + if (!currentSpan.isRecording()) { + return; + } + currentSpan.addEvent( + eventName, + toSpanEventAttributes(logRecordData), + logRecordData.getTimestampEpochNanos(), + TimeUnit.NANOSECONDS); + } + + private static Attributes toSpanEventAttributes(LogRecordData logRecord) { + AttributesBuilder builder = + logRecord.getAttributes().toBuilder().removeIf(key -> key.equals(EVENT_NAME)); + + builder.put(LOG_RECORD_OBSERVED_TIME_UNIX_NANO, logRecord.getObservedTimestampEpochNanos()); + + builder.put(LOG_RECORD_SEVERITY_NUMBER, logRecord.getSeverity().getSeverityNumber()); + + // Add bridging for logRecord.getSeverityText() if EventBuilder adds severity text setter + + Value body = logRecord.getBodyValue(); + if (body != null) { + MarshalerWithSize marshaler = AnyValueMarshaler.create(body); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + marshaler.writeJsonTo(baos); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Error converting log record body to JSON", e); + } + builder.put(LOG_RECORD_BODY, new String(baos.toByteArray(), StandardCharsets.UTF_8)); + } + + int droppedAttributesCount = + logRecord.getTotalAttributeCount() - logRecord.getAttributes().size(); + if (droppedAttributesCount > 0) { + builder.put(LOG_RECORD_DROPPED_ATTRIBUTES_COUNT, droppedAttributesCount); + } + + return builder.build(); + } + + @Override + public String toString() { + return "EventToSpanEventBridge{}"; + } +} diff --git a/processors/src/main/java/io/opentelemetry/contrib/eventbridge/internal/EventToSpanEventBridgeComponentProvider.java b/processors/src/main/java/io/opentelemetry/contrib/eventbridge/internal/EventToSpanEventBridgeComponentProvider.java new file mode 100644 index 000000000..593e92f61 --- /dev/null +++ b/processors/src/main/java/io/opentelemetry/contrib/eventbridge/internal/EventToSpanEventBridgeComponentProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.eventbridge.internal; + +import io.opentelemetry.contrib.eventbridge.EventToSpanEventBridge; +import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; +import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties; +import io.opentelemetry.sdk.logs.LogRecordProcessor; + +/** + * Declarative configuration SPI implementation for {@link EventToSpanEventBridge}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public class EventToSpanEventBridgeComponentProvider + implements ComponentProvider { + + @Override + public Class getType() { + return LogRecordProcessor.class; + } + + @Override + public String getName() { + return "event_to_span_event_bridge"; + } + + @Override + public LogRecordProcessor create(StructuredConfigProperties config) { + return EventToSpanEventBridge.create(); + } +} diff --git a/processors/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider b/processors/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider new file mode 100644 index 000000000..48b28e31f --- /dev/null +++ b/processors/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider @@ -0,0 +1 @@ +io.opentelemetry.contrib.eventbridge.internal.EventToSpanEventBridgeComponentProvider diff --git a/processors/src/test/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridgeTest.java b/processors/src/test/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridgeTest.java new file mode 100644 index 000000000..4b9bb7355 --- /dev/null +++ b/processors/src/test/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridgeTest.java @@ -0,0 +1,161 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.eventbridge; + +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.incubator.events.EventLogger; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.internal.SdkEventLoggerProvider; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.testing.time.TestClock; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class EventToSpanEventBridgeTest { + + private final InMemorySpanExporter spanExporter = InMemorySpanExporter.create(); + private final SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .setSampler(onlyServerSpans()) + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build(); + private final TestClock testClock = TestClock.create(); + private final SdkEventLoggerProvider eventLoggerProvider = + SdkEventLoggerProvider.create( + SdkLoggerProvider.builder() + .setClock(testClock) + .addLogRecordProcessor(EventToSpanEventBridge.create()) + .build()); + private final Tracer tracer = tracerProvider.get("tracer"); + private final EventLogger eventLogger = eventLoggerProvider.get("event-logger"); + + private static Sampler onlyServerSpans() { + return new Sampler() { + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return SpanKind.SERVER.equals(spanKind) + ? SamplingResult.recordAndSample() + : SamplingResult.drop(); + } + + @Override + public String getDescription() { + return "description"; + } + }; + } + + @Test + void withRecordingSpan_BridgesEvent() { + testClock.setTime(Instant.ofEpochMilli(1)); + + Span span = tracer.spanBuilder("span").setSpanKind(SpanKind.SERVER).startSpan(); + try (Scope unused = span.makeCurrent()) { + eventLogger + .builder("my.event-name") + .setTimestamp(100, TimeUnit.NANOSECONDS) + .setSeverity(Severity.DEBUG) + .put("foo", "bar") + .put("number", 1) + .put("map", Value.of(Collections.singletonMap("key", Value.of("value")))) + .setAttributes(Attributes.builder().put("color", "red").build()) + .setAttributes(Attributes.builder().put("shape", "square").build()) + .emit(); + } finally { + span.end(); + } + + assertThat(spanExporter.getFinishedSpanItems()) + .satisfiesExactly( + spanData -> + assertThat(spanData) + .hasName("span") + .hasEventsSatisfyingExactly( + spanEvent -> + spanEvent + .hasName("my.event-name") + .hasTimestamp(100, TimeUnit.NANOSECONDS) + .hasAttributesSatisfying( + attributes -> { + assertThat(attributes.get(stringKey("color"))) + .isEqualTo("red"); + assertThat(attributes.get(stringKey("shape"))) + .isEqualTo("square"); + assertThat( + attributes.get( + longKey("log.record.observed_time_unix_nano"))) + .isEqualTo(1000000L); + assertThat( + attributes.get(longKey("log.record.severity_number"))) + .isEqualTo(Severity.DEBUG.getSeverityNumber()); + assertThat(attributes.get(stringKey("log.record.body"))) + .isEqualTo( + "{\"kvlistValue\":{\"values\":[{\"key\":\"number\",\"value\":{\"intValue\":\"1\"}},{\"key\":\"foo\",\"value\":{\"stringValue\":\"bar\"}},{\"key\":\"map\",\"value\":{\"kvlistValue\":{\"values\":[{\"key\":\"key\",\"value\":{\"stringValue\":\"value\"}}]}}}]}}"); + }))); + } + + @Test + void noSpan_doesNotBridgeEvent() { + eventLogger + .builder("my.event-name") + .setTimestamp(100, TimeUnit.NANOSECONDS) + .setSeverity(Severity.DEBUG) + .put("foo", "bar") + .put("number", 1) + .put("map", Value.of(Collections.singletonMap("key", Value.of("value")))) + .setAttributes(Attributes.builder().put("color", "red").build()) + .setAttributes(Attributes.builder().put("shape", "square").build()) + .emit(); + + assertThat(spanExporter.getFinishedSpanItems()).isEmpty(); + } + + @Test + void nonRecordingSpan_doesNotBridgeEvent() { + Span span = tracer.spanBuilder("span").setSpanKind(SpanKind.INTERNAL).startSpan(); + try (Scope unused = span.makeCurrent()) { + eventLogger + .builder("my.event-name") + .setTimestamp(100, TimeUnit.NANOSECONDS) + .setSeverity(Severity.DEBUG) + .put("foo", "bar") + .put("number", 1) + .put("map", Value.of(Collections.singletonMap("key", Value.of("value")))) + .setAttributes(Attributes.builder().put("color", "red").build()) + .setAttributes(Attributes.builder().put("shape", "square").build()) + .emit(); + } finally { + span.end(); + } + + assertThat(spanExporter.getFinishedSpanItems()).isEmpty(); + } +} diff --git a/processors/src/test/java/io/opentelemetry/contrib/eventbridge/internal/EventToSpanBridgeComponentProviderTest.java b/processors/src/test/java/io/opentelemetry/contrib/eventbridge/internal/EventToSpanBridgeComponentProviderTest.java new file mode 100644 index 000000000..cd9e89eba --- /dev/null +++ b/processors/src/test/java/io/opentelemetry/contrib/eventbridge/internal/EventToSpanBridgeComponentProviderTest.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.eventbridge.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.extension.incubator.fileconfig.FileConfiguration; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class EventToSpanBridgeComponentProviderTest { + + @Test + void endToEnd() { + String yaml = + "file_format: 0.1\n" + + "logger_provider:\n" + + " processors:\n" + // TODO(jack-berg): remove "{}" after releasing + // https://github.com/open-telemetry/opentelemetry-java/pull/6891/files + + " - event_to_span_event_bridge: {}\n"; + + OpenTelemetrySdk openTelemetrySdk = + FileConfiguration.parseAndCreate( + new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8))); + + assertThat(openTelemetrySdk.getSdkLoggerProvider().toString()) + .matches("SdkLoggerProvider\\{.*logRecordProcessor=EventToSpanEventBridge\\{}.*}"); + } +} diff --git a/samplers/README.md b/samplers/README.md index c21877682..4be98779b 100644 --- a/samplers/README.md +++ b/samplers/README.md @@ -2,7 +2,7 @@ ## Declarative configuration -The following samplers support [declarative configuration](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/configuration#declarative-configuration): +The following samplers support [declarative configuration](https://opentelemetry.io/docs/languages/java/configuration/#declarative-configuration): * `RuleBasedRoutingSampler` From 6dcedc061a9e409c9e9a94ac5cdd4ad0b98e90e1 Mon Sep 17 00:00:00 2001 From: Jack Berg Date: Mon, 18 Nov 2024 15:42:19 -0600 Subject: [PATCH 2/5] Fix build, add jack-berg to processors component owners --- .github/component_owners.yml | 1 + processors/README.md | 1 + .../internal/EventToSpanBridgeComponentProviderTest.java | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/component_owners.yml b/.github/component_owners.yml index e82bc3877..a7489b9de 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -56,6 +56,7 @@ components: processors: - LikeTheSalad - breedx-splk + - jack-berg prometheus-collector: - jkwatson resource-providers: diff --git a/processors/README.md b/processors/README.md index 5ae81cab0..7c874768e 100644 --- a/processors/README.md +++ b/processors/README.md @@ -33,6 +33,7 @@ logger_provider: ## Component owners - [Cesar Munoz](https://github.com/LikeTheSalad), Elastic +- [Jack Berg](https://github.com/jack-berg), New Relic - [Jason Plumb](https://github.com/breedx-splk), Splunk Learn more about component owners in [component_owners.yml](../.github/component_owners.yml). diff --git a/processors/src/test/java/io/opentelemetry/contrib/eventbridge/internal/EventToSpanBridgeComponentProviderTest.java b/processors/src/test/java/io/opentelemetry/contrib/eventbridge/internal/EventToSpanBridgeComponentProviderTest.java index cd9e89eba..8c7a95c31 100644 --- a/processors/src/test/java/io/opentelemetry/contrib/eventbridge/internal/EventToSpanBridgeComponentProviderTest.java +++ b/processors/src/test/java/io/opentelemetry/contrib/eventbridge/internal/EventToSpanBridgeComponentProviderTest.java @@ -18,7 +18,7 @@ class EventToSpanBridgeComponentProviderTest { @Test void endToEnd() { String yaml = - "file_format: 0.1\n" + "file_format: 0.3\n" + "logger_provider:\n" + " processors:\n" // TODO(jack-berg): remove "{}" after releasing From 3b2e9d5d767382a0376654ca3040fb5c86c99e6f Mon Sep 17 00:00:00 2001 From: Jack Berg Date: Mon, 18 Nov 2024 15:51:19 -0600 Subject: [PATCH 3/5] fix link --- processors/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/processors/README.md b/processors/README.md index 7c874768e..890cb5e16 100644 --- a/processors/README.md +++ b/processors/README.md @@ -28,7 +28,7 @@ logger_provider: - event_to_span_event_bridge: {} ``` -// TODO(jack-berg): remove "{}" after merging / rle https://github.com/open-telemetry/opentelemetry-java/pull/6891/files +// TODO(jack-berg): remove "{}" after releasing [opentelemetry-java#6891](https://github.com/open-telemetry/opentelemetry-java/pull/6891/files) ## Component owners From c2580f3fa8c0d901561fc97e9bf0b88785d3f031 Mon Sep 17 00:00:00 2001 From: Jack Berg Date: Tue, 19 Nov 2024 16:35:43 -0600 Subject: [PATCH 4/5] PR feedback --- processors/README.md | 7 +- .../eventbridge/EventToSpanEventBridge.java | 20 ++++-- .../EventToSpanEventBridgeTest.java | 70 +++++++++++++++---- 3 files changed, 72 insertions(+), 25 deletions(-) diff --git a/processors/README.md b/processors/README.md index 890cb5e16..866daaae1 100644 --- a/processors/README.md +++ b/processors/README.md @@ -20,16 +20,15 @@ For details of how the event log record is translated to span event, see [EventT `EventToSpanEventBridge` can be referenced in [declarative configuration](https://opentelemetry.io/docs/languages/java/configuration/#declarative-configuration) as follows: ```yaml -// Configure tracer provider as usual, omitted for brevity +# Configure tracer provider as usual, omitted for brevity tracer_provider: ... logger_provider: processors: - - event_to_span_event_bridge: {} + # TODO(jack-berg): remove "{}" after releasing [opentelemetry-java#6891](https://github.com/open-telemetry/opentelemetry-java/pull/6891/files) + - event_to_span_event_bridge: {} ``` -// TODO(jack-berg): remove "{}" after releasing [opentelemetry-java#6891](https://github.com/open-telemetry/opentelemetry-java/pull/6891/files) - ## Component owners - [Cesar Munoz](https://github.com/LikeTheSalad), Elastic diff --git a/processors/src/main/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridge.java b/processors/src/main/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridge.java index 86b1cb82c..b61723545 100644 --- a/processors/src/main/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridge.java +++ b/processors/src/main/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridge.java @@ -10,6 +10,7 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.common.Value; import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.context.Context; import io.opentelemetry.exporter.internal.marshal.MarshalerWithSize; import io.opentelemetry.exporter.internal.otlp.AnyValueMarshaler; @@ -30,9 +31,10 @@ *

    *
  • The log record has a valid span context *
  • {@link Span#current()} returns a span where {@link Span#isRecording()} is true + *
  • The log record's span context is the same as {@link Span#current()} *
* - *

The event {@link LogRecordData} is converted to attributes on the span event as follows: + *

The event {@link LogRecordData} is converted to a span event as follows: * *

    *
  • {@code event.name} attribute is mapped to span event name @@ -52,7 +54,7 @@ */ public final class EventToSpanEventBridge implements LogRecordProcessor { - private static final Logger LOGGER = Logger.getLogger(EventToSpanEventBridge.class.getName()); + private static final Logger logger = Logger.getLogger(EventToSpanEventBridge.class.getName()); private static final AttributeKey EVENT_NAME = AttributeKey.stringKey("event.name"); private static final AttributeKey LOG_RECORD_OBSERVED_TIME_UNIX_NANO = @@ -78,13 +80,17 @@ public void onEmit(Context context, ReadWriteLogRecord logRecord) { if (eventName == null) { return; } - if (!logRecordData.getSpanContext().isValid()) { + SpanContext logSpanContext = logRecordData.getSpanContext(); + if (!logSpanContext.isValid()) { return; } Span currentSpan = Span.current(); if (!currentSpan.isRecording()) { return; } + if (!currentSpan.getSpanContext().equals(logSpanContext)) { + return; + } currentSpan.addEvent( eventName, toSpanEventAttributes(logRecordData), @@ -105,13 +111,13 @@ private static Attributes toSpanEventAttributes(LogRecordData logRecord) { Value body = logRecord.getBodyValue(); if (body != null) { MarshalerWithSize marshaler = AnyValueMarshaler.create(body); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); try { - marshaler.writeJsonTo(baos); + marshaler.writeJsonTo(out); } catch (IOException e) { - LOGGER.log(Level.WARNING, "Error converting log record body to JSON", e); + logger.log(Level.WARNING, "Error converting log record body to JSON", e); } - builder.put(LOG_RECORD_BODY, new String(baos.toByteArray(), StandardCharsets.UTF_8)); + builder.put(LOG_RECORD_BODY, new String(out.toByteArray(), StandardCharsets.UTF_8)); } int droppedAttributesCount = diff --git a/processors/src/test/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridgeTest.java b/processors/src/test/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridgeTest.java index 4b9bb7355..b45e36b44 100644 --- a/processors/src/test/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridgeTest.java +++ b/processors/src/test/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridgeTest.java @@ -14,7 +14,10 @@ import io.opentelemetry.api.incubator.events.EventLogger; import io.opentelemetry.api.logs.Severity; import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; @@ -22,6 +25,7 @@ import io.opentelemetry.sdk.logs.internal.SdkEventLoggerProvider; import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; import io.opentelemetry.sdk.testing.time.TestClock; +import io.opentelemetry.sdk.trace.IdGenerator; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; @@ -77,6 +81,8 @@ public String getDescription() { void withRecordingSpan_BridgesEvent() { testClock.setTime(Instant.ofEpochMilli(1)); + // The test tracerProvider has a sampler which records and samples SERVER spans, and drops all + // others. We create a recording span by setting kind to SERVER. Span span = tracer.spanBuilder("span").setSpanKind(SpanKind.SERVER).startSpan(); try (Scope unused = span.makeCurrent()) { eventLogger @@ -123,27 +129,46 @@ void withRecordingSpan_BridgesEvent() { } @Test - void noSpan_doesNotBridgeEvent() { - eventLogger - .builder("my.event-name") - .setTimestamp(100, TimeUnit.NANOSECONDS) - .setSeverity(Severity.DEBUG) - .put("foo", "bar") - .put("number", 1) - .put("map", Value.of(Collections.singletonMap("key", Value.of("value")))) - .setAttributes(Attributes.builder().put("color", "red").build()) - .setAttributes(Attributes.builder().put("shape", "square").build()) - .emit(); + void nonRecordingSpan_doesNotBridgeEvent() { + // The test tracerProvider has a sampler which records and samples server spans, and drops all + // others. We create a non-recording span by setting kind to INTERNAL. + Span span = tracer.spanBuilder("span").setSpanKind(SpanKind.INTERNAL).startSpan(); + try (Scope unused = span.makeCurrent()) { + eventLogger + .builder("my.event-name") + .setTimestamp(100, TimeUnit.NANOSECONDS) + .setSeverity(Severity.DEBUG) + .put("foo", "bar") + .put("number", 1) + .put("map", Value.of(Collections.singletonMap("key", Value.of("value")))) + .setAttributes(Attributes.builder().put("color", "red").build()) + .setAttributes(Attributes.builder().put("shape", "square").build()) + .emit(); + } finally { + span.end(); + } - assertThat(spanExporter.getFinishedSpanItems()).isEmpty(); + assertThat(spanExporter.getFinishedSpanItems()) + .allSatisfy(spanData -> assertThat(spanData.getEvents()).isEmpty()); } @Test - void nonRecordingSpan_doesNotBridgeEvent() { - Span span = tracer.spanBuilder("span").setSpanKind(SpanKind.INTERNAL).startSpan(); + void differentSpanContext_doesNotBridgeEvent() { + // The test tracerProvider has a sampler which records and samples SERVER spans, and drops all + // others. We create a recording span by setting kind to SERVER. + Span span = tracer.spanBuilder("span").setSpanKind(SpanKind.SERVER).startSpan(); try (Scope unused = span.makeCurrent()) { eventLogger .builder("my.event-name") + // Manually override the context + .setContext( + Span.wrap( + SpanContext.create( + IdGenerator.random().generateTraceId(), + IdGenerator.random().generateSpanId(), + TraceFlags.getDefault(), + TraceState.getDefault())) + .storeInContext(Context.current())) .setTimestamp(100, TimeUnit.NANOSECONDS) .setSeverity(Severity.DEBUG) .put("foo", "bar") @@ -156,6 +181,23 @@ void nonRecordingSpan_doesNotBridgeEvent() { span.end(); } + assertThat(spanExporter.getFinishedSpanItems()) + .allSatisfy(spanData -> assertThat(spanData.getEvents()).isEmpty()); + } + + @Test + void noSpan_doesNotBridgeEvent() { + eventLogger + .builder("my.event-name") + .setTimestamp(100, TimeUnit.NANOSECONDS) + .setSeverity(Severity.DEBUG) + .put("foo", "bar") + .put("number", 1) + .put("map", Value.of(Collections.singletonMap("key", Value.of("value")))) + .setAttributes(Attributes.builder().put("color", "red").build()) + .setAttributes(Attributes.builder().put("shape", "square").build()) + .emit(); + assertThat(spanExporter.getFinishedSpanItems()).isEmpty(); } } From e7ce50bcf3e81d5ee8b7486fbe1865858e63d727 Mon Sep 17 00:00:00 2001 From: Jack Berg Date: Wed, 20 Nov 2024 09:33:28 -0600 Subject: [PATCH 5/5] PR feedback --- .../contrib/eventbridge/EventToSpanEventBridge.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/processors/src/main/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridge.java b/processors/src/main/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridge.java index b61723545..18c533ced 100644 --- a/processors/src/main/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridge.java +++ b/processors/src/main/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridge.java @@ -114,10 +114,10 @@ private static Attributes toSpanEventAttributes(LogRecordData logRecord) { ByteArrayOutputStream out = new ByteArrayOutputStream(); try { marshaler.writeJsonTo(out); + builder.put(LOG_RECORD_BODY, out.toString(StandardCharsets.UTF_8.name())); } catch (IOException e) { logger.log(Level.WARNING, "Error converting log record body to JSON", e); } - builder.put(LOG_RECORD_BODY, new String(out.toByteArray(), StandardCharsets.UTF_8)); } int droppedAttributesCount =