Skip to content

Instantly share code, notes, and snippets.

@iRevive
Created September 16, 2025 08:03
Show Gist options
  • Save iRevive/8e82a7df34af15c4bec10cfe03ab5130 to your computer and use it in GitHub Desktop.
Save iRevive/8e82a7df34af15c4bec10cfe03ab5130 to your computer and use it in GitHub Desktop.
otel4s + log4cats example
import java.io.{PrintWriter, StringWriter}
import cats.Functor
import cats.effect.{Async, IO, IOApp, Resource}
import cats.syntax.all._
import cats.effect.std.Dispatcher
import org.typelevel.log4cats.{LoggerFactory, SelfAwareStructuredLogger}
import org.typelevel.otel4s.{AnyValue, Attribute, Attributes}
import org.typelevel.otel4s.logs.{Logger, LoggerProvider, Severity}
import org.typelevel.otel4s.sdk.OpenTelemetrySdk
import org.typelevel.otel4s.semconv.attributes.ExceptionAttributes
import org.typelevel.otel4s.sdk.exporter.otlp.autoconfigure.OtlpExportersAutoConfigure
object Main extends IOApp.Simple {
def run: IO[Unit] =
OpenTelemetrySdk
.autoConfigured[IO](_.addExportersConfigurer(OtlpExportersAutoConfigure[IO]))
.flatMap(otel4s => Otel4sLoggerFactory.create(otel4s.sdk.loggerProvider).tupleLeft(otel4s))
.use { case (otel4s, loggerFactory) =>
for {
logger <- loggerFactory.fromName("service.logger")
_ <- logger.info("my first log message")
} yield ()
}
}
private final class Otel4sLoggerFactory[F[_]: Functor, Ctx](lp: LoggerProvider[F, Ctx], dispatcher: Dispatcher[F])
extends LoggerFactory[F] {
def getLoggerFromName(name: String): SelfAwareStructuredLogger[F] =
dispatcher.unsafeRunSync(fromName(name))
def fromName(name: String): F[SelfAwareStructuredLogger[F]] =
lp.get(name).map(new Otel4sLogger[F, Ctx](_))
}
object Otel4sLoggerFactory {
def create[F[_]: Async, Ctx](loggerProvider: LoggerProvider[F, Ctx]): Resource[F, LoggerFactory[F]] =
for {
dispatcher <- Dispatcher.sequential[F]
} yield new Otel4sLoggerFactory[F, Ctx](loggerProvider, dispatcher)
}
private final class Otel4sLogger[F[_], Ctx](logger: Logger[F, Ctx]) extends SelfAwareStructuredLogger[F] {
def isTraceEnabled: F[Boolean] = logger.meta.isEnabled // todo: must be managed by the log4cats layer
def isDebugEnabled: F[Boolean] = logger.meta.isEnabled // todo: must be managed by the log4cats layer
def isInfoEnabled: F[Boolean] = logger.meta.isEnabled // todo: must be managed by the log4cats layer
def isWarnEnabled: F[Boolean] = logger.meta.isEnabled // todo: must be managed by the log4cats layer
def isErrorEnabled: F[Boolean] = logger.meta.isEnabled // todo: must be managed by the log4cats layer
def trace(ctx: Map[String, String])(msg: => String): F[Unit] = log(Severity.trace, ctx, None, msg)
def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = log(Severity.trace, ctx, Some(t), msg)
def debug(ctx: Map[String, String])(msg: => String): F[Unit] = log(Severity.debug, ctx, None, msg)
def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = log(Severity.debug, ctx, Some(t), msg)
def info(ctx: Map[String, String])(msg: => String): F[Unit] = log(Severity.info, ctx, None, msg)
def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = log(Severity.info, ctx, Some(t), msg)
def warn(ctx: Map[String, String])(msg: => String): F[Unit] = log(Severity.warn, ctx, None, msg)
def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = log(Severity.warn, ctx, Some(t), msg)
def error(ctx: Map[String, String])(msg: => String): F[Unit] = log(Severity.error, ctx, None, msg)
def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = log(Severity.error, ctx, Some(t), msg)
def error(t: Throwable)(message: => String): F[Unit] = log(Severity.error, Map.empty, Some(t), message)
def warn(t: Throwable)(message: => String): F[Unit] = log(Severity.warn, Map.empty, Some(t), message)
def info(t: Throwable)(message: => String): F[Unit] = log(Severity.info, Map.empty, Some(t), message)
def debug(t: Throwable)(message: => String): F[Unit] = log(Severity.debug, Map.empty, Some(t), message)
def trace(t: Throwable)(message: => String): F[Unit] = log(Severity.trace, Map.empty, Some(t), message)
def error(message: => String): F[Unit] = log(Severity.error, Map.empty, None, message)
def warn(message: => String): F[Unit] = log(Severity.warn, Map.empty, None, message)
def info(message: => String): F[Unit] = log(Severity.info, Map.empty, None, message)
def debug(message: => String): F[Unit] = log(Severity.debug, Map.empty, None, message)
def trace(message: => String): F[Unit] = log(Severity.trace, Map.empty, None, message)
private def log(severity: Severity, ctx: Map[String, String], error: Option[Throwable], msg: => String): F[Unit] =
logger.logRecordBuilder
.withSeverity(severity)
.withBody(AnyValue.string(msg))
.addAttributes(ctx.map { case (key, value) => Attribute(key, value) })
.addAttributes(error.map(exceptionAttributes).getOrElse(Attributes.empty))
.emit
private def exceptionAttributes(exception: Throwable) = {
val builder = Attributes.newBuilder
builder.addOne(
ExceptionAttributes.ExceptionType,
exception.getClass.getName
)
val message = exception.getMessage
if (message != null) {
builder.addOne(ExceptionAttributes.ExceptionMessage, message)
}
if (exception.getStackTrace.nonEmpty) {
val stringWriter = new StringWriter()
val printWriter = new PrintWriter(stringWriter)
exception.printStackTrace(printWriter)
builder.addOne(
ExceptionAttributes.ExceptionStacktrace,
stringWriter.toString
)
}
builder.result()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment