Skip to content

Instantly share code, notes, and snippets.

@calvinlfer
Last active February 21, 2025 17:45
Show Gist options
  • Save calvinlfer/ef0bd9e930e2f794ca7d6bf74c64e7c9 to your computer and use it in GitHub Desktop.
Save calvinlfer/ef0bd9e930e2f794ca7d6bf74c64e7c9 to your computer and use it in GitHub Desktop.
Ship ZIO Metrics to OpenTelemetry (OLTP gRPC) using zio-telemetry-opentelemetry by manually providing the instrumentation (this portion connects ZIO Metrics to the ZIO Opentelemetry machinery). I also have an example that uses auto-instrumentation
val scala3Version = "3.6.3"
lazy val root = project
.in(file("."))
.settings(
name := "zio-telemetry-playground",
version := "0.1.0-SNAPSHOT",
scalaVersion := scala3Version,
libraryDependencies ++=
Seq(
"dev.zio" %% "zio" % "2.1.15",
"dev.zio" %% "zio-logging-slf4j-bridge" % "2.4.0", // route all SLF4J logs to ZIO RTS
"dev.zio" %% "zio-opentelemetry" % "3.1.1", // integration for OTLP metrics + ZIO
"dev.zio" %% "zio-opentelemetry-zio-logging" % "3.1.1", // integration for OTLP logs + ZIO
"io.opentelemetry" % "opentelemetry-sdk" % "1.47.0",
"io.opentelemetry" % "opentelemetry-exporter-otlp" % "1.47.0"
)
)
services:
otel-lgtm:
image: grafana/otel-lgtm:latest
container_name: otel-lgtm
ports:
- "3000:3000" # Grafana UI
- "4317:4317" # OpenTelemetry Collector gRPC
- "4318:4318" # OpenTelemetry Collector HTTP
volumes:
- ./otel-lgtm-data:/var/lib/grafana
import zio.*
import zio.telemetry.opentelemetry.OpenTelemetry
import io.opentelemetry.sdk.OpenTelemetrySdk
import io.opentelemetry.api.metrics.MeterProvider
import io.opentelemetry.sdk.metrics.SdkMeterProvider
import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter
import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter
import io.opentelemetry.sdk.metrics.`export`.PeriodicMetricReader
import io.opentelemetry.sdk.resources.Resource as OltpResource
import zio.metrics.Metric
import java.util.concurrent.TimeUnit
import zio.metrics.jvm.DefaultJvmMetrics
import io.opentelemetry.sdk.logs.`export`.SimpleLogRecordProcessor
import io.opentelemetry.sdk.logs.`export`.BatchLogRecordProcessor
import io.opentelemetry.sdk.logs.SdkLoggerProvider
import zio.logging.slf4j.bridge.Slf4jBridge
object Main extends ZIOAppDefault:
override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] =
// metrics
val meterProvider: RIO[Scope, SdkMeterProvider] =
val metricExporter: RIO[Scope, OtlpGrpcMetricExporter] =
ZIO.fromAutoCloseable:
ZIO.attempt:
OtlpGrpcMetricExporter.builder().setEndpoint("http://localhost:4317").build()
metricExporter
.flatMap: exporter =>
ZIO
.fromAutoCloseable:
ZIO.attempt:
PeriodicMetricReader
.builder(exporter)
.setInterval(5, TimeUnit.SECONDS)
.build()
.map: meterReader =>
SdkMeterProvider
.builder()
.setResource(
OltpResource
.builder()
.put("service.name", "zio-telemetry-playground")
.put("job", "zio-telemetry-playground")
.build()
)
.registerMetricReader(meterReader)
.build()
// logging
val loggerProvider: RIO[Scope, SdkLoggerProvider] =
val logExporter =
ZIO.fromAutoCloseable:
ZIO.attempt:
OtlpGrpcLogRecordExporter
.builder()
.setEndpoint("http://localhost:4317")
.setCompression("gzip")
.build()
logExporter
.flatMap: exporter =>
ZIO.fromAutoCloseable:
ZIO.attempt:
BatchLogRecordProcessor
.builder(exporter)
.setMaxExportBatchSize(100)
.build()
.flatMap: processor =>
ZIO.fromAutoCloseable:
ZIO.attempt:
SdkLoggerProvider
.builder()
.setResource(
OltpResource
.builder()
.put("service.name", "zio-telemetry-playground")
.put("job", "zio-telemetry-playground")
.build()
)
.addLogRecordProcessor(processor)
.build()
val otelSdkLayer: TaskLayer[OpenTelemetrySdk] =
ZLayer.scoped:
for
meterProvider <- meterProvider
loggerProvider <- loggerProvider
otelSdk <- ZIO.fromAutoCloseable:
ZIO.attempt:
OpenTelemetrySdk
.builder()
.setMeterProvider(meterProvider)
.setLoggerProvider(loggerProvider)
.build()
yield otelSdk
Runtime.removeDefaultLoggers >>>
Slf4jBridge.initialize >>> // route all SLF4J logs to ZIO RTS
otelSdkLayer >+>
OpenTelemetry.contextZIO >+>
OpenTelemetry.metrics("zio-telemetry-playground", None, None) >+>
OpenTelemetry.logging("zio-telemetry-playground", logLevel = LogLevel.Debug) >+> // log all ZIO logs to OTLP
OpenTelemetry.zioMetrics >>> // ship all metrics to OTLP
DefaultJvmMetrics.live.unit
override def run =
val metric: Metric.Counter[Any] =
Metric
.counter("test_cal")
.contramap[Any](_ => 1L)
(ZIO.logInfo("Hello from cal!")
@@ ZIOAspect.annotated(
"bim" -> "boom",
"bim2" -> "boom2",
"bim3" -> "boom3"
)
@@ metric).repeat(Schedule.spaced(1.second))
@calvinlfer
Copy link
Author

