Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OpenTelemetry module #539

Merged
merged 8 commits into from
Oct 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/series/0.1')
run: mkdir -p modules/mtl/native/target modules/lightstep-grpc/target modules/log/jvm/target modules/noop/jvm/target modules/mock/target modules/examples/target target modules/log/native/target .js/target modules/core/native/target modules/docs/target modules/lightstep-http/target modules/datadog/target modules/xray/jvm/target modules/opentracing/target modules/noop/native/target modules/xray/js/target modules/core/js/target modules/noop/js/target modules/core/jvm/target .jvm/target modules/jaeger/target .native/target modules/opencensus/target modules/honeycomb/target modules/log/js/target modules/mtl/js/target modules/newrelic/target modules/log-odin/target modules/mtl/jvm/target modules/lightstep/target project/target
run: mkdir -p modules/mtl/native/target modules/lightstep-grpc/target modules/log/jvm/target modules/noop/jvm/target modules/mock/target modules/examples/target target modules/log/native/target .js/target modules/core/native/target modules/docs/target modules/lightstep-http/target modules/datadog/target modules/xray/jvm/target modules/opentracing/target modules/noop/native/target modules/xray/js/target modules/core/js/target modules/noop/js/target modules/core/jvm/target .jvm/target modules/jaeger/target .native/target modules/opencensus/target modules/honeycomb/target modules/log/js/target modules/mtl/js/target modules/newrelic/target modules/log-odin/target modules/mtl/jvm/target modules/opentelemetry/target modules/lightstep/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/series/0.1')
run: tar cf targets.tar modules/mtl/native/target modules/lightstep-grpc/target modules/log/jvm/target modules/noop/jvm/target modules/mock/target modules/examples/target target modules/log/native/target .js/target modules/core/native/target modules/docs/target modules/lightstep-http/target modules/datadog/target modules/xray/jvm/target modules/opentracing/target modules/noop/native/target modules/xray/js/target modules/core/js/target modules/noop/js/target modules/core/jvm/target .jvm/target modules/jaeger/target .native/target modules/opencensus/target modules/honeycomb/target modules/log/js/target modules/mtl/js/target modules/newrelic/target modules/log-odin/target modules/mtl/jvm/target modules/lightstep/target project/target
run: tar cf targets.tar modules/mtl/native/target modules/lightstep-grpc/target modules/log/jvm/target modules/noop/jvm/target modules/mock/target modules/examples/target target modules/log/native/target .js/target modules/core/native/target modules/docs/target modules/lightstep-http/target modules/datadog/target modules/xray/jvm/target modules/opentracing/target modules/noop/native/target modules/xray/js/target modules/core/js/target modules/noop/js/target modules/core/jvm/target .jvm/target modules/jaeger/target .native/target modules/opencensus/target modules/honeycomb/target modules/log/js/target modules/mtl/js/target modules/newrelic/target modules/log-odin/target modules/mtl/jvm/target modules/opentelemetry/target modules/lightstep/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/series/0.1')
Expand Down
17 changes: 17 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ lazy val root = tlCrossRootProject.aggregate(
jaeger,
honeycomb,
opencensus,
opentelemetry,
lightstep, lightstepGrpc, lightstepHttp,
opentracing,
datadog,
Expand Down Expand Up @@ -192,6 +193,22 @@ lazy val opentracing = project
)
)

lazy val opentelemetry = project
.in(file("modules/opentelemetry"))
.dependsOn(core.jvm)
.enablePlugins(AutomateHeaderPlugin)
.settings(commonSettings)
.settings(
name := "natchez-opentelemetry",
description := "Base OpenTelemetry Utilities for Natchez",
tlVersionIntroduced := List("2.12", "2.13", "3").map(_ -> "0.1.7").toMap,
libraryDependencies ++= Seq(
"org.scala-lang.modules" %% "scala-collection-compat" % collectionCompatVersion,
"io.opentelemetry" % "opentelemetry-sdk" % "1.12.0"
)
)



lazy val datadog = project
.in(file("modules/datadog"))
Expand Down
4 changes: 2 additions & 2 deletions modules/opencensus/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# OpenCensus

OpenCensus is capable of exporting to different collector types.
Exporters are added registered globally against a singleton registry.
Exporters are registered globally against a singleton registry.
There is nothing stopping someone registering a new exporter outside
of a side effect, so it is up to the user whether to do so inside the
effects system or not.
Expand All @@ -24,4 +24,4 @@ The recommended approach for registering exporters is to use a resource.
OcAgentTraceExporter.unregister()
))
.flatMap(_ => Resource.liftF(entryPoint[F]))
```
```
65 changes: 65 additions & 0 deletions modules/opentelemetry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# OpenTelemetry

