Merge pull request #1535 from eikek/logging-improvements

Improve logging configuration
This commit is contained in:
mergify[bot]
2022-04-30 16:43:03 +00:00
committed by GitHub
12 changed files with 91 additions and 51 deletions

View File

@ -27,9 +27,7 @@ class MigrationTest
PostgreSQLContainer.Def(DockerImageName.parse("postgres:14")) PostgreSQLContainer.Def(DockerImageName.parse("postgres:14"))
override def docspellLogConfig: LogConfig = override def docspellLogConfig: LogConfig =
LogConfig(Level.Debug, LogConfig.Format.Fancy) super.docspellLogConfig.docspellLevel(Level.Debug)
override def rootMinimumLevel = Level.Warn
test("create schema") { test("create schema") {
withContainers { cnt => withContainers { cnt =>

View File

@ -31,9 +31,7 @@ class PsqlFtsClientTest
private val table = FtsRepository.table private val table = FtsRepository.table
override def docspellLogConfig: LogConfig = override def docspellLogConfig: LogConfig =
LogConfig(Level.Debug, LogConfig.Format.Fancy) super.docspellLogConfig.docspellLevel(Level.Debug)
override def rootMinimumLevel = Level.Warn
test("insert data into index") { test("insert data into index") {
withContainers { cnt => withContainers { cnt =>

View File

@ -26,7 +26,15 @@ docspell.joex {
# The minimum level to log. From lowest to highest: # The minimum level to log. From lowest to highest:
# Trace, Debug, Info, Warn, Error # Trace, Debug, Info, Warn, Error
minimum-level = "Info" minimum-level = "Warn"
# Override the log level of specific loggers
levels = {
"docspell" = "Info"
"org.flywaydb" = "Info"
"binny" = "Info"
"org.http4s" = "Info"
}
} }
# The database connection. # The database connection.

View File

@ -10,10 +10,26 @@ import cats.data.NonEmptyList
import io.circe.{Decoder, Encoder} import io.circe.{Decoder, Encoder}
final case class LogConfig(minimumLevel: Level, format: LogConfig.Format) {} final case class LogConfig(
minimumLevel: Level,
format: LogConfig.Format,
levels: LogConfig.ExtraLevels
) {
def clearLevels: LogConfig =
copy(levels = Map.empty)
def withLevel(logger: String, level: Level): LogConfig =
copy(levels = levels.updated(logger, level))
def docspellLevel(level: Level): LogConfig =
withLevel("docspell", level)
}
object LogConfig { object LogConfig {
type ExtraLevels = Map[String, Level]
sealed trait Format { self: Product => sealed trait Format { self: Product =>
def name: String = def name: String =
productPrefix.toLowerCase productPrefix.toLowerCase
@ -38,8 +54,10 @@ object LogConfig {
} }
implicit val jsonDecoder: Decoder[LogConfig] = implicit val jsonDecoder: Decoder[LogConfig] =
Decoder.forProduct2("minimumLevel", "format")(LogConfig.apply) Decoder.forProduct3("minimumLevel", "format", "levels")(LogConfig.apply)
implicit val jsonEncoder: Encoder[LogConfig] = implicit val jsonEncoder: Encoder[LogConfig] =
Encoder.forProduct2("minimumLevel", "format")(r => (r.minimumLevel, r.format)) Encoder.forProduct3("minimumLevel", "format", "levels")(r =>
(r.minimumLevel, r.format, r.levels)
)
} }

View File

@ -26,6 +26,9 @@ final case class LogEvent(
def data[A: Encoder](key: String, value: => A): LogEvent = def data[A: Encoder](key: String, value: => A): LogEvent =
copy(data = data.updated(key, () => Encoder[A].apply(value))) copy(data = data.updated(key, () => Encoder[A].apply(value)))
def addData(m: Map[String, Json]): LogEvent =
copy(data = data ++ m.view.mapValues(json => () => json).toMap)
def addMessage(msg: => String): LogEvent = def addMessage(msg: => String): LogEvent =
copy(additional = (() => Left(msg)) :: additional) copy(additional = (() => Left(msg)) :: additional)

View File

@ -16,40 +16,36 @@ import scribe.jul.JULHandler
import scribe.writer.SystemOutWriter import scribe.writer.SystemOutWriter
object ScribeConfigure { object ScribeConfigure {
private[this] val docspellRootVerbose = "DOCSPELL_ROOT_LOGGER_LEVEL"
def configure[F[_]: Sync](cfg: LogConfig): F[Unit] = def configure[F[_]: Sync](cfg: LogConfig): F[Unit] =
Sync[F].delay { Sync[F].delay {
replaceJUL() replaceJUL()
val docspellLogger = scribe.Logger("docspell") unsafeConfigure(cfg)
unsafeConfigure(scribe.Logger.root, cfg.copy(minimumLevel = getRootMinimumLevel))
unsafeConfigure(docspellLogger, cfg)
unsafeConfigure(scribe.Logger("org.flywaydb"), cfg)
unsafeConfigure(scribe.Logger("binny"), cfg)
unsafeConfigure(scribe.Logger("org.http4s"), cfg)
} }
private[this] def getRootMinimumLevel: Level = def unsafeConfigure(cfg: LogConfig): Unit = {
Option(System.getenv(docspellRootVerbose)) unsafeConfigure(scribe.Logger.root, cfg.format, cfg.minimumLevel)
.map(Level.fromString) cfg.levels.foreach { case (name, level) =>
.flatMap { unsafeConfigure(scribe.Logger(name), cfg.format, level)
case Right(level) => Some(level) }
case Left(err) =>
scribe.warn(
s"Environment variable '$docspellRootVerbose' has invalid value: $err"
)
None
} }
.getOrElse(Level.Error)
def unsafeConfigure(logger: scribe.Logger, cfg: LogConfig): Unit = { def unsafeConfigure(logger: String, cfg: LogConfig): Unit = {
val log = scribe.Logger(logger)
val level = cfg.levels.getOrElse(logger, cfg.minimumLevel)
unsafeConfigure(log, cfg.format, level)
}
def unsafeConfigure(
logger: scribe.Logger,
format: LogConfig.Format,
level: Level
): Unit = {
val mods = List[scribe.Logger => scribe.Logger]( val mods = List[scribe.Logger => scribe.Logger](
_.clearHandlers(), _.clearHandlers(),
_.withMinimumLevel(ScribeWrapper.convertLevel(cfg.minimumLevel)), _.withMinimumLevel(ScribeWrapper.convertLevel(level)),
l => l =>
if (logger.id == scribe.Logger.RootId) { if (logger.id == scribe.Logger.RootId) {
cfg.format match { format match {
case Format.Fancy => case Format.Fancy =>
l.withHandler(formatter = Formatter.enhanced, writer = SystemOutWriter) l.withHandler(formatter = Formatter.enhanced, writer = SystemOutWriter)
case Format.Plain => case Format.Plain =>

View File

@ -11,16 +11,13 @@ import docspell.logging.impl.ScribeConfigure
import munit.Suite import munit.Suite
trait TestLoggingConfig extends Suite { trait TestLoggingConfig extends Suite {
def docspellLogConfig: LogConfig = LogConfig(Level.Warn, LogConfig.Format.Fancy) def docspellLogConfig: LogConfig =
LogConfig(rootMinimumLevel, LogConfig.Format.Fancy, Map.empty)
def rootMinimumLevel: Level = Level.Error def rootMinimumLevel: Level = Level.Error
override def beforeAll(): Unit = { override def beforeAll(): Unit = {
super.beforeAll() super.beforeAll()
val docspellLogger = scribe.Logger("docspell") ScribeConfigure.unsafeConfigure(docspellLogConfig)
ScribeConfigure.unsafeConfigure(docspellLogger, docspellLogConfig)
val rootCfg = docspellLogConfig.copy(minimumLevel = rootMinimumLevel)
ScribeConfigure.unsafeConfigure(scribe.Logger.root, rootCfg)
()
} }
} }

View File

@ -29,7 +29,15 @@ docspell.server {
# The minimum level to log. From lowest to highest: # The minimum level to log. From lowest to highest:
# Trace, Debug, Info, Warn, Error # Trace, Debug, Info, Warn, Error
minimum-level = "Info" minimum-level = "Warn"
# Override the log level of specific loggers
levels = {
"docspell" = "Info"
"org.flywaydb" = "Info"
"binny" = "Info"
"org.http4s" = "Info"
}
} }
# Where the server binds to. # Where the server binds to.

View File

@ -11,6 +11,8 @@ import cats.implicits._
import docspell.common._ import docspell.common._
import io.circe.Json
case class LogEvent( case class LogEvent(
jobId: Ident, jobId: Ident,
taskName: Ident, taskName: Ident,
@ -19,7 +21,8 @@ case class LogEvent(
time: Timestamp, time: Timestamp,
level: LogLevel, level: LogLevel,
msg: String, msg: String,
ex: Option[Throwable] = None ex: Option[Throwable],
data: Map[String, Json]
) { ) {
def logLine: String = def logLine: String =
@ -35,10 +38,11 @@ object LogEvent {
group: Ident, group: Ident,
jobInfo: String, jobInfo: String,
level: LogLevel, level: LogLevel,
msg: String msg: String,
data: Map[String, Json]
): F[LogEvent] = ): F[LogEvent] =
Timestamp Timestamp
.current[F] .current[F]
.map(now => LogEvent(jobId, taskName, group, jobInfo, now, level, msg)) .map(now => LogEvent(jobId, taskName, group, jobInfo, now, level, msg, None, data))
} }

View File

@ -29,12 +29,13 @@ object LogSink {
} }
def logInternal[F[_]: Sync](e: LogEvent): F[Unit] = { def logInternal[F[_]: Sync](e: LogEvent): F[Unit] = {
val logger = docspell.logging.getLogger[F] val logger = docspell.logging.getLogger[F](e.taskName.id)
val addData: logging.LogEvent => logging.LogEvent = val addData: logging.LogEvent => logging.LogEvent =
_.data("jobId", e.jobId) _.data("jobId", e.jobId)
.data("task", e.taskName) .data("task", e.taskName)
.data("group", e.group) .data("group", e.group)
.data("jobInfo", e.jobInfo) .data("jobInfo", e.jobInfo)
.addData(e.data)
e.level match { e.level match {
case LogLevel.Info => case LogLevel.Info =>

View File

@ -15,6 +15,9 @@ import docspell.common.{Ident, LogLevel}
import docspell.logging import docspell.logging
import docspell.logging.{Level, Logger} import docspell.logging.{Level, Logger}
/** Background tasks use this logger to emit the log events to a queue. The consumer is
* [[LogSink]], which picks up log events in a separate thread.
*/
object QueueLogger { object QueueLogger {
def create[F[_]: Sync]( def create[F[_]: Sync](
@ -34,7 +37,8 @@ object QueueLogger {
group, group,
jobInfo, jobInfo,
level2Level(logEvent.level), level2Level(logEvent.level),
logEvent.msg() logEvent.msg(),
logEvent.data.view.mapValues(f => f()).toMap
) )
.flatMap { ev => .flatMap { ev =>
val event = val event =

View File

@ -158,9 +158,9 @@ Please have a look at the corresponding
# Logging # Logging
By default, docspell logs to stdout. This works well, when managed by Docspell logs to stdout. This works well, when managed by systemd or
systemd or other inits. Logging can be configured in the configuration similar tools. Logging can be configured in the configuration file or
file or via environment variables. There are only two settings: via environment variables. There are these settings:
- `minimum-level` specifies the log level to control the verbosity. - `minimum-level` specifies the log level to control the verbosity.
Levels are ordered from: *Trace*, *Debug*, *Info*, *Warn* and Levels are ordered from: *Trace*, *Debug*, *Info*, *Warn* and
@ -169,8 +169,13 @@ file or via environment variables. There are only two settings:
formats for humans: *Plain* and *Fancy*. And two more suited for formats for humans: *Plain* and *Fancy*. And two more suited for
machine consumption: *Json* and *Logfmt*. The *Json* format contains machine consumption: *Json* and *Logfmt*. The *Json* format contains
all details, while the others may omit some for readability all details, while the others may omit some for readability
- `levels` optional _logger name_ to _level_ mappings to override the
log level for specific loggers. If not mentioned here, everything is
logged with `minimum-level`.
These settings are the same for joex and the restserver component. The default settings for joex and restserver are the same and
configure a minimum level of `Warn` and override some loggers with
`Info`.
# Default Config # Default Config
## Rest Server ## Rest Server