Skip to content

Commit

Permalink
Add Otel OTLP HTTP exporter (#195)
Browse files Browse the repository at this point in the history
Adds Money adapter for the OTLP HTTP exporter
  • Loading branch information
HaloFour committed Jul 31, 2023
1 parent 1b0f2ec commit 37e1225
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
scala: [2.12.15, 2.13.8]
scala: [2.12.18, 2.13.11]
java: [adopt@1.8]
runs-on: ${{ matrix.os }}
steps:
Expand Down
25 changes: 23 additions & 2 deletions build.sbt
Expand Up @@ -32,6 +32,7 @@ lazy val money =
moneyOtelFormatters,
moneyOtelHandler,
moneyOtlpExporter,
moneyOtlpHttpExporter,
moneyOtelInMemoryExporter,
moneyOtelLoggingExporter,
moneyOtelZipkinExporter,
Expand Down Expand Up @@ -299,7 +300,6 @@ lazy val moneyOtelLoggingExporter =
)
.dependsOn(moneyCore, moneyOtelHandler % "test->test;compile->compile")


lazy val moneyOtlpExporter =
Project("money-otlp-exporter", file("./money-otlp-exporter"))
.enablePlugins(AutomateHeaderPlugin)
Expand All @@ -322,6 +322,27 @@ lazy val moneyOtlpExporter =
)
.dependsOn(moneyCore, moneyOtelHandler % "test->test;compile->compile")

lazy val moneyOtlpHttpExporter =
Project("money-otlp-http-exporter", file("./money-otlp-http-exporter"))
.enablePlugins(AutomateHeaderPlugin)
.settings(projectSettings: _*)
.settings(
libraryDependencies ++=
Seq(
typesafeConfig,
openTelemetryApi,
openTelemetrySdk,
openTelemetryOtlpHttpExporter,
grpc,
junit,
junitInterface,
assertj,
powerMock,
powerMockApi
) ++ commonTestDependencies,
testOptions += Tests.Argument(TestFrameworks.JUnit, "-v")
)
.dependsOn(moneyCore, moneyOtelHandler % "test->test;compile->compile")

