Skip to content

Commit 2ca6669

Browse files
authored
Merge pull request #977 from pnerg/otel-trace-exporter
Added support for opentelemetry trace exporter
2 parents dab289b + 1c89af5 commit 2ca6669

File tree

11 files changed

+1025
-0
lines changed

11 files changed

+1025
-0
lines changed

build.sbt

+11
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ lazy val reporters = (project in file("reporters"))
476476
`kamon-influxdb`,
477477
`kamon-jaeger`,
478478
`kamon-newrelic`,
479+
`kamon-opentelemetry`,
479480
`kamon-prometheus`,
480481
`kamon-statsd`,
481482
`kamon-zipkin`,
@@ -591,6 +592,16 @@ lazy val `kamon-newrelic` = (project in file("reporters/kamon-newrelic"))
591592
)
592593
).dependsOn(`kamon-core`)
593594

595+
lazy val `kamon-opentelemetry` = (project in file("reporters/kamon-opentelemetry"))
596+
.disablePlugins(AssemblyPlugin)
597+
.settings(
598+
libraryDependencies ++= Seq(
599+
"io.opentelemetry" % "opentelemetry-proto" % "0.17.1",
600+
"io.grpc" % "grpc-netty" % "1.36.0",
601+
scalatest % "test",
602+
logbackClassic % "test"
603+
)
604+
).dependsOn(`kamon-core`, `kamon-testkit` % "test")
594605