OpenTelemetry is capable of exporting to different collector types. Exporters are registered globally against a
singleton registry. There is nothing stopping someone registering a new exporter outside of a side effect, so it is up
to the user whether to do so inside the effects system or not.

The recommended approach for registering exporters is to use a resource. The snippet below shows how this may be done
for the Google Cloud Trace exporter implementation:

The google cloud trace export can be found here:

```scala
libraryDependencies ++= Seq(
"com.google.cloud.opentelemetry" % "exporter-trace" % "0.20.0",
"com.google.cloud" % "google-cloud-trace" % "2.1.9"
)
```

```scala
import cats.effect.{Resource, Sync}
import io.opentelemetry.api.GlobalOpenTelemetry
import natchez.opentelemetry.OpenTelemetry
import io.opentelemetry.sdk.OpenTelemetrySdk
import io.opentelemetry.sdk.trace.SdkTracerProvider
import io.opentelemetry.sdk.trace.`export`.BatchSpanProcessor
import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator
import io.opentelemetry.context.propagation.{ContextPropagators, TextMapPropagator}
import com.google.cloud.opentelemetry.trace.{TraceConfiguration, TraceExporter}
import natchez.opentelemetry.OpenTelemetry

def entrypoint[F[_] : Sync](projectId: String)(configure: TraceConfiguration.Builder => TraceConfiguration.Builder): Resource[F, EntryPoint[F]] =
Resource
.make(
Sync[F].delay(
OpenTelemetrySdk
.builder()
.setTracerProvider(
SdkTracerProvider
.builder()
.addSpanProcessor(
BatchSpanProcessor
.builder(
TraceExporter
.createWithConfiguration(
configure(TraceConfiguration.builder().setProjectId(projectId)).build()
)
)
.build()
)
.build()
).setPropagators(
ContextPropagators.create(
TextMapPropagator.composite(W3CTraceContextPropagator.getInstance(), W3CBaggagePropagator.getInstance())
)
).build()
)
)(sdk =>
Sync[F].blocking {
sdk.getSdkTracerProvider.close()
}
)
.flatMap(sdk => Resource.eval(OpenTelemetry.entryPointForSdk[F](sdk)))

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) 2019-2020 by Rob Norris and Contributors
// This software is licensed under the MIT License (MIT).
// For more information see LICENSE or https://opensource.org/licenses/MIT

package natchez.opentelemetry

import cats.syntax.all._
import cats.effect.{Resource, Sync}
import io.opentelemetry.sdk.OpenTelemetrySdk
import natchez.{EntryPoint, Kernel, Span}

object OpenTelemetry {
def entryPointForSdk[F[_] : Sync](sdk: OpenTelemetrySdk): F[EntryPoint[F]] =
Sync[F]
.delay(sdk.getTracer("natchez"))
.map { t =>
new EntryPoint[F] {
def continue(name: String, kernel: Kernel): Resource[F, Span[F]] =
Resource.makeCase(OpenTelemetrySpan.fromKernel(sdk, t, name, kernel))(OpenTelemetrySpan.finish).widen

def root(name: String): Resource[F, Span[F]] =
Resource.makeCase(OpenTelemetrySpan.root(sdk, t, name))(OpenTelemetrySpan.finish).widen

def continueOrElseRoot(name: String, kernel: Kernel): Resource[F, Span[F]] =
Resource
.makeCase(OpenTelemetrySpan.fromKernelOrElseRoot(sdk, t, name, kernel))(OpenTelemetrySpan.finish)
.widen
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright (c) 2019-2020 by Rob Norris and Contributors
// This software is licensed under the MIT License (MIT).
// For more information see LICENSE or https://opensource.org/licenses/MIT

package natchez.opentelemetry


import cats.effect.{Resource, Sync}
import cats.effect.kernel.Resource.ExitCase
import cats.effect.kernel.Resource.ExitCase.{Canceled, Errored, Succeeded}
import cats.syntax.all._
import io.opentelemetry.api.trace.StatusCode
import io.opentelemetry.context.propagation.{TextMapGetter, TextMapSetter}
import io.opentelemetry.context.Context

import java.lang
import io.opentelemetry.api.trace.{Tracer, Span => TSpan}
import io.opentelemetry.sdk.OpenTelemetrySdk
import natchez.{Fields, Kernel, Span, TraceValue}
import natchez.TraceValue.{BooleanValue, NumberValue, StringValue}

import java.net.URI
import scala.collection.mutable

private[opentelemetry] final case class OpenTelemetrySpan[F[_] : Sync](sdk: OpenTelemetrySdk, tracer: Tracer, span: TSpan) extends Span[F] {

import OpenTelemetrySpan._

override def put(fields: (String, TraceValue)*): F[Unit] =
fields.toList.traverse_ {
case (k, StringValue(v)) =>
val safeString =
if (v == null) "null" else v
Sync[F].delay(span.setAttribute(k, safeString))
case (k, NumberValue(v)) =>
Sync[F].delay(span.setAttribute(k, v.doubleValue()))
case (k, BooleanValue(v)) =>
Sync[F].delay(span.setAttribute(k, v))
}

override def kernel: F[Kernel] =
Sync[F].delay {
val headers: mutable.Map[String, String] = mutable.Map.empty[String, String]
sdk.getPropagators.getTextMapPropagator.inject(Context.current(), headers, spanContextSetter)
Kernel(headers.toMap)
}

override def span(name: String): Resource[F, Span[F]] =
Span.putErrorFields(Resource.makeCase(OpenTelemetrySpan.child(this, name))(OpenTelemetrySpan.finish).widen)

def traceId: F[Option[String]] =
Sync[F].pure {
val rawId = span.getSpanContext.getTraceId
if (rawId.nonEmpty) rawId.some else none
}

def spanId: F[Option[String]] =
Sync[F].pure {
val rawId = span.getSpanContext.getSpanId
if (rawId.nonEmpty) rawId.some else none
}

def traceUri: F[Option[URI]] = none[URI].pure[F]

}

private[opentelemetry] object OpenTelemetrySpan {
def finish[F[_] : Sync]: (OpenTelemetrySpan[F], ExitCase) => F[Unit] = { (outer, exitCase) =>
for {
// collect error details, if any
_ <-
exitCase.some
.collect {
case Errored(t: Fields) => t.fields.toList
}
.traverse(outer.put)
_ <- Sync[F].delay {
exitCase match {
case Succeeded => outer.span.setStatus(StatusCode.OK)
case Canceled => outer.span.setStatus(StatusCode.UNSET)
case Errored(ex) =>
outer.span.setStatus(StatusCode.ERROR, ex.getMessage)
outer.span.recordException(ex)
}
}
_ <- Sync[F].delay(outer.span.end())
} yield ()
}

def child[F[_] : Sync](
parent: OpenTelemetrySpan[F],
name: String
): F[OpenTelemetrySpan[F]] =
Sync[F]
.delay(
parent.tracer
.spanBuilder(name)
.setParent(Context.current().`with`(parent.span))
.startSpan()
)
.map(OpenTelemetrySpan(parent.sdk, parent.tracer, _))

def root[F[_] : Sync](
sdk: OpenTelemetrySdk,
tracer: Tracer,
name: String
): F[OpenTelemetrySpan[F]] =
Sync[F]
.delay(
tracer
.spanBuilder(name)
.startSpan()
)
.map(OpenTelemetrySpan(sdk, tracer, _))

def fromKernel[F[_] : Sync](
sdk: OpenTelemetrySdk,
tracer: Tracer,
name: String,
kernel: Kernel
): F[OpenTelemetrySpan[F]] =
Sync[F]
.delay {
val ctx = sdk.getPropagators.getTextMapPropagator
.extract(Context.current(), kernel, spanContextGetter)
tracer.spanBuilder(name).setParent(ctx).startSpan()
}
.map(OpenTelemetrySpan(sdk, tracer, _))

def fromKernelOrElseRoot[F[_]](
sdk: OpenTelemetrySdk,
tracer: Tracer,
name: String,
kernel: Kernel
)(implicit ev: Sync[F]): F[OpenTelemetrySpan[F]] =
fromKernel(sdk, tracer, name, kernel).recoverWith {
case _: NoSuchElementException =>
root(sdk, tracer, name) // means headers are incomplete or invalid
case _: NullPointerException =>
root(sdk, tracer, name) // means headers are incomplete or invalid
}

private val spanContextGetter: TextMapGetter[Kernel] = new TextMapGetter[Kernel] {

import scala.jdk.CollectionConverters._

override def keys(carrier: Kernel): lang.Iterable[String] = carrier.toHeaders.keys.asJava

override def get(carrier: Kernel, key: String): String =
carrier.toHeaders(key)
}

private val spanContextSetter = new TextMapSetter[mutable.Map[String, String]] {
override def set(carrier: mutable.Map[String, String], key: String, value: String): Unit = {
carrier.put(key, value)
()
}
}
}