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;
+ }
+ 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),
+ 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 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);
+ }
+ }
+
+ 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..b45e36b44
--- /dev/null
+++ b/processors/src/test/java/io/opentelemetry/contrib/eventbridge/EventToSpanEventBridgeTest.java
@@ -0,0 +1,203 @@
+/*
+ * 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.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;
+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.IdGenerator;
+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));
+
+ // 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")
+ .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 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())
+ .allSatisfy(spanData -> assertThat(spanData.getEvents()).isEmpty());
+ }
+
+ @Test
+ 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")
+ .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())
+ .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();
+ }
+}
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..8c7a95c31
--- /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.3\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`