Skip to content

Commit 4c72e59

Browse files
authored
Add LogRecordProcessor to record event log records as span events (#1551)
1 parent c2f8c17 commit 4c72e59

File tree

9 files changed

+450
-2
lines changed

9 files changed

+450
-2
lines changed

.github/component_owners.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ components:
5656
processors:
5757
- LikeTheSalad
5858
- breedx-splk
59+
- jack-berg
5960
prometheus-collector:
6061
- jkwatson
6162
resource-providers:

processors/README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,38 @@
11
# Processors
22

3-
This module provides tools to intercept and process signals globally.
3+
## Interceptable exporters
4+
5+
This module provides tools to intercept and process signals globally:
6+
7+
* `InterceptableSpanExporter`
8+
* `InterceptableMetricExporter`
9+
* `InterceptableLogRecordExporter`
10+
11+
## Event to SpanEvent Bridge
12+
13+
`EventToSpanEventBridge` is a `LogRecordProcessor` which records events (i.e. log records with an `event.name` attribute) as span events for the current span if:
14+
15+
* The log record has a valid span context
16+
* `Span.current()` returns a span where `Span.isRecording()` is true
17+
18+
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).
19+
20+
`EventToSpanEventBridge` can be referenced in [declarative configuration](https://opentelemetry.io/docs/languages/java/configuration/#declarative-configuration) as follows:
21+
22+
```yaml
23+
# Configure tracer provider as usual, omitted for brevity
24+
tracer_provider: ...
25+
26+
logger_provider:
27+
processors:
28+
# TODO(jack-berg): remove "{}" after releasing [opentelemetry-java#6891](https://github.com/open-telemetry/opentelemetry-java/pull/6891/files)
29+
- event_to_span_event_bridge: {}
30+
```
431

532
## Component owners
633

734
- [Cesar Munoz](https://github.com/LikeTheSalad), Elastic
35+
- [Jack Berg](https://github.com/jack-berg), New Relic
836
- [Jason Plumb](https://github.com/breedx-splk), Splunk
937

1038
Learn more about component owners in [component_owners.yml](../.github/component_owners.yml).

processors/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,13 @@ java {
1313

1414
dependencies {
1515
api("io.opentelemetry:opentelemetry-sdk")
16+
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
17+
18+
// For EventToSpanEventBridge
19+
implementation("io.opentelemetry:opentelemetry-exporter-otlp-common")
20+
implementation("com.fasterxml.jackson.core:jackson-core")
21+
1622
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
23+
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
24+
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator")
1725
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.eventbridge;
7+
8+
import io.opentelemetry.api.common.AttributeKey;
9+
import io.opentelemetry.api.common.Attributes;
10+
import io.opentelemetry.api.common.AttributesBuilder;
11+
import io.opentelemetry.api.common.Value;
12+
import io.opentelemetry.api.trace.Span;
13+
import io.opentelemetry.api.trace.SpanContext;
14+
import io.opentelemetry.context.Context;
15+
import io.opentelemetry.exporter.internal.marshal.MarshalerWithSize;
16+
import io.opentelemetry.exporter.internal.otlp.AnyValueMarshaler;
17+
import io.opentelemetry.sdk.logs.LogRecordProcessor;
18+
import io.opentelemetry.sdk.logs.ReadWriteLogRecord;
19+
import io.opentelemetry.sdk.logs.data.LogRecordData;
20+
import java.io.ByteArrayOutputStream;
21+
import java.io.IOException;
22+
import java.nio.charset.StandardCharsets;
23+
import java.util.concurrent.TimeUnit;
24+
import java.util.logging.Level;
25+
import java.util.logging.Logger;
26+
27+
/**
28+
* A processor that records events (i.e. log records with an {@code event.name} attribute) as span
29+
* events for the current span if:
30+
*
31+
* <ul>
32+
* <li>The log record has a valid span context
33+
* <li>{@link Span#current()} returns a span where {@link Span#isRecording()} is true
34+
* <li>The log record's span context is the same as {@link Span#current()}
35+
* </ul>
36+
*
37+
* <p>The event {@link LogRecordData} is converted to a span event as follows:
38+
*
39+
* <ul>
40+
* <li>{@code event.name} attribute is mapped to span event name
41+
* <li>{@link LogRecordData#getTimestampEpochNanos()} is mapped to span event timestamp
42+
* <li>{@link LogRecordData#getAttributes()} are mapped to span event attributes, excluding {@code
43+
* event.name}
44+
* <li>{@link LogRecordData#getObservedTimestampEpochNanos()} is mapped to span event attribute
45+
* with key {@code log.record.observed_timestamp}
46+
* <li>{@link LogRecordData#getSeverity()} is mapped to span event attribute with key {@code
47+
* log.record.severity_number}
48+
* <li>{@link LogRecordData#getBodyValue()} is mapped to span event attribute with key {@code
49+
* log.record.body}, as an escaped JSON string following the standard protobuf JSON encoding
50+
* <li>{@link LogRecordData#getTotalAttributeCount()} - {@link
51+
* LogRecordData#getAttributes()}.size() is mapped to span event attribute with key {@code
52+
* log.record.dropped_attributes_count}
53+
* </ul>
54+
*/
55+
public final class EventToSpanEventBridge implements LogRecordProcessor {
56+
57+
private static final Logger logger = Logger.getLogger(EventToSpanEventBridge.class.getName());
58+
59+
private static final AttributeKey<String> EVENT_NAME = AttributeKey.stringKey("event.name");
60+
private static final AttributeKey<Long> LOG_RECORD_OBSERVED_TIME_UNIX_NANO =
61+
AttributeKey.longKey("log.record.observed_time_unix_nano");
62+
private static final AttributeKey<Long> LOG_RECORD_SEVERITY_NUMBER =
63+
AttributeKey.longKey("log.record.severity_number");
64+
private static final AttributeKey<String> LOG_RECORD_BODY =
65+
AttributeKey.stringKey("log.record.body");
66+
private static final AttributeKey<Long> LOG_RECORD_DROPPED_ATTRIBUTES_COUNT =
67+
AttributeKey.longKey("log.record.dropped_attributes_count");
68+
69+
private EventToSpanEventBridge() {}
70+
71+
/** Create an instance. */
72+
public static EventToSpanEventBridge create() {
73+
return new EventToSpanEventBridge();
74+
}
75+
76+
@Override
77+
public void onEmit(Context context, ReadWriteLogRecord logRecord) {
78+
LogRecordData logRecordData = logRecord.toLogRecordData();
79+
String eventName = logRecordData.getAttributes().get(EVENT_NAME);
80+
if (eventName == null) {
81+
return;
82+
}
83+
SpanContext logSpanContext = logRecordData.getSpanContext();
84+
if (!logSpanContext.isValid()) {
85+
return;
86+
}
87+
Span currentSpan = Span.current();
88+
if (!currentSpan.isRecording()) {
89+
return;
90+
}
91+
if (!currentSpan.getSpanContext().equals(logSpanContext)) {
92+
return;
93+
}
94+
currentSpan.addEvent(
95+
eventName,
96+
toSpanEventAttributes(logRecordData),
97+
logRecordData.getTimestampEpochNanos(),
98+
TimeUnit.NANOSECONDS);
99+
}
100+
101+
private static Attributes toSpanEventAttributes(LogRecordData logRecord) {
102+
AttributesBuilder builder =
103+
logRecord.getAttributes().toBuilder().removeIf(key -> key.equals(EVENT_NAME));
104+
105+
builder.put(LOG_RECORD_OBSERVED_TIME_UNIX_NANO, logRecord.getObservedTimestampEpochNanos());
106+
107+
builder.put(LOG_RECORD_SEVERITY_NUMBER, logRecord.getSeverity().getSeverityNumber());
108+
109+
// Add bridging for logRecord.getSeverityText() if EventBuilder adds severity text setter
110+
111+
Value<?> body = logRecord.getBodyValue();
112+
if (body != null) {
113+
MarshalerWithSize marshaler = AnyValueMarshaler.create(body);
114+
ByteArrayOutputStream out = new ByteArrayOutputStream();
115+
try {
116+
marshaler.writeJsonTo(out);
117+
builder.put(LOG_RECORD_BODY, out.toString(StandardCharsets.UTF_8.name()));
118+
} catch (IOException e) {
119+
logger.log(Level.WARNING, "Error converting log record body to JSON", e);
120+
}
121+
}
122+
123+
int droppedAttributesCount =
124+
logRecord.getTotalAttributeCount() - logRecord.getAttributes().size();
125+
if (droppedAttributesCount > 0) {
126+
builder.put(LOG_RECORD_DROPPED_ATTRIBUTES_COUNT, droppedAttributesCount);
127+
}
128+
129+
return builder.build();
130+
}
131+
132+
@Override
133+
public String toString() {
134+
return "EventToSpanEventBridge{}";
135+
}
136+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.eventbridge.internal;
7+
8+
import io.opentelemetry.contrib.eventbridge.EventToSpanEventBridge;
9+
import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider;
10+
import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties;
11+
import io.opentelemetry.sdk.logs.LogRecordProcessor;
12+
13+
/**
14+
* Declarative configuration SPI implementation for {@link EventToSpanEventBridge}.
15+
*
16+
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
17+
* at any time.
18+
*/
19+
public class EventToSpanEventBridgeComponentProvider
20+
implements ComponentProvider<LogRecordProcessor> {
21+
22+
@Override
23+
public Class<LogRecordProcessor> getType() {
24+
return LogRecordProcessor.class;
25+
}
26+
27+
@Override
28+
public String getName() {
29+
return "event_to_span_event_bridge";
30+
}
31+
32+
@Override
33+
public LogRecordProcessor create(StructuredConfigProperties config) {
34+
return EventToSpanEventBridge.create();
35+
}
36+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
io.opentelemetry.contrib.eventbridge.internal.EventToSpanEventBridgeComponentProvider

0 commit comments

Comments
 (0)