def aspectjProjectSettings = projectSettings ++ Seq(
Test / javaOptions ++= (Aspectj / aspectjWeaverOptions).value // adds javaagent:aspectjweaver to java options, including test
Expand Down Expand Up @@ -355,7 +376,7 @@ def basicSettings = Defaults.itSettings ++ Seq(
organization := "com.comcast.money",
sonatypeProfileName := "com.comcast",
scalaVersion := "2.12.15",
crossScalaVersions := List("2.13.8", "2.12.15"),
crossScalaVersions := List("2.13.11", "2.12.18"),
resolvers ++= Seq(
("spray repo" at "http://repo.spray.io/").withAllowInsecureProtocol(true),
"Sonatype OSS Releases" at "https://oss.sonatype.org/content/repositories/releases/",
Expand Down
@@ -0,0 +1,74 @@
/*
* Copyright 2012 Comcast Cable Communications Management, LLC
*
* 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.comcast.money.otel.handlers.otlp.http

import com.comcast.money.otel.handlers.OtelSpanHandler
import com.typesafe.config.Config
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter
import io.opentelemetry.sdk.trace.`export`.SpanExporter

import java.time.Duration

/**
* A Money [[com.comcast.money.api.SpanHandler]] that can export spans to OTLP Collector
* through the OpenTelemetry OTLP `SpanExporter`.
*
* Sample configuration:
*
* {{{
* handling = {
* async = true
* handlers = [
* {
* class = "com.comcast.money.otel.handlers.otlp.http.OtlpHttpHandler"
* batch = true
* exporter-timeout-ms = 30000
* max-batch-size = 512
* max-queue-size = 2048
* schedule-delay-ms = 5000
* exporter {
* endpoint = "https://localhost:14250"
* compression = "gzip"
* timeout-ms = 1000
* }
* }
* ]
* }
* }}}
*
*/
class OtlpHttpHandler(config: Config) extends OtelSpanHandler(config) {
override protected def createSpanExporter(config: Config): SpanExporter = {
val builder = OtlpHttpSpanExporter.builder()

val endpointKey = "endpoint"
val compressionKey = "compression"
val deadlineMillisKey = "timeout-ms"

if (config.hasPath(endpointKey)) {
builder.setEndpoint(config.getString(endpointKey))
}
if (config.hasPath(compressionKey)) {
builder.setCompression(config.getString(compressionKey))
}
if (config.hasPath(deadlineMillisKey)) {
builder.setTimeout(Duration.ofMillis(config.getLong(deadlineMillisKey)))
}

builder.build()
}
}
@@ -0,0 +1,146 @@
/*
* Copyright 2012 Comcast Cable Communications Management, LLC
*
* 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.comcast.money.otel.handlers.otlp;

import java.time.Duration;
import java.util.Collection;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.trace.data.SpanData;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import com.comcast.money.api.SpanId;
import com.comcast.money.api.SpanInfo;
import com.comcast.money.otel.handlers.TestSpanInfo;
import com.comcast.money.otel.handlers.otlp.http.OtlpHttpHandler;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;

@RunWith(PowerMockRunner.class)
@PrepareForTest({
OtlpHttpSpanExporter.class,
OtlpHttpSpanExporterBuilder.class
})
public class OtlpHttpHandlerSpec {

private OtlpHttpSpanExporter spanExporter;
private OtlpHttpSpanExporterBuilder spanExporterBuilder;

private OtlpHttpHandler underTest;

@Before
public void beforeEach() throws Exception {
PowerMockito.mockStatic(OtlpHttpSpanExporter.class);

spanExporter = PowerMockito.mock(OtlpHttpSpanExporter.class);
spanExporterBuilder = PowerMockito.mock(OtlpHttpSpanExporterBuilder.class);

PowerMockito.when(OtlpHttpSpanExporter.builder()).thenReturn(spanExporterBuilder);
PowerMockito.when(spanExporterBuilder.build()).thenReturn(spanExporter);
PowerMockito.when(spanExporter.export(any())).thenReturn(CompletableResultCode.ofSuccess());
}

@Test
public void configuresOtlpExporter() {
Config config = ConfigFactory.parseString("batch = false");

OtlpHttpHandler underTest = new OtlpHttpHandler(config);

PowerMockito.verifyStatic(OtlpHttpSpanExporter.class);
OtlpHttpSpanExporter.builder();
Mockito.verify(spanExporterBuilder).build();

SpanId spanId = SpanId.createNew();
SpanInfo spanInfo = new TestSpanInfo(spanId);

underTest.handle(spanInfo);

@SuppressWarnings("unchecked")
ArgumentCaptor<Collection<SpanData>> captor = ArgumentCaptor.forClass(Collection.class);
Mockito.verify(spanExporter).export(captor.capture());
Collection<SpanData> collection = captor.getValue();

assertThat(collection).hasSize(1);
SpanData spanData = collection.stream().findFirst().get();

assertThat(spanData.getTraceId()).isEqualTo(spanId.traceIdAsHex());
assertThat(spanData.getSpanId()).isEqualTo(spanId.selfIdAsHex());
}

@Test
public void configuresOtlpExporterWithEndpoint() {
Config config = ConfigFactory.parseString(
"batch = false\n" +
"exporter {\n" +
" endpoint = \"endpoint\"\n" +
"}"
);

OtlpHttpHandler underTest = new OtlpHttpHandler(config);

PowerMockito.verifyStatic(OtlpHttpSpanExporter.class);
OtlpHttpSpanExporter.builder();
Mockito.verify(spanExporterBuilder).setEndpoint("endpoint");
Mockito.verify(spanExporterBuilder).build();
}

@Test
public void configuresOtlpExporterWithCompression() {
Config config = ConfigFactory.parseString(
"batch = false\n" +
"exporter {\n" +
" compression = \"gzip\"\n" +
"}"
);

OtlpHttpHandler underTest = new OtlpHttpHandler(config);

PowerMockito.verifyStatic(OtlpHttpSpanExporter.class);
OtlpHttpSpanExporter.builder();
Mockito.verify(spanExporterBuilder).setCompression("gzip");
Mockito.verify(spanExporterBuilder).build();
}

@Test
public void configuresOtlpExporterWithDeadlineMillis() {
Config config = ConfigFactory.parseString(
"batch = false\n" +
"exporter {\n" +
" timeout-ms = 500\n" +
"}"
);

OtlpHttpHandler underTest = new OtlpHttpHandler(config);

PowerMockito.verifyStatic(OtlpHttpSpanExporter.class);
OtlpHttpSpanExporter.builder();
Mockito.verify(spanExporterBuilder).setTimeout(Duration.ofMillis(500L));
Mockito.verify(spanExporterBuilder).build();
}
}
1 change: 1 addition & 0 deletions project/Dependencies.scala
Expand Up @@ -68,6 +68,7 @@ object Dependencies {
val openTelemetrySdkTesting = "io.opentelemetry" % "opentelemetry-sdk-testing" % openTelemetryV changing()
val openTelemetryLoggingExporter = "io.opentelemetry" % "opentelemetry-exporter-logging" % openTelemetryInstV changing()
val openTelemetryOtlpExporter = "io.opentelemetry" % "opentelemetry-exporter-otlp" % openTelemetryInstV changing()
val openTelemetryOtlpHttpExporter = "io.opentelemetry" % "opentelemetry-exporter-otlp-http-trace" % openTelemetryInstV changing()
val openTelemetryZipkinExporter = "io.opentelemetry" % "opentelemetry-exporter-zipkin" % openTelemetryInstV changing()
val openTelemetryJaegerExporter = "io.opentelemetry" % "opentelemetry-exporter-jaeger" % openTelemetryInstV changing()
val grpc = "io.grpc" % "grpc-all" % "1.44.0"
Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
@@ -1 +1 @@
sbt.version=1.6.2
sbt.version=1.9.2

0 comments on commit 37e1225

Please sign in to comment.