Skip to content

Instantly share code, notes, and snippets.

@iRevive
Created March 10, 2026 06:51
Show Gist options
  • Select an option

  • Save iRevive/27333f48cbfc560c16c41287c8a10348 to your computer and use it in GitHub Desktop.

Select an option

Save iRevive/27333f48cbfc560c16c41287c8a10348 to your computer and use it in GitHub Desktop.
log4cats + otel4s
package org.typelevel.log4cats.otel4s
import cats.effect.{IO, IOApp}
import cats.syntax.all._
import org.typelevel.otel4s.oteljava.OtelJava
import org.typelevel.otel4s.oteljava.context.Context
object Main extends IOApp.Simple {
def run: IO[Unit] =
OtelJava
.autoConfigured[IO]()
.flatMap { otelJava =>
implicit val LP = otelJava.loggerProvider
implicit val A = otelJava.localContext
Otel4sLoggerKernel.factory[IO, Context].tupleLeft(otelJava)
}
.use { case (otel4s, factory) =>
val logger = factory.getLoggerFromClass(getClass)
otel4s.tracerProvider.get("tracer").flatMap { tracer =>
tracer.spanBuilder("test-span").build.surround {
logger.error(new RuntimeException("Oops, something went wrong"))(
"something went wrong"
)
}
}
}
}
package org.typelevel.log4cats.otel4s
import cats.Monad
import cats.effect.Async
import cats.effect.kernel.Resource
import cats.effect.std.Dispatcher
import cats.mtl.Ask
import cats.syntax.all._
import org.typelevel.log4cats._
import org.typelevel.otel4s.logs.{LoggerProvider, Severity, Logger => Otel4sLogger}
import org.typelevel.otel4s.semconv.attributes.CodeAttributes
import org.typelevel.otel4s.{AnyValue, Attribute, Attributes}
import scala.util.chaining._
private class Otel4sLoggerKernel[F[_], Otel4sCtx, LogCtx](
underlying: Otel4sLogger[F, Otel4sCtx]
)(implicit
F: Monad[F],
M: Attributes.Make[Map[String, LogCtx]],
A: Ask[F, Otel4sCtx]
) extends LoggerKernel[F, LogCtx] {
def log(
level: KernelLogLevel,
record: Log.Builder[LogCtx] => Log.Builder[LogCtx]
): F[Unit] = {
val logRecord = record(Log.mutableBuilder[LogCtx]()).build()
val severity = level match {
case KernelLogLevel.Trace => Severity.trace
case KernelLogLevel.Debug => Severity.debug
case KernelLogLevel.Info => Severity.info
case KernelLogLevel.Warn => Severity.warn
case KernelLogLevel.Error => Severity.error
case KernelLogLevel.Fatal => Severity.fatal
}
val attributes = Attributes.from(logRecord.context)
A.ask
.flatMap(ctx => underlying.meta.isEnabled(ctx, Some(severity), None))
.flatMap {
case true =>
underlying.logRecordBuilder
.withSeverity(severity)
.withSeverityText(level.name)
.withBody(AnyValue.string(logRecord.message()))
.pipe(b => logRecord.timestamp.fold(b)(t => b.withTimestamp(t)))
.pipe(b => logRecord.throwable.fold(b)(t => b.withException(t)))
.pipe(b =>
logRecord.className.fold(b)(c => b.addAttribute(Attribute("code.class.name", c)))
)
.pipe(b =>
logRecord.fileName.fold(b)(f => b.addAttribute(CodeAttributes.CodeFilePath(f)))
)
.pipe(b =>
logRecord.methodName.fold(b)(f => b.addAttribute(CodeAttributes.CodeFunctionName(f)))
)
.pipe(b =>
logRecord.line.fold(b)(f => b.addAttribute(CodeAttributes.CodeLineNumber(f.toLong)))
)
.addAttributes(attributes)
.emit
case false =>
F.unit
}
}
}
object Otel4sLoggerKernel {
def factory[F[_], Otel4sCtx](implicit
F: Async[F],
LP: LoggerProvider[F, Otel4sCtx],
A: Ask[F, Otel4sCtx]
): Resource[F, LoggerFactoryGen[F]] =
for {
dispatcher <- Dispatcher.sequential[F](await = false)
} yield new Factory(dispatcher, LP)
class Factory[F[_]: Monad, Otel4sCtx](
dispatcher: Dispatcher[F],
provider: LoggerProvider[F, Otel4sCtx]
)(implicit A: Ask[F, Otel4sCtx])
extends LoggerFactoryGen[F] {
type LoggerType = Logger[F]
private implicit val make: Attributes.Make[Map[String, String]] =
map => Attributes.fromSpecific(map.map { case (k, v) => Attribute(k, v) })
def getLoggerFromName(name: String): Logger[F] =
dispatcher.unsafeRunSync(fromName(name))
def fromName(name: String): F[Logger[F]] =
for {
logger <- provider.get(name)
} yield Logger.wrap(new Otel4sLoggerKernel[F, Otel4sCtx, String](logger))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment