diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/extras/LogFormatter.scala b/core/shared/src/main/scala/org/typelevel/log4cats/extras/LogFormatter.scala new file mode 100644 index 00000000..1becf5d1 --- /dev/null +++ b/core/shared/src/main/scala/org/typelevel/log4cats/extras/LogFormatter.scala @@ -0,0 +1,92 @@ +/* + * Copyright 2018 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.log4cats.extras + +import java.io.{ByteArrayOutputStream, PrintStream} +import java.time.Instant +import java.time.format.DateTimeFormatter + +trait LogFormatter { + def format(loggerName: String, level: LogLevel, timestamp: Instant, msg: String): String + def format( + loggerName: String, + level: LogLevel, + timestamp: Instant, + msg: String, + throwable: Throwable + ): String + def format( + loggerName: String, + level: LogLevel, + timestamp: Instant, + msg: String, + ctx: Map[String, String] + ): String + def format( + loggerName: String, + level: LogLevel, + timestamp: Instant, + msg: String, + ctx: Map[String, String], + throwable: Throwable + ): String +} +object LogFormatter { + val Default: LogFormatter = new LogFormatter { + private def fmt(i: Instant): String = DateTimeFormatter.ISO_INSTANT.format(i) + private def fmt(ctx: Map[String, String]): String = { + val builder = new StringBuilder() + ctx.toVector.foreach { case (key, value) => + builder.append(" ").append(key).append(" = ").append(value).append("\n") + } + builder.result() + } + private def fmt(t: Throwable): String = { + val baos = new ByteArrayOutputStream() + val ps = new PrintStream(baos) + t.printStackTrace(ps) + baos.toString.linesIterator.map(s => s" $s").mkString("\n") + } + + override def format(n: String, l: LogLevel, ts: Instant, m: String): String = + s"${fmt(ts)} $l $n - $m" + + override def format(n: String, l: LogLevel, ts: Instant, m: String, t: Throwable): String = + s"${fmt(ts)} $l $n - $m\n${fmt(t)}" + + override def format( + n: String, + l: LogLevel, + ts: Instant, + m: String, + ctx: Map[String, String] + ): String = + if (ctx.isEmpty) format(n, l, ts, m) + else s"${fmt(ts)} $l $n - $m\n${fmt(ctx)}" + + override def format( + n: String, + l: LogLevel, + ts: Instant, + m: String, + ctx: Map[String, String], + t: Throwable + ): String = + if (ctx.isEmpty) format(n, l, ts, m, t) + else s"${fmt(ts)} $l $n - $m\n${fmt(ctx)}\t${fmt(t)}" + } +} diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/extras/StdErrLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/extras/StdErrLogger.scala new file mode 100644 index 00000000..e3393cdb --- /dev/null +++ b/core/shared/src/main/scala/org/typelevel/log4cats/extras/StdErrLogger.scala @@ -0,0 +1,300 @@ +/* + * Copyright 2018 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.log4cats.extras + +import cats.effect.kernel.Async +import cats.effect.std.Console +import cats.syntax.all.* +import cats.{~>, Show} +import org.typelevel.log4cats.SelfAwareStructuredLogger + +import java.time.Instant + +/** + * A simple logger that prints logs to standard error output. + * + * The intended use-case is for one-off scripts, examples, and other situations where a more + * fully-featured and performance-optimized logger would be overkill + * + * Log output format is roughly: + * {{{ + * %level %name - %message + * %ctx + * %throwable + * }}} + */ +trait StdErrLogger[F[_]] extends SelfAwareStructuredLogger[F] { + + override def mapK[G[_]](fk: F ~> G): StdErrLogger[G] = + StdErrLogger.mapK(this, fk) + + override def addContext(ctx: Map[String, String]): StdErrLogger[F] = + StdErrLogger.withContext(this)(ctx) + + override def addContext(pairs: (String, Show.Shown)*): StdErrLogger[F] = + StdErrLogger.withContext(this)( + pairs.map { case (k, v) => (k, v.toString) }.toMap + ) + + override def withModifiedString(f: String => String): StdErrLogger[F] = + StdErrLogger.withModifiedString(this, f) +} +object StdErrLogger { + def apply[F[_]: Console: Async](name: String, logLevel: LogLevel): StdErrLogger[F] = + apply[F](name, logLevel, LogFormatter.Default) + + def apply[F[_]](name: String, logLevel: LogLevel, format: LogFormatter)(implicit + console: Console[F], + F: Async[F] + ): StdErrLogger[F] = + new StdErrLogger[F] { + private def isLevelEnabled(level: LogLevel): Boolean = logLevel >= level + + override def isTraceEnabled: F[Boolean] = isLevelEnabled(LogLevel.Trace).pure[F] + + override def isDebugEnabled: F[Boolean] = isLevelEnabled(LogLevel.Debug).pure[F] + + override def isInfoEnabled: F[Boolean] = isLevelEnabled(LogLevel.Info).pure[F] + + override def isWarnEnabled: F[Boolean] = isLevelEnabled(LogLevel.Warn).pure[F] + + override def isErrorEnabled: F[Boolean] = isLevelEnabled(LogLevel.Error).pure[F] + + private def now: F[Instant] = F.realTime.map(d => Instant.EPOCH.plusNanos(d.toNanos)) + + private def log(level: LogLevel, msg: => String): F[Unit] = + now + .map(format.format(name, level, _, msg)) + .flatMap(console.errorln(_)) + .whenA(isLevelEnabled(level)) + + private def log(level: LogLevel, msg: => String, throwable: Throwable): F[Unit] = + now + .map(format.format(name, level, _, msg, throwable)) + .flatMap(console.errorln(_)) + .whenA(isLevelEnabled(level)) + + private def log(level: LogLevel, msg: => String, ctx: Map[String, String]): F[Unit] = + now + .map(format.format(name, level, _, msg, ctx)) + .flatMap(console.errorln(_)) + .whenA(isLevelEnabled(level)) + + private def log( + level: LogLevel, + msg: => String, + throwable: Throwable, + ctx: Map[String, String] + ): F[Unit] = + now + .map(format.format(name, level, _, msg, ctx, throwable)) + .flatMap(console.errorln(_)) + .whenA(isLevelEnabled(level)) + + override def trace(ctx: Map[String, String])(msg: => String): F[Unit] = + log(LogLevel.Trace, msg, ctx) + + override def debug(ctx: Map[String, String])(msg: => String): F[Unit] = + log(LogLevel.Debug, msg, ctx) + override def info(ctx: Map[String, String])(msg: => String): F[Unit] = + log(LogLevel.Info, msg, ctx) + override def warn(ctx: Map[String, String])(msg: => String): F[Unit] = + log(LogLevel.Warn, msg, ctx) + override def error(ctx: Map[String, String])(msg: => String): F[Unit] = + log(LogLevel.Error, msg, ctx) + + override def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + log(LogLevel.Trace, msg, t, ctx) + override def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + log(LogLevel.Debug, msg, t, ctx) + override def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + log(LogLevel.Info, msg, t, ctx) + override def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + log(LogLevel.Warn, msg, t, ctx) + override def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + log(LogLevel.Error, msg, t, ctx) + + override def error(t: Throwable)(message: => String): F[Unit] = + log(LogLevel.Error, message, t) + override def warn(t: Throwable)(message: => String): F[Unit] = log(LogLevel.Warn, message, t) + override def info(t: Throwable)(message: => String): F[Unit] = log(LogLevel.Info, message, t) + override def debug(t: Throwable)(message: => String): F[Unit] = + log(LogLevel.Debug, message, t) + override def trace(t: Throwable)(message: => String): F[Unit] = + log(LogLevel.Trace, message, t) + + override def error(message: => String): F[Unit] = log(LogLevel.Error, message) + override def warn(message: => String): F[Unit] = log(LogLevel.Warn, message) + override def info(message: => String): F[Unit] = log(LogLevel.Info, message) + override def debug(message: => String): F[Unit] = log(LogLevel.Debug, message) + override def trace(message: => String): F[Unit] = log(LogLevel.Trace, message) + } + + private def mapK[F[_], G[_]]( + logger: StdErrLogger[F], + fk: F ~> G + ): StdErrLogger[G] = + new StdErrLogger[G] { + override def isTraceEnabled: G[Boolean] = fk(logger.isTraceEnabled) + override def isDebugEnabled: G[Boolean] = fk(logger.isDebugEnabled) + override def isInfoEnabled: G[Boolean] = fk(logger.isInfoEnabled) + override def isWarnEnabled: G[Boolean] = fk(logger.isWarnEnabled) + override def isErrorEnabled: G[Boolean] = fk(logger.isErrorEnabled) + + override def trace(ctx: Map[String, String], t: Throwable)(msg: => String): G[Unit] = fk( + logger.trace(ctx, t)(msg) + ) + override def debug(ctx: Map[String, String], t: Throwable)(msg: => String): G[Unit] = fk( + logger.debug(ctx, t)(msg) + ) + override def info(ctx: Map[String, String], t: Throwable)(msg: => String): G[Unit] = fk( + logger.info(ctx, t)(msg) + ) + override def warn(ctx: Map[String, String], t: Throwable)(msg: => String): G[Unit] = fk( + logger.warn(ctx, t)(msg) + ) + override def error(ctx: Map[String, String], t: Throwable)(msg: => String): G[Unit] = fk( + logger.error(ctx, t)(msg) + ) + + override def trace(ctx: Map[String, String])(msg: => String): G[Unit] = fk( + logger.trace(ctx)(msg) + ) + override def debug(ctx: Map[String, String])(msg: => String): G[Unit] = fk( + logger.debug(ctx)(msg) + ) + override def info(ctx: Map[String, String])(msg: => String): G[Unit] = fk( + logger.info(ctx)(msg) + ) + override def warn(ctx: Map[String, String])(msg: => String): G[Unit] = fk( + logger.warn(ctx)(msg) + ) + override def error(ctx: Map[String, String])(msg: => String): G[Unit] = fk( + logger.error(ctx)(msg) + ) + + override def trace(t: Throwable)(message: => String): G[Unit] = fk(logger.trace(t)(message)) + override def debug(t: Throwable)(message: => String): G[Unit] = fk(logger.debug(t)(message)) + override def info(t: Throwable)(message: => String): G[Unit] = fk(logger.info(t)(message)) + override def warn(t: Throwable)(message: => String): G[Unit] = fk(logger.warn(t)(message)) + override def error(t: Throwable)(message: => String): G[Unit] = fk(logger.error(t)(message)) + + override def trace(message: => String): G[Unit] = fk(logger.trace(message)) + override def debug(message: => String): G[Unit] = fk(logger.debug(message)) + override def info(message: => String): G[Unit] = fk(logger.info(message)) + override def warn(message: => String): G[Unit] = fk(logger.warn(message)) + override def error(message: => String): G[Unit] = fk(logger.error(message)) + } + + def withContext[F[_]]( + logger: StdErrLogger[F] + )(baseCtx: Map[String, String]): StdErrLogger[F] = + new StdErrLogger[F] { + private def addCtx(ctx: Map[String, String]): Map[String, String] = baseCtx ++ ctx + + override def isTraceEnabled: F[Boolean] = logger.isTraceEnabled + override def isDebugEnabled: F[Boolean] = logger.isDebugEnabled + override def isInfoEnabled: F[Boolean] = logger.isInfoEnabled + override def isWarnEnabled: F[Boolean] = logger.isWarnEnabled + override def isErrorEnabled: F[Boolean] = logger.isErrorEnabled + + override def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + logger.trace(addCtx(ctx), t)(msg) + override def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + logger.debug(addCtx(ctx), t)(msg) + override def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + logger.info(addCtx(ctx), t)(msg) + override def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + logger.warn(addCtx(ctx), t)(msg) + override def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + logger.error(addCtx(ctx), t)(msg) + + override def trace(ctx: Map[String, String])(msg: => String): F[Unit] = + logger.trace(addCtx(ctx))(msg) + override def debug(ctx: Map[String, String])(msg: => String): F[Unit] = + logger.debug(addCtx(ctx))(msg) + override def info(ctx: Map[String, String])(msg: => String): F[Unit] = + logger.info(addCtx(ctx))(msg) + override def warn(ctx: Map[String, String])(msg: => String): F[Unit] = + logger.warn(addCtx(ctx))(msg) + override def error(ctx: Map[String, String])(msg: => String): F[Unit] = + logger.error(addCtx(ctx))(msg) + + override def trace(t: Throwable)(message: => String): F[Unit] = + logger.trace(baseCtx, t)(message) + override def debug(t: Throwable)(message: => String): F[Unit] = + logger.debug(baseCtx, t)(message) + override def info(t: Throwable)(message: => String): F[Unit] = + logger.info(baseCtx, t)(message) + override def warn(t: Throwable)(message: => String): F[Unit] = + logger.warn(baseCtx, t)(message) + override def error(t: Throwable)(message: => String): F[Unit] = + logger.error(baseCtx, t)(message) + + override def trace(message: => String): F[Unit] = logger.trace(baseCtx)(message) + override def debug(message: => String): F[Unit] = logger.debug(baseCtx)(message) + override def info(message: => String): F[Unit] = logger.info(baseCtx)(message) + override def warn(message: => String): F[Unit] = logger.warn(baseCtx)(message) + override def error(message: => String): F[Unit] = logger.error(baseCtx)(message) + } + + def withModifiedString[F[_]]( + logger: StdErrLogger[F], + f: String => String + ): StdErrLogger[F] = + new StdErrLogger[F] { + override def isTraceEnabled: F[Boolean] = logger.isTraceEnabled + override def isDebugEnabled: F[Boolean] = logger.isDebugEnabled + override def isInfoEnabled: F[Boolean] = logger.isInfoEnabled + override def isWarnEnabled: F[Boolean] = logger.isWarnEnabled + override def isErrorEnabled: F[Boolean] = logger.isErrorEnabled + + override def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + logger.trace(ctx, t)(f(msg)) + override def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + logger.debug(ctx, t)(f(msg)) + override def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + logger.info(ctx, t)(f(msg)) + override def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + logger.warn(ctx, t)(f(msg)) + override def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + logger.error(ctx, t)(f(msg)) + + override def trace(ctx: Map[String, String])(msg: => String): F[Unit] = + logger.trace(ctx)(f(msg)) + override def debug(ctx: Map[String, String])(msg: => String): F[Unit] = + logger.debug(ctx)(f(msg)) + override def info(ctx: Map[String, String])(msg: => String): F[Unit] = + logger.info(ctx)(f(msg)) + override def warn(ctx: Map[String, String])(msg: => String): F[Unit] = + logger.warn(ctx)(f(msg)) + override def error(ctx: Map[String, String])(msg: => String): F[Unit] = + logger.error(ctx)(f(msg)) + + override def trace(t: Throwable)(message: => String): F[Unit] = logger.trace(t)(f(message)) + override def debug(t: Throwable)(message: => String): F[Unit] = logger.debug(t)(f(message)) + override def info(t: Throwable)(message: => String): F[Unit] = logger.info(t)(f(message)) + override def warn(t: Throwable)(message: => String): F[Unit] = logger.warn(t)(f(message)) + override def error(t: Throwable)(message: => String): F[Unit] = logger.error(t)(f(message)) + + override def trace(message: => String): F[Unit] = logger.trace(f(message)) + override def debug(message: => String): F[Unit] = logger.debug(f(message)) + override def info(message: => String): F[Unit] = logger.info(f(message)) + override def warn(message: => String): F[Unit] = logger.warn(f(message)) + override def error(message: => String): F[Unit] = logger.error(f(message)) + } +} diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/extras/StdErrLoggerFactory.scala b/core/shared/src/main/scala/org/typelevel/log4cats/extras/StdErrLoggerFactory.scala new file mode 100644 index 00000000..eb884df9 --- /dev/null +++ b/core/shared/src/main/scala/org/typelevel/log4cats/extras/StdErrLoggerFactory.scala @@ -0,0 +1,204 @@ +/* + * Copyright 2018 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.log4cats.extras + +import cats.Show.Shown +import cats.data.Chain +import cats.effect.kernel.{Async, Ref, Resource} +import cats.effect.std.{Console, Dispatcher} +import cats.syntax.all.* +import cats.{~>, Functor} +import org.typelevel.log4cats.{LoggerFactory, SelfAwareStructuredLogger} + +import scala.concurrent.Await +import scala.concurrent.duration.Duration + +/** + * Creates simple loggers that print logs to standard error output. + * + * The intended use-case is for one-off scripts, examples, and other situations where a more + * fully-featured and performance-optimized logger would be overkill + */ +trait StdErrLoggerFactory[F[_]] extends LoggerFactory[F] { + override def getLoggerFromName(name: String): SelfAwareStructuredLogger[F] + + /** + * Set the initial log level for loggers that will be created. + * + * @param logLevel + * Sets the log level such that logs at or above this level will be logged. + * @note + * This does not propagate to loggers which have already been created. + */ + def setGlobalLogLevel(logLevel: LogLevel): F[Unit] + + /** + * Add an override for loggers that will be created which have a matching prefix. + * + * @param prefix + * Similar to how `name` works in a `log4j` config file, this is a simple prefix match. + * @note + * This does not propagate to loggers which have already been created. + * @note + * These are applied on a first-match basis: + * {{{ + * logger.addLogLevelOverride("", LogLevel.Warn) *> + * logger.addLogLevelOverride("foo", LogLevel.Info) // This is effectively dead code + * }}} + */ + def addLogLevelOverride(prefix: String, logLevel: LogLevel): F[Unit] + + /** + * Remove an existing log level override. + * + * @param prefix + * This must match exactly, if overrides for `foo.bar` and `foo` have been added, then removing + * `foo` will not remove the override for `foo.bar` + * @note + * This does not propagate to loggers which have already been created. + */ + def removeLogLevelOverride(prefix: String): F[Unit] + + override def addContext(ctx: Map[String, String])(implicit + F: Functor[F] + ): StdErrLoggerFactory[F] = + StdErrLoggerFactory.addContext(this, ctx) + + override def addContext(pairs: (String, Shown)*)(implicit + F: Functor[F] + ): StdErrLoggerFactory[F] = + StdErrLoggerFactory.addContext(this, pairs.map { case (k, v) => (k, v.toString) }.toMap) + + override def withModifiedString(f: String => String)(implicit + F: Functor[F] + ): StdErrLoggerFactory[F] = + StdErrLoggerFactory.withModifiedString(this, f) + + override def mapK[G[_]](fk: F ~> G)(implicit F: Functor[F]): StdErrLoggerFactory[G] = + StdErrLoggerFactory.mapK[F, G](fk)(this) +} +object StdErrLoggerFactory { + + def apply[F[_]: Async: Console]( + defaultLogLevel: LogLevel, + logLevelOverrides: (String, LogLevel)* + ): Resource[F, StdErrLoggerFactory[F]] = + apply[F](defaultLogLevel, LogFormatter.Default, logLevelOverrides*) + + def apply[F[_]: Async: Console]( + defaultLogLevel: LogLevel, + format: LogFormatter, + logLevelOverrides: (String, LogLevel)* + ): Resource[F, StdErrLoggerFactory[F]] = + Dispatcher.sequential[F].evalMap { dispatcher => + ( + Ref[F].of(defaultLogLevel), + Ref[F].of(Chain.fromSeq(logLevelOverrides.map((new LogLevelOverride(_, _)).tupled))) + ).mapN { (globalLogLevelRef, logLevelOverrides) => + new StdErrLoggerFactory[F] { + override def setGlobalLogLevel(logLevel: LogLevel): F[Unit] = + globalLogLevelRef.set(logLevel) + + override def addLogLevelOverride(prefix: String, logLevel: LogLevel): F[Unit] = + logLevelOverrides.update(_.append(new LogLevelOverride(prefix, logLevel))) + + override def removeLogLevelOverride(prefix: String): F[Unit] = + logLevelOverrides.update(_.filterNot(_.prefix == prefix)) + + override def fromName(name: String): F[SelfAwareStructuredLogger[F]] = + (globalLogLevelRef.get, logLevelOverrides.get).mapN { + (globalLogLevel, logLevelOverrides) => + val logLevel = + logLevelOverrides.find(_.matches(name)).fold(globalLogLevel)(_.logLevel) + StdErrLogger[F](name, logLevel, format) + }.widen + + override def getLoggerFromName(name: String): SelfAwareStructuredLogger[F] = + // Note: switch to dispatcher.unsafeRunSync when 2.12 support is dropped + Await.result(dispatcher.unsafeToFuture(fromName(name)), Duration.Inf) + } + } + } + + private class LogLevelOverride(val prefix: String, val logLevel: LogLevel) { + def matches(name: String): Boolean = name.startsWith(prefix) + } + + private def mapK[F[_]: Functor, G[_]]( + fk: F ~> G + )(lf: StdErrLoggerFactory[F]): StdErrLoggerFactory[G] = + new StdErrLoggerFactory[G] { + override def setGlobalLogLevel(logLevel: LogLevel): G[Unit] = fk( + lf.setGlobalLogLevel(logLevel) + ) + + override def addLogLevelOverride(prefix: String, logLevel: LogLevel): G[Unit] = + fk(lf.addLogLevelOverride(prefix, logLevel)) + + override def removeLogLevelOverride(prefix: String): G[Unit] = fk( + lf.removeLogLevelOverride(prefix) + ) + + override def getLoggerFromName(name: String): SelfAwareStructuredLogger[G] = + lf.getLoggerFromName(name).mapK(fk) + + override def fromName(name: String): G[SelfAwareStructuredLogger[G]] = fk( + lf.fromName(name).map(_.mapK(fk)) + ) + } + + private def addContext[F[_]: Functor]( + lf: StdErrLoggerFactory[F], + ctx: Map[String, String] + ): StdErrLoggerFactory[F] = + new StdErrLoggerFactory[F] { + override def setGlobalLogLevel(logLevel: LogLevel): F[Unit] = lf.setGlobalLogLevel(logLevel) + + override def addLogLevelOverride(prefix: String, logLevel: LogLevel): F[Unit] = + lf.addLogLevelOverride(prefix, logLevel) + + override def removeLogLevelOverride(prefix: String): F[Unit] = + lf.removeLogLevelOverride(prefix) + + override def getLoggerFromName(name: String): SelfAwareStructuredLogger[F] = + lf.getLoggerFromName(name).addContext(ctx) + + override def fromName(name: String): F[SelfAwareStructuredLogger[F]] = + lf.fromName(name).map(_.addContext(ctx)) + } + + private def withModifiedString[F[_]: Functor]( + lf: StdErrLoggerFactory[F], + f: String => String + ): StdErrLoggerFactory[F] = + new StdErrLoggerFactory[F] { + override def setGlobalLogLevel(logLevel: LogLevel): F[Unit] = lf.setGlobalLogLevel(logLevel) + + override def addLogLevelOverride(prefix: String, logLevel: LogLevel): F[Unit] = + lf.addLogLevelOverride(prefix, logLevel) + + override def removeLogLevelOverride(prefix: String): F[Unit] = + lf.removeLogLevelOverride(prefix) + + override def getLoggerFromName(name: String): SelfAwareStructuredLogger[F] = + lf.getLoggerFromName(name).withModifiedString(f) + + override def fromName(name: String): F[SelfAwareStructuredLogger[F]] = + lf.fromName(name).map(_.withModifiedString(f)) + } + +}