diff --git a/money-otel-handler/src/main/scala/com/comcast/money/otel/handlers/MoneyReadableSpanData.scala b/money-otel-handler/src/main/scala/com/comcast/money/otel/handlers/MoneyReadableSpanData.scala index 323f7b30..cb96800d 100644 --- a/money-otel-handler/src/main/scala/com/comcast/money/otel/handlers/MoneyReadableSpanData.scala +++ b/money-otel-handler/src/main/scala/com/comcast/money/otel/handlers/MoneyReadableSpanData.scala @@ -24,14 +24,16 @@ import io.opentelemetry.sdk.resources.Resource import io.opentelemetry.sdk.trace.ReadableSpan import io.opentelemetry.sdk.trace.data.{ EventData, LinkData, SpanData, StatusData } import io.opentelemetry.api.trace.{ SpanContext, SpanKind, TraceState, Span => OtelSpan } +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes import scala.collection.JavaConverters._ -private[otel] class MoneyReadableSpanData(info: SpanInfo) extends ReadableSpan with SpanData { +private[otel] class MoneyReadableSpanData(info: SpanInfo, resourceAttributes: Attributes) extends ReadableSpan with SpanData { private val id = info.id private lazy val spanContext = id.toSpanContext private lazy val parentSpanContext = convertParentSpanContext(info.id) private lazy val libraryInfo = convertLibraryInfo(info.library) + private lazy val resource = convertResource(info, resourceAttributes) private lazy val attributes = convertAttributes(info.notes) private lazy val events = convertEvents(info.events) private lazy val links = convertLinks(info.links) @@ -45,7 +47,7 @@ private[otel] class MoneyReadableSpanData(info: SpanInfo) extends ReadableSpan w override def getLatencyNanos: Long = info.durationNanos override def getTraceId: String = id.traceIdAsHex override def getSpanId: String = id.selfIdAsHex - override def getResource: Resource = Resource.getDefault + override def getResource: Resource = resource override def getKind: SpanKind = info.kind override def getStartEpochNanos: Long = info.startTimeNanos override def getLinks: util.List[LinkData] = links @@ -72,6 +74,18 @@ private[otel] class MoneyReadableSpanData(info: SpanInfo) extends ReadableSpan w InstrumentationLibraryInfo.empty } + private def convertResource(info: SpanInfo, resourceAttributes: Attributes): Resource = { + val library = info.library() + Resource.create(Attributes.builder() + .put(ResourceAttributes.SERVICE_NAME, info.appName()) + .put(ResourceAttributes.TELEMETRY_SDK_NAME, library.name()) + .put(ResourceAttributes.TELEMETRY_SDK_VERSION, library.version()) + .put(ResourceAttributes.TELEMETRY_SDK_LANGUAGE, "scala") + .put(ResourceAttributes.HOST_NAME, info.host()) + .putAll(resourceAttributes) + .build()) + } + private def appendNoteToBuilder[T](builder: AttributesBuilder, note: Note[T]): AttributesBuilder = builder.put(note.key, note.value) diff --git a/money-otel-handler/src/main/scala/com/comcast/money/otel/handlers/OtelSpanHandler.scala b/money-otel-handler/src/main/scala/com/comcast/money/otel/handlers/OtelSpanHandler.scala index 0222ba02..7f4f1d08 100644 --- a/money-otel-handler/src/main/scala/com/comcast/money/otel/handlers/OtelSpanHandler.scala +++ b/money-otel-handler/src/main/scala/com/comcast/money/otel/handlers/OtelSpanHandler.scala @@ -18,9 +18,11 @@ package com.comcast.money.otel.handlers import com.comcast.money.api.{ SpanHandler, SpanInfo } import com.typesafe.config.{ Config, ConfigFactory } +import io.opentelemetry.api.common.{ AttributeKey, Attributes } import io.opentelemetry.sdk.trace.SpanProcessor import io.opentelemetry.sdk.trace.`export`.{ BatchSpanProcessor, SimpleSpanProcessor, SpanExporter } +import scala.collection.JavaConverters._ import java.time.Duration /** @@ -29,6 +31,7 @@ import java.time.Duration */ abstract class OtelSpanHandler(config: Config) extends SpanHandler { + private[otel] val resourceAttributes: Attributes = createResourceAttributes(config) private[otel] val exporter: SpanExporter = createSpanExporter(getExporterConfig(config)) private[otel] val processor: SpanProcessor = createSpanProcessor(exporter, config) @@ -38,7 +41,24 @@ abstract class OtelSpanHandler(config: Config) extends SpanHandler { * @param span `SpanInfo` that contains the information for the completed span */ override def handle(span: SpanInfo): Unit = { - processor.onEnd(new MoneyReadableSpanData(span)) + processor.onEnd(new MoneyReadableSpanData(span, resourceAttributes)) + } + + protected def createResourceAttributes(config: Config): Attributes = { + + val resourceKey = "resource" + if (config.hasPath(resourceKey)) { + val attributes = Attributes.builder() + val resourceConfig = config.getConfig(resourceKey) + for (entry <- resourceConfig.entrySet().asScala) { + val key = entry.getKey + val value = resourceConfig.getString(key) + attributes.put(AttributeKey.stringKey(key), value) + } + attributes.build() + } else { + Attributes.empty() + } } protected def createSpanProcessor(spanExporter: SpanExporter, config: Config): SpanProcessor = { diff --git a/money-otel-handler/src/test/java/com/comcast/money/otel/handlers/OtelSpanHandlerSpec.java b/money-otel-handler/src/test/java/com/comcast/money/otel/handlers/OtelSpanHandlerSpec.java index 4bf2572e..e2d56dfc 100644 --- a/money-otel-handler/src/test/java/com/comcast/money/otel/handlers/OtelSpanHandlerSpec.java +++ b/money-otel-handler/src/test/java/com/comcast/money/otel/handlers/OtelSpanHandlerSpec.java @@ -20,12 +20,15 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; import io.opentelemetry.sdk.trace.export.BatchSpanProcessorBuilder; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -137,6 +140,23 @@ public void configuresBatchSpanProcessor() { assertThat(spanContext.getSpanId()).isEqualTo(spanId.selfIdAsHex()); } + @Test + public void configuresResourceAttributes() { + + Config config = ConfigFactory.parseString( + "resource = {\n" + + " foo = \"bar\"\n" + + " container.name = \"money-core-autoconf\"\n" + + "}" + ); + + OtelSpanHandler underTest = new TestOtelSpanHandler(config); + assertThat(underTest.resourceAttributes()).isEqualTo(Attributes.of( + AttributeKey.stringKey("foo"), "bar", + ResourceAttributes.CONTAINER_NAME, "money-core-autoconf" + )); + } + class TestOtelSpanHandler extends OtelSpanHandler { public TestOtelSpanHandler(Config config) { super(config); diff --git a/money-otel-handler/src/test/scala/com/comcast/money/otel/handlers/MoneyReadableSpanDataSpec.scala b/money-otel-handler/src/test/scala/com/comcast/money/otel/handlers/MoneyReadableSpanDataSpec.scala index 0735e28c..c747c46b 100644 --- a/money-otel-handler/src/test/scala/com/comcast/money/otel/handlers/MoneyReadableSpanDataSpec.scala +++ b/money-otel-handler/src/test/scala/com/comcast/money/otel/handlers/MoneyReadableSpanDataSpec.scala @@ -23,6 +23,7 @@ import io.opentelemetry.api.common.{ AttributeKey, Attributes } import io.opentelemetry.sdk.resources.Resource import io.opentelemetry.api.trace.{ Span, SpanContext, SpanKind, StatusCode, TraceFlags, TraceState } import io.opentelemetry.sdk.trace.data.StatusData +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -31,10 +32,11 @@ import scala.collection.JavaConverters._ class MoneyReadableSpanDataSpec extends AnyWordSpec with Matchers { val spanId = SpanId.createFrom(UUID.fromString("01234567-890A-BCDE-F012-34567890ABCD"), 81985529216486895L, 81985529216486895L) val childSpanId = SpanId.createFrom(UUID.fromString("01234567-890A-BCDE-F012-34567890ABCD"), 1147797409030816545L, 81985529216486895L) + val resourceAttributes = Attributes.of(AttributeKey.stringKey("foo"), "bar") "MoneyReadableSpanDataSpec" should { "wrap Money SpanInfo" in { - val underTest = new MoneyReadableSpanData(TestSpanInfo(spanId)) + val underTest = new MoneyReadableSpanData(TestSpanInfo(spanId), resourceAttributes) underTest.getInstrumentationLibraryInfo.getName shouldBe "test" underTest.getTraceId shouldBe "01234567890abcdef01234567890abcd" @@ -47,7 +49,13 @@ class MoneyReadableSpanDataSpec extends AnyWordSpec with Matchers { underTest.hasEnded shouldBe true underTest.getLinks.asScala should contain(MoneyLink(link)) underTest.getTotalRecordedLinks shouldBe 0 - underTest.getResource shouldBe Resource.getDefault + underTest.getResource.getAttributes shouldBe Attributes.of( + ResourceAttributes.TELEMETRY_SDK_NAME, "test", + ResourceAttributes.TELEMETRY_SDK_VERSION, "0.0.1", + ResourceAttributes.TELEMETRY_SDK_LANGUAGE, "scala", + ResourceAttributes.SERVICE_NAME, "app", + ResourceAttributes.HOST_NAME, "host", + AttributeKey.stringKey("foo"), "bar") underTest.getLatencyNanos shouldBe 2000000L underTest.getStatus shouldBe StatusData.create(StatusCode.OK, "description") underTest.getTotalAttributeCount shouldBe 1 @@ -59,7 +67,7 @@ class MoneyReadableSpanDataSpec extends AnyWordSpec with Matchers { } "wrap child Money SpanInfo" in { - val underTest = new MoneyReadableSpanData(TestSpanInfo(childSpanId)) + val underTest = new MoneyReadableSpanData(TestSpanInfo(childSpanId), resourceAttributes) underTest.getInstrumentationLibraryInfo.getName shouldBe "test" underTest.getTraceId shouldBe "01234567890abcdef01234567890abcd" @@ -72,7 +80,13 @@ class MoneyReadableSpanDataSpec extends AnyWordSpec with Matchers { underTest.hasEnded shouldBe true underTest.getLinks.asScala should contain(MoneyLink(link)) underTest.getTotalRecordedLinks shouldBe 0 - underTest.getResource shouldBe Resource.getDefault + underTest.getResource.getAttributes shouldBe Attributes.of( + ResourceAttributes.TELEMETRY_SDK_NAME, "test", + ResourceAttributes.TELEMETRY_SDK_VERSION, "0.0.1", + ResourceAttributes.TELEMETRY_SDK_LANGUAGE, "scala", + ResourceAttributes.SERVICE_NAME, "app", + ResourceAttributes.HOST_NAME, "host", + AttributeKey.stringKey("foo"), "bar") underTest.getLatencyNanos shouldBe 2000000L underTest.getStatus shouldBe StatusData.create(StatusCode.OK, "description") underTest.getTotalAttributeCount shouldBe 1