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

Allow direct access to trace and span id for zio-logging's LogFormat #842

Merged
merged 14 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
24 changes: 21 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ lazy val root =
project
.in(file("."))
.settings(publish / skip := true)
.aggregate(opentracing, opentelemetry, opencensus, docs)
.aggregate(opentracing, opentelemetry, opencensus, opentelemetryZioLogging, docs)

lazy val opentracing =
project
Expand Down Expand Up @@ -146,6 +146,19 @@ lazy val opencensus = project
.settings(mimaSettings(failOnProblem = true))
.settings(unusedCompileDependenciesFilter -= moduleFilter("io.opencensus", "opencensus-impl"))

lazy val opentelemetryZioLogging = project
.in(file("opentelemetry-zio-logging"))
.settings(enableZIO())
.settings(
stdModuleSettings(
name = Some("zio-opentelemetry-zio-logging"),
packageName = Some("zio.telemetry.opentelemetry.zio.logging")
)
)
.settings(libraryDependencies ++= Dependencies.opentelemetryZioLogging)
.settings(mimaSettings(failOnProblem = true))
.dependsOn(opentelemetry)

lazy val opentracingExample =
project
.in(file("opentracing-example"))
Expand Down Expand Up @@ -194,9 +207,14 @@ lazy val docs =
projectName := "ZIO Telemetry",
mainModuleName := (opentracing / moduleName).value,
projectStage := ProjectStage.ProductionReady,
ScalaUnidoc / unidoc / unidocProjectFilter := inProjects(opentracing, opentelemetry, opencensus),
ScalaUnidoc / unidoc / unidocProjectFilter := inProjects(
opentracing,
opentelemetry,
opencensus,
opentelemetryZioLogging
),
scalacOptions --= Seq("-Yno-imports", "-Xfatal-warnings")
)
.settings(unusedCompileDependenciesFilter -= moduleFilter("org.scalameta", "mdoc"))
.dependsOn(opentracing, opentelemetry, opencensus)
.dependsOn(opentracing, opentelemetry, opencensus, opentelemetryZioLogging)
.enablePlugins(WebsitePlugin)
36 changes: 36 additions & 0 deletions docs/opentelemetry-zio-logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
id: opentelemetry-zio-logging
title: "OpenTelemetry ZIO Logging"
---

`zio-opentelemetry` logging facilities are implemented around OpenTelemetry Logging.

In order to use `zio-opentelemetry` feature with `zio-logging` you should use `zio-opentelemetry-zio-logging` module.

`OpenTelemetry ZIO Logging` contains utilities for combining ZIO Opentelemetry with ZIO Logging

## Installation

```scala
"dev.zio" %% "zio-opentelemetry-zio-logging" % "<version>"
```

## Features

### Log formats

This library implements [Log Format](https://zio.dev/zio-logging/formatting-log-records) for span information (`spanId` and `traceId`)

```scala
ZIO.serviceWithZIO[ContextStorage] { ctxStorage =>
import zio.logging.console
import zio.logging.LogFormat._
import zio.telemetry.opentelemetry.zio.logging.TelemetryLogFormats

val spanIdLabel = label("spanId", TelemetryLogFormats.spanId(ctxStorage))
val traceIdLabel = label("traceId", TelemetryLogFormats.traceId(ctxStorage))

val myLogFormat = timestamp.fixed(32) |-| level |-| label("message", quoted(line)) |-| spanIdLabel |-| traceIdLabel
val myConsoleLogger = console(myLogFormat)
}
grouzen marked this conversation as resolved.
Show resolved Hide resolved
grouzen marked this conversation as resolved.
Show resolved Hide resolved
```
1 change: 1 addition & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const sidebars = {
"opentracing-example",
"opencensus",
"opentelemetry",
"opentelemetry-zio-logging",
"opentelemetry-example",
"opentelemetry-instrumentation-example"
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package zio.telemetry.opentelemetry.zio.logging

import io.opentelemetry.api.trace.{Span, SpanContext}
import io.opentelemetry.context.Context
import zio.FiberRefs
import zio.logging.LogFormat
import zio.telemetry.opentelemetry.context.ContextStorage

object TelemetryLogFormats {
grouzen marked this conversation as resolved.
Show resolved Hide resolved
grouzen marked this conversation as resolved.
Show resolved Hide resolved

/**
* Will print traceId from current span or nothing when not in span
*/
def traceId(contextStorage: ContextStorage): LogFormat = LogFormat.make { (builder, _, _, _, _, _, fiberRefs, _, _) =>
getSpanContext(contextStorage, fiberRefs).map(_.getTraceId) match {
case Some(traceId) => builder.appendText(traceId)
case None => ()
}
grouzen marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Will print spanId from current span or nothing when not in span
*/
def spanId(contextStorage: ContextStorage): LogFormat = LogFormat.make { (builder, _, _, _, _, _, fiberRefs, _, _) =>
getSpanContext(contextStorage, fiberRefs).map(_.getSpanId) match {
case Some(spanId) => builder.appendText(spanId)
case None => ()
}
}

private def getSpanContext(ctxStorage: ContextStorage, fiberRefs: FiberRefs): Option[SpanContext] =
(ctxStorage match {
case ref: ContextStorage.ZIOFiberRef => fiberRefs.get(ref.ref)
case ContextStorage.Native => Some(Context.current())
})
grouzen marked this conversation as resolved.
Show resolved Hide resolved
.map(Span.fromContext)
.map(_.getSpanContext)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package zio.telemetry.opentelemetry.zio.logging

import io.opentelemetry.api.trace.Tracer
import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter
import io.opentelemetry.sdk.trace.SdkTracerProvider
import io.opentelemetry.sdk.trace.data.SpanData
import io.opentelemetry.sdk.trace.`export`.SimpleSpanProcessor
import zio.Runtime.removeDefaultLoggers
import zio.logging.LogFormat.label
import zio.telemetry.opentelemetry.context.ContextStorage
import zio.telemetry.opentelemetry.tracing.Tracing
import zio.test.{Spec, TestEnvironment, ZIOSpecDefault, assertTrue}
import zio.{Scope, UIO, ULayer, URLayer, ZEnvironment, ZIO, ZLayer}

import scala.collection.mutable
import scala.jdk.CollectionConverters._

object TelemetryLogFormatsSpec extends ZIOSpecDefault {

val inMemoryTracer: UIO[(InMemorySpanExporter, Tracer)] = for {
spanExporter <- ZIO.succeed(InMemorySpanExporter.create())
spanProcessor <- ZIO.succeed(SimpleSpanProcessor.create(spanExporter))
tracerProvider <- ZIO.succeed(SdkTracerProvider.builder().addSpanProcessor(spanProcessor).build())
tracer = tracerProvider.get("TracingTest")
} yield (spanExporter, tracer)

val inMemoryTracerLayer: ULayer[InMemorySpanExporter with Tracer] =
ZLayer.fromZIOEnvironment(inMemoryTracer.map { case (inMemorySpanExporter, tracer) =>
ZEnvironment(inMemorySpanExporter).add(tracer)
})

def tracingMockLayer(
logAnnotated: Boolean = false
): URLayer[ContextStorage, Tracing with InMemorySpanExporter with Tracer] =
inMemoryTracerLayer >>> (Tracing.live(logAnnotated) ++ inMemoryTracerLayer)

def getFinishedSpans: ZIO[InMemorySpanExporter, Nothing, List[SpanData]] =
ZIO.serviceWith[InMemorySpanExporter](_.getFinishedSpanItems.asScala.toList)

override def spec: Spec[TestEnvironment with Scope, Any] =
suiteAll("opentelemetry-zio-logging LogFormats") {
test("SpanId and traceId are extracted") {
ZIO.serviceWithZIO[Tracing] { tracing =>
import tracing.aspects._
val logs = mutable.Buffer[String]()

for {
contextStorage <- ZIO.service[ContextStorage]
grouzen marked this conversation as resolved.
Show resolved Hide resolved
format = label("spanId", TelemetryLogFormats.spanId(contextStorage)) |-| label(
"traceId",
TelemetryLogFormats.traceId(contextStorage)
)
zLogger = format.toLogger.map(logs.append(_))
_ <- zio.ZIO.logInfo("TEST").withLogger(zLogger) @@ span("Span") @@ root("Root")

spans <- getFinishedSpans
child = spans.find(_.getName == "Span").get
log = logs.head
} yield assertTrue(log == s"spanId=${child.getSpanId} traceId=${child.getTraceId}")
}
}
}.provide(removeDefaultLoggers, tracingMockLayer(), ContextStorage.fiberRef)

}
4 changes: 4 additions & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ object Dependencies {
Orgs.scalaLangModules %% "scala-collection-compat" % Versions.scalaCollectionCompat % Test
)

lazy val opentelemetryZioLogging = opentelemetry ++ Seq(
Orgs.zio %% "zio-logging" % ExampleVersions.zioLogging
)

lazy val example = zio ++ Seq(
Orgs.typelevel %% "cats-core" % ExampleVersions.cats,
Orgs.jaegertracing % "jaeger-core" % ExampleVersions.jaeger,
Expand Down
Loading