image

@calvinlfer
Copy link
Author

Structured OTLP Logs
image

@calvinlfer
Copy link
Author

Auto instrumentation makes this even easier provided you use the java-agent:

build.sbt

val scala3Version = "3.6.3"

inThisBuild(
  List(
    semanticdbEnabled := true
  )
)

lazy val root = project
  .in(file("."))
  .enablePlugins(JavaAgent)
  .settings(
    name         := "zio-telemetry-playground",
    version      := "0.1.0-SNAPSHOT",
    scalaVersion := scala3Version,
    scalacOptions ++= Seq("-Wunused:imports"),
    javaAgents += "io.opentelemetry.javaagent" % "opentelemetry-javaagent" % "2.13.1" % "compile",
    fork                                      := true,
    libraryDependencies ++=
      Seq(
        "dev.zio"         %% "zio"                           % "2.1.15",
        "dev.zio"         %% "zio-logging-slf4j-bridge"      % "2.4.0", // route all SLF4J logs to ZIO RTS
        "dev.zio"         %% "zio-opentelemetry"             % "3.1.1",
        "dev.zio"         %% "zio-opentelemetry-zio-logging" % "3.1.1",
        "io.opentelemetry" % "opentelemetry-sdk"             % "1.47.0",
        "io.opentelemetry" % "opentelemetry-exporter-otlp"   % "1.47.0"
      )
  )

project/plugins.sbt

addSbtPlugin("org.scalameta"  % "sbt-scalafmt"        % "2.5.4")
addSbtPlugin("ch.epfl.scala"  % "sbt-scalafix"        % "0.14.2")
addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.11.1")
addSbtPlugin("com.github.sbt" % "sbt-javaagent"       % "0.1.7")
addSbtPlugin("nl.gn0s1s"      % "sbt-dotenv"          % "3.1.1")

.env

OTEL_SERVICE_NAME="zio-telemetry-auto-example"
OTEL_EXPORTER_OTLP_PROTOCOL="grpc"
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"

# 100% sampling rate for traces (we aren't using traces in this example)
OTEL_TRACES_SAMPLER="always_on"

Main.scala

import zio.*
import zio.logging.slf4j.bridge.Slf4jBridge
import zio.metrics.Metric
import zio.metrics.jvm.DefaultJvmMetrics
import zio.telemetry.opentelemetry.OpenTelemetry

object Main extends ZIOAppDefault:

  override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] =
    Runtime.removeDefaultLoggers >>>
      Slf4jBridge.initialize >>>   // route all SLF4J logs to ZIO RTS
      OpenTelemetry.global >+>     // auto-instrumentation handshake (requires javaagent)
      OpenTelemetry.contextJVM >+> // auto-instrumentation context propagation handshake
      OpenTelemetry.metrics("zio-telemetry-playground", None, None) >+>
      OpenTelemetry.logging("zio-telemetry-playground", logLevel = LogLevel.Debug) >+> // log all ZIO logs to OTLP
      OpenTelemetry.zioMetrics >>>                                                     // ship all metrics to OTLP
      DefaultJvmMetrics.live.unit

  override def run =
    val metric: Metric.Counter[Any] =
      Metric
        .counter("test_cal")
        .contramap[Any](_ => 1L)

    (ZIO.logInfo("Hello from cal auto!")
      @@ ZIOAspect.annotated(
        "iam" -> "auto",
        "bim" -> "boom"
      ) @@ metric).repeat(Schedule.spaced(1.second))

@calvinlfer
Copy link
Author

An example using zio-telemetry + ZIO showing how to do:

  • tracing
  • logging
  • metrics

Main.scala

import zio.*
import zio.logging.slf4j.bridge.Slf4jBridge
import zio.metrics.Metric
import zio.metrics.jvm.DefaultJvmMetrics
import zio.telemetry.opentelemetry.OpenTelemetry
import zio.telemetry.opentelemetry.context.ContextStorage
import zio.telemetry.opentelemetry.tracing.Tracing

object Main extends ZIOApp:

  override type Environment = ContextStorage & Tracing

  override implicit def environmentTag: EnvironmentTag[Environment] = EnvironmentTag.tagFromTagMacro[Environment]

  override val bootstrap: ZLayer[ZIOAppArgs, Any, Environment] =
    ZLayer.make[Environment](
      Runtime.removeDefaultLoggers,
      Slf4jBridge.initialize,   // route all SLF4J logs to ZIO RTS
      OpenTelemetry.global,     // auto-instrumentation handshake (requires javaagent)
      OpenTelemetry.contextJVM, // auto-instrumentation context propagation handshake
      OpenTelemetry.tracing("zio-telemetry-playground", None, None, logAnnotated = true),
      OpenTelemetry.metrics("zio-telemetry-playground", None, None),
      OpenTelemetry.logging("zio-telemetry-playground", logLevel = LogLevel.Debug), // log all ZIO logs to OTLP
      OpenTelemetry.zioMetrics,                                                     // ship all metrics to OTLP
      DefaultJvmMetrics.live.unit
    )

  override def run =
    val metric: Metric.Counter[Any] =
      Metric
        .counter("test_cal")
        .contramap[Any](_ => 1L)

    ZIO.serviceWithZIO[Tracing]: tracing =>
      (ZIO.logInfo("Hello from cal auto!")
        @@ ZIOAspect.annotated("iam" -> "auto", "bim" -> "boom")
        @@ metric
        @@ tracing.aspects.span("test_span_cal")).repeat(Schedule.spaced(1.second))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment