Skip to content

Commit

Permalink
Translate OTel logs/events to spans (#515)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mateusz Rzeszutek authored May 4, 2023
1 parent f73261b commit c8aa408
Show file tree
Hide file tree
Showing 5 changed files with 325 additions and 1 deletion.
6 changes: 5 additions & 1 deletion sample-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ android {
}
}

val otelVersion = "1.23.1"
val otelVersion = "1.24.0"
val otelAlphaVersion = "$otelVersion-alpha"
val otelInstrumentationVersion = "1.25.1"
val otelInstrumentationAlphaVersion = "$otelInstrumentationVersion-alpha"
Expand All @@ -73,5 +73,9 @@ dependencies {
implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:$otelInstrumentationVersion")
implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv:$otelInstrumentationAlphaVersion")

implementation("io.opentelemetry:opentelemetry-api-events:$otelAlphaVersion")
implementation("io.opentelemetry:opentelemetry-sdk-logs:$otelAlphaVersion")
implementation("io.opentelemetry:opentelemetry-sdk:$otelAlphaVersion")

testImplementation("junit:junit:4.13.2")
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@
import com.splunk.rum.SplunkRum;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.events.EventEmitter;
import io.opentelemetry.api.events.EventEmitterProvider;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.logs.SdkEventEmitterProvider;

import java.util.Random;
import java.util.concurrent.Executors;
Expand Down Expand Up @@ -104,6 +108,7 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
v -> {
SplunkRum.getInstance()
.addRumEvent("this span will be ignored", Attributes.empty());
emitEvent(SplunkRum.getInstance(), "SecondFragment", "toWebViewClick");

NavHostFragment.findNavController(SecondFragment.this)
.navigate(R.id.action_SecondFragment_to_webViewFragment);
Expand Down Expand Up @@ -215,4 +220,16 @@ private void createSpamSpan() {
.end();
updateLabel();
}

public static void emitEvent(SplunkRum splunkRum, String eventDomain, String eventName) {
EventEmitterProvider eventEmitterProvider =
SdkEventEmitterProvider.create(
((OpenTelemetrySdk) splunkRum.getOpenTelemetry()).getSdkLoggerProvider());
EventEmitter eventEmitter =
eventEmitterProvider
.eventEmitterBuilder("test")
.setEventDomain(eventDomain)
.build();
eventEmitter.emit(eventName, Attributes.empty());
}
}
122 changes: 122 additions & 0 deletions splunk-otel-android/src/main/java/com/splunk/rum/LogToSpanBridge.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.splunk.rum;

import static io.opentelemetry.api.common.AttributeKey.longKey;
import static io.opentelemetry.api.common.AttributeKey.stringKey;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.logs.Severity;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.TracerBuilder;
import io.opentelemetry.api.trace.TracerProvider;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
import io.opentelemetry.sdk.logs.LogRecordProcessor;
import io.opentelemetry.sdk.logs.ReadWriteLogRecord;
import io.opentelemetry.sdk.logs.data.Body;
import io.opentelemetry.sdk.logs.data.LogRecordData;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;

import java.util.concurrent.TimeUnit;

final class LogToSpanBridge implements LogRecordProcessor {

// can be used in Splunk code to set span names in instrumentations
static final AttributeKey<String> OPERATION_NAME = stringKey("log.operation_name");

static final AttributeKey<Long> LOG_SEVERITY = longKey("log.severity");
static final AttributeKey<String> LOG_SEVERITY_TEXT = stringKey("log.severity_text");
static final AttributeKey<String> LOG_BODY = stringKey("log.body");

private volatile TracerProvider tracerProvider;

void setTracerProvider(TracerProvider tracerProvider) {
this.tracerProvider = tracerProvider;
}

@Override
public void onEmit(Context context, ReadWriteLogRecord logRecord) {
TracerProvider tracerProvider = this.tracerProvider;
if (tracerProvider == null) {
// if this is null then we've messed up the RumInitializer implementation
return;
}

LogRecordData log = logRecord.toLogRecordData();
Tracer tracer = getTracer(tracerProvider, log.getInstrumentationScopeInfo());

SpanBuilder spanBuilder = tracer.spanBuilder(getSpanName(log));
setLogAttributes(spanBuilder, log);
Span span =
spanBuilder
.setStartTimestamp(log.getEpochNanos(), TimeUnit.NANOSECONDS)
.startSpan();
span.end(log.getEpochNanos(), TimeUnit.NANOSECONDS);
}

private static Tracer getTracer(TracerProvider tracerProvider, InstrumentationScopeInfo scope) {
TracerBuilder builder = tracerProvider.tracerBuilder(scope.getName());
String version = scope.getVersion();
if (version != null) {
builder.setInstrumentationVersion(version);
}
String schemaUrl = scope.getSchemaUrl();
if (schemaUrl != null) {
builder.setSchemaUrl(schemaUrl);
}
return builder.build();
}

private static String getSpanName(LogRecordData log) {
String operationName = log.getAttributes().get(OPERATION_NAME);
if (operationName != null) {
return operationName;
}
String eventDomain = log.getAttributes().get(SemanticAttributes.EVENT_DOMAIN);
String eventName = log.getAttributes().get(SemanticAttributes.EVENT_NAME);
if (eventDomain != null || eventName != null) {
return (eventDomain == null ? "" : eventDomain + "/")
+ (eventName == null ? "" : eventName);
}
return "Log";
}

private static void setLogAttributes(SpanBuilder spanBuilder, LogRecordData log) {
spanBuilder.setAllAttributes(log.getAttributes());
int severity = log.getSeverity().getSeverityNumber();
if (severity != Severity.UNDEFINED_SEVERITY_NUMBER.getSeverityNumber()) {
spanBuilder.setAttribute(LOG_SEVERITY, (long) severity);
}
String severityText = log.getSeverityText();
if (severityText != null) {
spanBuilder.setAttribute(LOG_SEVERITY_TEXT, severityText);
}
Body logBody = log.getBody();
switch (logBody.getType()) {
case STRING:
spanBuilder.setAttribute(LOG_BODY, logBody.asString());
break;

case EMPTY:
default:
break;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,18 @@ SplunkRum initialize(
return tracerProviderBuilder;
});

// install the log->span bridge
LogToSpanBridge logBridge = new LogToSpanBridge();
otelRumBuilder.addLoggerProviderCustomizer(
(loggerProviderBuilder, app) ->
loggerProviderBuilder.addLogRecordProcessor(logBridge));
// make sure the TracerProvider gets set as the very first thing, before any other
// instrumentations
otelRumBuilder.addInstrumentation(
instrumentedApplication ->
logBridge.setTracerProvider(
instrumentedApplication.getOpenTelemetrySdk().getTracerProvider()));

if (builder.isAnrDetectionEnabled()) {
installAnrDetector(otelRumBuilder, mainLooper);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.splunk.rum;

import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;

import static org.mockito.Mockito.when;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.logs.Severity;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
import io.opentelemetry.sdk.logs.ReadWriteLogRecord;
import io.opentelemetry.sdk.logs.data.Body;
import io.opentelemetry.sdk.logs.data.LogRecordData;
import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension;
import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.List;

@ExtendWith(MockitoExtension.class)
class LogToSpanBridgeTest {

@RegisterExtension OpenTelemetryExtension testing = OpenTelemetryExtension.create();

@Mock ReadWriteLogRecord logRecord;
@Mock LogRecordData log;

final LogToSpanBridge bridge = new LogToSpanBridge();

@Test
void misconfiguration() {
bridge.onEmit(Context.root(), logRecord);

assertThat(testing.getSpans()).isEmpty();
}

@Test
void unnamedLog() {
InstrumentationScopeInfo scope =
InstrumentationScopeInfo.builder("test")
.setVersion("1.2.3")
.setSchemaUrl("http://schema")
.build();
long epochNanos = 123_456_789_000_000L;
when(log.getInstrumentationScopeInfo()).thenReturn(scope);
when(log.getAttributes())
.thenReturn(Attributes.builder().put("attr1", "12").put("attr2", "42").build());
when(log.getEpochNanos()).thenReturn(epochNanos);
when(log.getSeverity()).thenReturn(Severity.DEBUG);
when(log.getSeverityText()).thenReturn("just testing");
when(log.getBody()).thenReturn(Body.string("hasta la vista"));
when(logRecord.toLogRecordData()).thenReturn(log);

bridge.setTracerProvider(testing.getOpenTelemetry().getTracerProvider());
bridge.onEmit(Context.root(), logRecord);

List<SpanData> spans = testing.getSpans();
assertThat(spans).hasSize(1);
assertThat(spans.get(0))
.hasInstrumentationScopeInfo(scope)
.hasName("Log")
.startsAt(epochNanos)
.endsAt(epochNanos)
.hasAttributes(
Attributes.builder()
.put("attr1", "12")
.put("attr2", "42")
.put(
LogToSpanBridge.LOG_SEVERITY,
Severity.DEBUG.getSeverityNumber())
.put(LogToSpanBridge.LOG_SEVERITY_TEXT, "just testing")
.put(LogToSpanBridge.LOG_BODY, "hasta la vista")
.build());
}

@Test
void event() {
long epochNanos = 123_456_789_000_000L;
when(log.getInstrumentationScopeInfo()).thenReturn(InstrumentationScopeInfo.create("test"));
when(log.getAttributes())
.thenReturn(
Attributes.builder()
.put(SemanticAttributes.EVENT_DOMAIN, "androidApp")
.put(SemanticAttributes.EVENT_NAME, "buttonClick")
.put("attr", "value")
.build());
when(log.getEpochNanos()).thenReturn(epochNanos);
when(log.getSeverity()).thenReturn(Severity.UNDEFINED_SEVERITY_NUMBER);
when(log.getBody()).thenReturn(Body.empty());
when(logRecord.toLogRecordData()).thenReturn(log);

bridge.setTracerProvider(testing.getOpenTelemetry().getTracerProvider());
bridge.onEmit(Context.root(), logRecord);

List<SpanData> spans = testing.getSpans();
assertThat(spans).hasSize(1);
assertThat(spans.get(0))
.hasInstrumentationScopeInfo(InstrumentationScopeInfo.create("test"))
.hasName("androidApp/buttonClick")
.startsAt(epochNanos)
.endsAt(epochNanos)
.hasAttributes(
Attributes.builder()
.put(SemanticAttributes.EVENT_DOMAIN, "androidApp")
.put(SemanticAttributes.EVENT_NAME, "buttonClick")
.put("attr", "value")
.build());
}

@Test
void customNamedLog() {
long epochNanos = 123_456_789_000_000L;
when(log.getInstrumentationScopeInfo()).thenReturn(InstrumentationScopeInfo.create("test"));
when(log.getAttributes())
.thenReturn(
Attributes.builder()
.put("attr1", "12")
.put("attr2", "42")
.put(LogToSpanBridge.OPERATION_NAME, "span name")
.build());
when(log.getEpochNanos()).thenReturn(epochNanos);
when(log.getSeverity()).thenReturn(Severity.INFO);
when(log.getBody()).thenReturn(Body.string("message"));
when(logRecord.toLogRecordData()).thenReturn(log);

bridge.setTracerProvider(testing.getOpenTelemetry().getTracerProvider());
bridge.onEmit(Context.root(), logRecord);

List<SpanData> spans = testing.getSpans();
assertThat(spans).hasSize(1);
assertThat(spans.get(0))
.hasInstrumentationScopeInfo(InstrumentationScopeInfo.create("test"))
.hasName("span name")
.startsAt(epochNanos)
.endsAt(epochNanos)
.hasAttributes(
Attributes.builder()
.put("attr1", "12")
.put("attr2", "42")
.put(LogToSpanBridge.OPERATION_NAME, "span name")
.put(
LogToSpanBridge.LOG_SEVERITY,
Severity.INFO.getSeverityNumber())
.put(LogToSpanBridge.LOG_BODY, "message")
.build());
}
}

0 comments on commit c8aa408

Please sign in to comment.