595606
lazy val `kamon-prometheus` = (project in file("reporters/kamon-prometheus"))
596607
.disablePlugins(AssemblyPlugin)
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Kamon OpenTelemetry Exporter
2+
The exporter currently only provides a OpenTelemetry (OTLP) exporter for Kamon spans (metrics to be supported)
3+
4+
The reporter relies on the [opentelemetry-proto](https://github.com/open-telemetry/opentelemetry-proto) library for the gRPC communication with an OpenTelemetry (OTLP) service.
5+
6+
## Trace Exporter
7+
Converts internal finished Kamon spans to OTEL proto format and exports them the to configured endpoint.
8+
9+
## Metrics Exporter
10+
To be implemented
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# ======================================== #
2+
# kamon-otlp reference configuration #
3+
# ======================================== #
4+
5+
kamon.otel.trace {
6+
# Hostname and port where the OTLP Server is running
7+
host = "localhost"
8+
port = 4317
9+
10+
# Decides whether to use HTTP or HTTPS when connecting to the OTel server
11+
protocol = "http"
12+
13+
# default to support the ENV:s as described at
14+
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md
15+
endpoint = ${kamon.otel.trace.protocol}"://"${kamon.otel.trace.host}":"${kamon.otel.trace.port}
16+
endpoint = ${?OTEL_EXPORTER_OTLP_ENDPOINT}
17+
endpoint = ${?OTEL_EXPORTER_OTLP_TRACES_ENDPOINT}
18+
19+
# Enable or disable including tags from kamon.environment.tags as resource labels in the exported trace
20+
# Any keys containing a hyphen (-) will be converted to a dot (.) as that is the standard.
21+
# e.g. service-version becomes service.version
22+
# reference: https://github.com/kamon-io/Kamon/blob/master/core/kamon-core/src/main/resources/reference.conf#L23
23+
include-environment-tags = no
24+
25+
# Arbitrary key-value pairs that further identify the environment where this service instance is running.
26+
# These are added as KeyValue labels to the Resource part of the exported traces
27+
# Requires 'include-environment-tags' to be set to 'yes'
28+
#
29+
# kamon.environment.tags {
30+
# service-version = "x.x.x"
31+
# service-namespace = "ns"
32+
# service-instance.id = "xxx-yyy"
33+
# }
34+
}
35+
36+
kamon.modules {
37+
otel-trace-reporter {
38+
enabled = true
39+
name = "OpenTelemetry Trace Reporter"
40+
description = "Sends trace data to a OpenTelemetry server via gRPC"
41+
factory = "kamon.otel.OpenTelemetryTraceReporter$Factory"
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright 2013-2021 The Kamon Project <https://kamon.io>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package kamon.otel
17+
18+
import com.typesafe.config.Config
19+
import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest
20+
import io.opentelemetry.proto.common.v1.InstrumentationLibrary
21+
import io.opentelemetry.proto.resource.v1.Resource
22+
import io.opentelemetry.proto.trace.v1.ResourceSpans
23+
import kamon.Kamon
24+
import kamon.module.{Module, ModuleFactory, SpanReporter}
25+
import kamon.trace.Span
26+
import kamon.otel.SpanConverter._
27+
import org.slf4j.LoggerFactory
28+
29+
import java.util.Collections
30+
import scala.concurrent.ExecutionContext
31+
import scala.util.{Failure, Success}
32+
33+
object OpenTelemetryTraceReporter {
34+
private val logger = LoggerFactory.getLogger(classOf[OpenTelemetryTraceReporter])
35+
private val kamonVersion = Kamon.status().settings().version
36+
37+
class Factory extends ModuleFactory {
38+
override def create(settings: ModuleFactory.Settings): Module = {
39+
logger.info("Creating OpenTelemetry Trace Reporter")
40+
41+
val module = new OpenTelemetryTraceReporter(GrpcTraceService(_))(settings.executionContext)
42+
module.reconfigure(settings.config)
43+
module
44+
}
45+
}
46+
}
47+
48+
import kamon.otel.OpenTelemetryTraceReporter._
49+
/**
50+
* Converts internal finished Kamon spans to OpenTelemetry format and sends to a configured OpenTelemetry endpoint using gRPC.
51+
*/
52+
class OpenTelemetryTraceReporter(traceServiceFactory:Config=>TraceService)(implicit ec:ExecutionContext) extends SpanReporter {
53+
private var traceService:Option[TraceService] = None
54+
private var spanConverterFunc:Seq[Span.Finished]=>ResourceSpans = (_ => ResourceSpans.newBuilder().build())
55+
56+
override def reportSpans(spans: Seq[Span.Finished]): Unit = {
57+
if(!spans.isEmpty) {
58+
val resources = Collections.singletonList(spanConverterFunc(spans)) //all spans should belong to the same single resource
59+
val exportTraceServiceRequest = ExportTraceServiceRequest.newBuilder()
60+
.addAllResourceSpans(resources)
61+
.build()
62+
63+
traceService.foreach (
64+
_.export(exportTraceServiceRequest).onComplete {
65+
case Success(_) => logger.debug("Successfully exported traces")
66+
67+
//TODO is there result for which a retry is relevant? Perhaps a glitch in the receiving service
68+
//Keeping logs to debug as other exporters (e.g.Zipkin) won't log anything if it fails to export traces
69+
case Failure(t) => logger.debug("Failed to export traces", t)
70+
}
71+
)
72+
}
73+
}
74+
75+
override def reconfigure(newConfig: Config): Unit = {
76+
logger.info("Reconfigure OpenTelemetry Trace Reporter")
77+
78+
//pre-generate the function for converting Kamon span to proto span
79+
val instrumentationLibrary:InstrumentationLibrary = InstrumentationLibrary.newBuilder().setName("kamon").setVersion(kamonVersion).build()
80+
val resource:Resource = buildResource(newConfig.getBoolean("kamon.otel.trace.include-environment-tags"))
81+
this.spanConverterFunc = SpanConverter.toProtoResourceSpan(resource, instrumentationLibrary)
82+
83+
this.traceService = Option(traceServiceFactory.apply(newConfig))
84+
}
85+
86+
override def stop(): Unit = {
87+
logger.info("Stopping OpenTelemetry Trace Reporter")
88+
this.traceService.foreach(_.close())
89+
this.traceService = None
90+
}
91+
92+
/**
93+
* Builds the resource information added as resource labels to the exported traces
94+
* @param includeEnvTags
95+
* @return
96+
*/
97+
private def buildResource(includeEnvTags:Boolean):Resource = {
98+
val env = Kamon.environment
99+
val builder = Resource.newBuilder()
100+
.addAttributes(stringKeyValue("service.name", env.service))
101+
.addAttributes(stringKeyValue("telemetry.sdk.name", "kamon"))
102+
.addAttributes(stringKeyValue("telemetry.sdk.language", "scala"))
103+
.addAttributes(stringKeyValue("telemetry.sdk.version", kamonVersion))
104+
105+
//add all kamon.environment.tags as KeyValues to the Resource object
106+
if(includeEnvTags) {
107+
env.tags.iterator().map(toProtoKeyValue).foreach(builder.addAttributes)
108+
}
109+
110+
builder.build()
111+
}
112+
}

0 commit comments

Comments
 (0)