diff --git a/build.sbt b/build.sbt index baff161a..4f99fcdf 100644 --- a/build.sbt +++ b/build.sbt @@ -9,9 +9,6 @@ val elmCompileMode = settingKey[ElmCompileMode]("How to compile elm sources") // --- Settings -def inTest(d0: Seq[ModuleID], ds: Seq[ModuleID]*) = - ds.fold(d0)(_ ++ _).map(_ % Test) - val scalafixSettings = Seq( semanticdbEnabled := true, // enable SemanticDB semanticdbVersion := scalafixSemanticdb.revision, // "4.4.0" @@ -58,14 +55,10 @@ val sharedSettings = Seq( libraryDependencySchemes ++= Seq( "com.github.eikek" %% "calev-core" % VersionScheme.Always, "com.github.eikek" %% "calev-circe" % VersionScheme.Always - ) + ), + addCompilerPlugin(Dependencies.kindProjectorPlugin) ) ++ scalafixSettings -val testSettingsMUnit = Seq( - libraryDependencies ++= inTest(Dependencies.munit, Dependencies.logging), - testFrameworks += new TestFramework("munit.Framework") -) - lazy val noPublish = Seq( publish := {}, publishLocal := {}, @@ -294,6 +287,20 @@ val openapiScalaSettings = Seq( // --- Modules +val loggingApi = project + .in(file("modules/logging/api")) + .disablePlugins(RevolverPlugin) + .settings(sharedSettings) + .withTestSettings + .settings( + name := "docspell-logging-api", + libraryDependencies ++= + Dependencies.catsEffect ++ + Dependencies.circeCore ++ + Dependencies.fs2Core ++ + Dependencies.sourcecode + ) + // Base module, everything depends on this – including restapi and // joexapi modules. This should aim to have least possible // dependencies @@ -301,31 +308,44 @@ val common = project .in(file("modules/common")) .disablePlugins(RevolverPlugin) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettings .settings( name := "docspell-common", - addCompilerPlugin(Dependencies.kindProjectorPlugin), libraryDependencies ++= Dependencies.fs2 ++ Dependencies.circe ++ - Dependencies.loggingApi ++ Dependencies.calevCore ++ Dependencies.calevCirce ) + .dependsOn(loggingApi) val config = project .in(file("modules/config")) .disablePlugins(RevolverPlugin) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettings .settings( name := "docspell-config", - addCompilerPlugin(Dependencies.kindProjectorPlugin), libraryDependencies ++= Dependencies.fs2 ++ Dependencies.pureconfig ) - .dependsOn(common) + .dependsOn(common, loggingApi) + +val loggingScribe = project + .in(file("modules/logging/scribe")) + .disablePlugins(RevolverPlugin) + .settings(sharedSettings) + .withTestSettings + .settings( + name := "docspell-logging-scribe", + libraryDependencies ++= + Dependencies.scribe ++ + Dependencies.catsEffect ++ + Dependencies.circeCore ++ + Dependencies.fs2Core + ) + .dependsOn(loggingApi) // Some example files for testing // https://file-examples.com/index.php/sample-documents-download/sample-doc-download/ @@ -333,7 +353,7 @@ val files = project .in(file("modules/files")) .disablePlugins(RevolverPlugin) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettings .settings( name := "docspell-files", libraryDependencies ++= @@ -372,7 +392,7 @@ val query = .in(file("modules/query")) .disablePlugins(RevolverPlugin) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettings .settings( name := "docspell-query", libraryDependencies += @@ -392,7 +412,7 @@ val totp = project .in(file("modules/totp")) .disablePlugins(RevolverPlugin) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettings .settings( name := "docspell-totp", libraryDependencies ++= @@ -406,7 +426,7 @@ val jsonminiq = project .in(file("modules/jsonminiq")) .disablePlugins(RevolverPlugin) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettings .settings( name := "docspell-jsonminiq", libraryDependencies ++= @@ -419,25 +439,23 @@ val notificationApi = project .in(file("modules/notification/api")) .disablePlugins(RevolverPlugin) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettings .settings( name := "docspell-notification-api", - addCompilerPlugin(Dependencies.kindProjectorPlugin), libraryDependencies ++= Dependencies.fs2 ++ Dependencies.emilCommon ++ Dependencies.circeGenericExtra ) - .dependsOn(common) + .dependsOn(common, loggingScribe) val store = project .in(file("modules/store")) .disablePlugins(RevolverPlugin) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettingsDependsOn(loggingScribe) .settings( name := "docspell-store", - addCompilerPlugin(Dependencies.kindProjectorPlugin), libraryDependencies ++= Dependencies.doobie ++ Dependencies.binny ++ @@ -445,7 +463,6 @@ val store = project Dependencies.fs2 ++ Dependencies.databases ++ Dependencies.flyway ++ - Dependencies.loggingApi ++ Dependencies.emil ++ Dependencies.emilDoobie ++ Dependencies.calevCore ++ @@ -453,16 +470,15 @@ val store = project libraryDependencies ++= Dependencies.testContainer.map(_ % Test) ) - .dependsOn(common, query.jvm, totp, files, notificationApi, jsonminiq) + .dependsOn(common, query.jvm, totp, files, notificationApi, jsonminiq, loggingScribe) val notificationImpl = project .in(file("modules/notification/impl")) .disablePlugins(RevolverPlugin) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettings .settings( name := "docspell-notification-impl", - addCompilerPlugin(Dependencies.kindProjectorPlugin), libraryDependencies ++= Dependencies.fs2 ++ Dependencies.emil ++ @@ -479,10 +495,9 @@ val pubsubApi = project .in(file("modules/pubsub/api")) .disablePlugins(RevolverPlugin) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettings .settings( name := "docspell-pubsub-api", - addCompilerPlugin(Dependencies.kindProjectorPlugin), libraryDependencies ++= Dependencies.fs2 ) @@ -492,10 +507,9 @@ val pubsubNaive = project .in(file("modules/pubsub/naive")) .disablePlugins(RevolverPlugin) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettings .settings( name := "docspell-pubsub-naive", - addCompilerPlugin(Dependencies.kindProjectorPlugin), libraryDependencies ++= Dependencies.fs2 ++ Dependencies.http4sCirce ++ @@ -509,7 +523,7 @@ val extract = project .in(file("modules/extract")) .disablePlugins(RevolverPlugin) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettingsDependsOn(loggingScribe) .settings( name := "docspell-extract", libraryDependencies ++= @@ -517,16 +531,15 @@ val extract = project Dependencies.twelvemonkeys ++ Dependencies.pdfbox ++ Dependencies.poi ++ - Dependencies.commonsIO ++ - Dependencies.julOverSlf4j + Dependencies.commonsIO ) - .dependsOn(common, files % "compile->compile;test->test") + .dependsOn(common, loggingScribe, files % "compile->compile;test->test") val convert = project .in(file("modules/convert")) .disablePlugins(RevolverPlugin) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettingsDependsOn(loggingScribe) .settings( name := "docspell-convert", libraryDependencies ++= @@ -541,7 +554,7 @@ val analysis = project .disablePlugins(RevolverPlugin) .enablePlugins(NerModelsPlugin) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettingsDependsOn(loggingScribe) .settings(NerModelsPlugin.nerClassifierSettings) .settings( name := "docspell-analysis", @@ -549,24 +562,24 @@ val analysis = project Dependencies.fs2 ++ Dependencies.stanfordNlpCore ) - .dependsOn(common, files % "test->test") + .dependsOn(common, files % "test->test", loggingScribe) val ftsclient = project .in(file("modules/fts-client")) .disablePlugins(RevolverPlugin) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettings .settings( name := "docspell-fts-client", libraryDependencies ++= Seq.empty ) - .dependsOn(common) + .dependsOn(common, loggingScribe) val ftssolr = project .in(file("modules/fts-solr")) .disablePlugins(RevolverPlugin) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettings .settings( name := "docspell-fts-solr", libraryDependencies ++= @@ -582,7 +595,7 @@ val restapi = project .disablePlugins(RevolverPlugin) .enablePlugins(OpenApiSchema) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettings .settings(openapiScalaSettings) .settings( name := "docspell-restapi", @@ -600,7 +613,7 @@ val joexapi = project .disablePlugins(RevolverPlugin) .enablePlugins(OpenApiSchema) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettings .settings(openapiScalaSettings) .settings( name := "docspell-joexapi", @@ -613,41 +626,39 @@ val joexapi = project openapiSpec := (Compile / resourceDirectory).value / "joex-openapi.yml", openapiStaticGen := OpenApiDocGenerator.Redoc ) - .dependsOn(common) + .dependsOn(common, loggingScribe) val backend = project .in(file("modules/backend")) .disablePlugins(RevolverPlugin) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettings .settings( name := "docspell-backend", libraryDependencies ++= - Dependencies.loggingApi ++ - Dependencies.fs2 ++ + Dependencies.fs2 ++ Dependencies.bcrypt ++ Dependencies.http4sClient ++ Dependencies.emil ) - .dependsOn(store, notificationApi, joexapi, ftsclient, totp, pubsubApi) + .dependsOn(store, notificationApi, joexapi, ftsclient, totp, pubsubApi, loggingApi) val oidc = project .in(file("modules/oidc")) .disablePlugins(RevolverPlugin) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettings .settings( name := "docspell-oidc", libraryDependencies ++= - Dependencies.loggingApi ++ - Dependencies.fs2 ++ + Dependencies.fs2 ++ Dependencies.http4sClient ++ Dependencies.http4sCirce ++ Dependencies.http4sDsl ++ Dependencies.circe ++ Dependencies.jwtScala ) - .dependsOn(common) + .dependsOn(common, loggingScribe) val webapp = project .in(file("modules/webapp")) @@ -678,7 +689,7 @@ val joex = project ClasspathJarPlugin ) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettings .settings(debianSettings("docspell-joex")) .settings(buildInfoSettings) .settings( @@ -698,10 +709,7 @@ val joex = project Dependencies.emilMarkdown ++ Dependencies.emilJsoup ++ Dependencies.jsoup ++ - Dependencies.yamusca ++ - Dependencies.loggingApi ++ - Dependencies.logging.map(_ % Runtime), - addCompilerPlugin(Dependencies.kindProjectorPlugin), + Dependencies.yamusca, addCompilerPlugin(Dependencies.betterMonadicFor), buildInfoPackage := "docspell.joex", reStart / javaOptions ++= Seq( @@ -713,6 +721,8 @@ val joex = project ) .dependsOn( config, + loggingApi, + loggingScribe, store, backend, extract, @@ -735,7 +745,7 @@ val restserver = project ClasspathJarPlugin ) .settings(sharedSettings) - .settings(testSettingsMUnit) + .withTestSettings .settings(debianSettings("docspell-server")) .settings(buildInfoSettings) .settings( @@ -751,10 +761,7 @@ val restserver = project Dependencies.pureconfig ++ Dependencies.yamusca ++ Dependencies.kittens ++ - Dependencies.webjars ++ - Dependencies.loggingApi ++ - Dependencies.logging.map(_ % Runtime), - addCompilerPlugin(Dependencies.kindProjectorPlugin), + Dependencies.webjars, addCompilerPlugin(Dependencies.betterMonadicFor), buildInfoPackage := "docspell.restserver", Compile / sourceGenerators += Def.task { @@ -788,6 +795,8 @@ val restserver = project ) .dependsOn( config, + loggingApi, + loggingScribe, restapi, joexapi, backend, @@ -869,6 +878,8 @@ val root = project ) .aggregate( common, + loggingApi, + loggingScribe, config, extract, convert, diff --git a/modules/analysis/src/main/scala/docspell/analysis/TextAnalyser.scala b/modules/analysis/src/main/scala/docspell/analysis/TextAnalyser.scala index 0158a6af..ca92b377 100644 --- a/modules/analysis/src/main/scala/docspell/analysis/TextAnalyser.scala +++ b/modules/analysis/src/main/scala/docspell/analysis/TextAnalyser.scala @@ -15,8 +15,7 @@ import docspell.analysis.contact.Contact import docspell.analysis.date.DateFind import docspell.analysis.nlp._ import docspell.common._ - -import org.log4s.getLogger +import docspell.logging.Logger trait TextAnalyser[F[_]] { @@ -30,7 +29,6 @@ trait TextAnalyser[F[_]] { def classifier: TextClassifier[F] } object TextAnalyser { - private[this] val logger = getLogger case class Result(labels: Vector[NerLabel], dates: Vector[NerDateLabel]) { @@ -87,10 +85,11 @@ object TextAnalyser { private object Nlp { def apply[F[_]: Async]( cfg: TextAnalysisConfig.NlpConfig - ): F[Input[F] => F[Vector[NerLabel]]] = + ): F[Input[F] => F[Vector[NerLabel]]] = { + val log = docspell.logging.getLogger[F] cfg.mode match { case NlpMode.Disabled => - Logger.log4s(logger).info("NLP is disabled as defined in config.") *> + log.info("NLP is disabled as defined in config.") *> Applicative[F].pure(_ => Vector.empty[NerLabel].pure[F]) case _ => PipelineCache(cfg.clearInterval)( @@ -99,6 +98,7 @@ object TextAnalyser { ) .map(annotate[F]) } + } final case class Input[F[_]]( key: Ident, diff --git a/modules/analysis/src/main/scala/docspell/analysis/classifier/StanfordTextClassifier.scala b/modules/analysis/src/main/scala/docspell/analysis/classifier/StanfordTextClassifier.scala index 9fe4f72f..15ac3f24 100644 --- a/modules/analysis/src/main/scala/docspell/analysis/classifier/StanfordTextClassifier.scala +++ b/modules/analysis/src/main/scala/docspell/analysis/classifier/StanfordTextClassifier.scala @@ -17,6 +17,7 @@ import docspell.analysis.classifier.TextClassifier._ import docspell.analysis.nlp.Properties import docspell.common._ import docspell.common.syntax.FileSyntax._ +import docspell.logging.Logger import edu.stanford.nlp.classify.ColumnDataClassifier diff --git a/modules/analysis/src/main/scala/docspell/analysis/classifier/TextClassifier.scala b/modules/analysis/src/main/scala/docspell/analysis/classifier/TextClassifier.scala index b0450e92..3fc2662d 100644 --- a/modules/analysis/src/main/scala/docspell/analysis/classifier/TextClassifier.scala +++ b/modules/analysis/src/main/scala/docspell/analysis/classifier/TextClassifier.scala @@ -10,7 +10,7 @@ import cats.data.Kleisli import fs2.Stream import docspell.analysis.classifier.TextClassifier.Data -import docspell.common._ +import docspell.logging.Logger trait TextClassifier[F[_]] { diff --git a/modules/analysis/src/main/scala/docspell/analysis/nlp/Annotator.scala b/modules/analysis/src/main/scala/docspell/analysis/nlp/Annotator.scala index b35700ee..48be13fa 100644 --- a/modules/analysis/src/main/scala/docspell/analysis/nlp/Annotator.scala +++ b/modules/analysis/src/main/scala/docspell/analysis/nlp/Annotator.scala @@ -12,6 +12,7 @@ import cats.{Applicative, FlatMap} import docspell.analysis.NlpSettings import docspell.common._ +import docspell.logging.Logger import edu.stanford.nlp.pipeline.StanfordCoreNLP diff --git a/modules/analysis/src/main/scala/docspell/analysis/nlp/BasicCRFAnnotator.scala b/modules/analysis/src/main/scala/docspell/analysis/nlp/BasicCRFAnnotator.scala index ae580992..63fd79d4 100644 --- a/modules/analysis/src/main/scala/docspell/analysis/nlp/BasicCRFAnnotator.scala +++ b/modules/analysis/src/main/scala/docspell/analysis/nlp/BasicCRFAnnotator.scala @@ -19,14 +19,13 @@ import docspell.common._ import edu.stanford.nlp.ie.AbstractSequenceClassifier import edu.stanford.nlp.ie.crf.CRFClassifier import edu.stanford.nlp.ling.{CoreAnnotations, CoreLabel} -import org.log4s.getLogger /** This is only using the CRFClassifier without building an analysis pipeline. The * ner-classifier cannot use results from POS-tagging etc. and is therefore not as good * as the [[StanfordNerAnnotator]]. But it uses less memory, while still being not bad. */ object BasicCRFAnnotator { - private[this] val logger = getLogger + private[this] val logger = docspell.logging.unsafeLogger // assert correct resource names NLPLanguage.all.toList.foreach(classifierResource) diff --git a/modules/analysis/src/main/scala/docspell/analysis/nlp/PipelineCache.scala b/modules/analysis/src/main/scala/docspell/analysis/nlp/PipelineCache.scala index caee9e70..f8423492 100644 --- a/modules/analysis/src/main/scala/docspell/analysis/nlp/PipelineCache.scala +++ b/modules/analysis/src/main/scala/docspell/analysis/nlp/PipelineCache.scala @@ -15,8 +15,6 @@ import cats.implicits._ import docspell.analysis.NlpSettings import docspell.common._ -import org.log4s.getLogger - /** Creating the StanfordCoreNLP pipeline is quite expensive as it involves IO and * initializing large objects. * @@ -31,17 +29,19 @@ trait PipelineCache[F[_]] { } object PipelineCache { - private[this] val logger = getLogger + private[this] val logger = docspell.logging.unsafeLogger def apply[F[_]: Async](clearInterval: Duration)( creator: NlpSettings => Annotator[F], release: F[Unit] - ): F[PipelineCache[F]] = + ): F[PipelineCache[F]] = { + val log = docspell.logging.getLogger[F] for { data <- Ref.of(Map.empty[String, Entry[Annotator[F]]]) cacheClear <- CacheClearing.create(data, clearInterval, release) - _ <- Logger.log4s(logger).info("Creating nlp pipeline cache") + _ <- log.info("Creating nlp pipeline cache") } yield new Impl[F](data, creator, cacheClear) + } final private class Impl[F[_]: Async]( data: Ref[F, Map[String, Entry[Annotator[F]]]], @@ -116,7 +116,7 @@ object PipelineCache { for { counter <- Ref.of(0L) cleaning <- Ref.of(None: Option[Fiber[F, Throwable, Unit]]) - log = Logger.log4s(logger) + log = docspell.logging.getLogger[F] result <- if (interval.millis <= 0) log @@ -145,7 +145,7 @@ object PipelineCache { release: F[Unit] )(implicit F: Async[F]) extends CacheClearing[F] { - private[this] val log = Logger.log4s[F](logger) + private[this] val log = docspell.logging.getLogger[F] def withCache: Resource[F, Unit] = Resource.make(counter.update(_ + 1) *> cancelClear)(_ => diff --git a/modules/analysis/src/main/scala/docspell/analysis/nlp/StanfordNerAnnotator.scala b/modules/analysis/src/main/scala/docspell/analysis/nlp/StanfordNerAnnotator.scala index 1ed0fb5e..36e40b12 100644 --- a/modules/analysis/src/main/scala/docspell/analysis/nlp/StanfordNerAnnotator.scala +++ b/modules/analysis/src/main/scala/docspell/analysis/nlp/StanfordNerAnnotator.scala @@ -14,10 +14,9 @@ import fs2.io.file.Path import docspell.common._ import edu.stanford.nlp.pipeline.{CoreDocument, StanfordCoreNLP} -import org.log4s.getLogger object StanfordNerAnnotator { - private[this] val logger = getLogger + private[this] val logger = docspell.logging.unsafeLogger /** Runs named entity recognition on the given `text`. * diff --git a/modules/analysis/src/test/scala/docspell/analysis/classifier/StanfordTextClassifierSuite.scala b/modules/analysis/src/test/scala/docspell/analysis/classifier/StanfordTextClassifierSuite.scala index 3dd34255..86804836 100644 --- a/modules/analysis/src/test/scala/docspell/analysis/classifier/StanfordTextClassifierSuite.scala +++ b/modules/analysis/src/test/scala/docspell/analysis/classifier/StanfordTextClassifierSuite.scala @@ -17,11 +17,12 @@ import fs2.io.file.Files import docspell.analysis.classifier.TextClassifier.Data import docspell.common._ +import docspell.logging.TestLoggingConfig import munit._ -class StanfordTextClassifierSuite extends FunSuite { - val logger = Logger.log4s[IO](org.log4s.getLogger) +class StanfordTextClassifierSuite extends FunSuite with TestLoggingConfig { + val logger = docspell.logging.getLogger[IO] test("learn from data") { val cfg = TextClassifierConfig(File.path(Paths.get("target")), NonEmptyList.of(Map())) diff --git a/modules/analysis/src/test/scala/docspell/analysis/nlp/BaseCRFAnnotatorSuite.scala b/modules/analysis/src/test/scala/docspell/analysis/nlp/BaseCRFAnnotatorSuite.scala index 77f665b9..d5d07a46 100644 --- a/modules/analysis/src/test/scala/docspell/analysis/nlp/BaseCRFAnnotatorSuite.scala +++ b/modules/analysis/src/test/scala/docspell/analysis/nlp/BaseCRFAnnotatorSuite.scala @@ -10,10 +10,11 @@ import docspell.analysis.Env import docspell.common.Language.NLPLanguage import docspell.common._ import docspell.files.TestFiles +import docspell.logging.TestLoggingConfig import munit._ -class BaseCRFAnnotatorSuite extends FunSuite { +class BaseCRFAnnotatorSuite extends FunSuite with TestLoggingConfig { def annotate(language: NLPLanguage): String => Vector[NerLabel] = BasicCRFAnnotator.nerAnnotate(BasicCRFAnnotator.Cache.getAnnotator(language)) diff --git a/modules/analysis/src/test/scala/docspell/analysis/nlp/StanfordNerAnnotatorSuite.scala b/modules/analysis/src/test/scala/docspell/analysis/nlp/StanfordNerAnnotatorSuite.scala index eee0a9c5..d522ec6c 100644 --- a/modules/analysis/src/test/scala/docspell/analysis/nlp/StanfordNerAnnotatorSuite.scala +++ b/modules/analysis/src/test/scala/docspell/analysis/nlp/StanfordNerAnnotatorSuite.scala @@ -14,11 +14,12 @@ import cats.effect.unsafe.implicits.global import docspell.analysis.Env import docspell.common._ import docspell.files.TestFiles +import docspell.logging.TestLoggingConfig import edu.stanford.nlp.pipeline.StanfordCoreNLP import munit._ -class StanfordNerAnnotatorSuite extends FunSuite { +class StanfordNerAnnotatorSuite extends FunSuite with TestLoggingConfig { lazy val germanClassifier = new StanfordCoreNLP(Properties.nerGerman(None, false)) lazy val englishClassifier = diff --git a/modules/backend/src/main/scala/docspell/backend/auth/Login.scala b/modules/backend/src/main/scala/docspell/backend/auth/Login.scala index 12752962..2091e8f3 100644 --- a/modules/backend/src/main/scala/docspell/backend/auth/Login.scala +++ b/modules/backend/src/main/scala/docspell/backend/auth/Login.scala @@ -17,7 +17,6 @@ import docspell.store.queries.QLogin import docspell.store.records._ import docspell.totp.{OnetimePassword, Totp} -import org.log4s.getLogger import org.mindrot.jbcrypt.BCrypt import scodec.bits.ByteVector @@ -41,8 +40,6 @@ trait Login[F[_]] { } object Login { - private[this] val logger = getLogger - case class Config( serverSecret: ByteVector, sessionValid: Duration, @@ -93,7 +90,7 @@ object Login { def apply[F[_]: Async](store: Store[F], totp: Totp): Resource[F, Login[F]] = Resource.pure[F, Login[F]](new Login[F] { - private val logF = Logger.log4s(logger) + private val logF = docspell.logging.getLogger[F] def loginExternal(config: Config)(accountId: AccountId): F[Result] = for { @@ -124,7 +121,7 @@ object Login { case Right(acc) => for { data <- store.transact(QLogin.findUser(acc)) - _ <- Sync[F].delay(logger.trace(s"Account lookup: $data")) + _ <- logF.trace(s"Account lookup: $data") res <- if (data.exists(check(up.pass))) doLogin(config, acc, up.rememberMe) else Result.invalidAuth.pure[F] diff --git a/modules/backend/src/main/scala/docspell/backend/fulltext/CreateIndex.scala b/modules/backend/src/main/scala/docspell/backend/fulltext/CreateIndex.scala index 214bef1a..38c1ea67 100644 --- a/modules/backend/src/main/scala/docspell/backend/fulltext/CreateIndex.scala +++ b/modules/backend/src/main/scala/docspell/backend/fulltext/CreateIndex.scala @@ -12,6 +12,7 @@ import cats.effect._ import docspell.common._ import docspell.ftsclient.FtsClient import docspell.ftsclient.TextData +import docspell.logging.Logger import docspell.store.Store import docspell.store.queries.QAttachment import docspell.store.queries.QItem diff --git a/modules/backend/src/main/scala/docspell/backend/item/Merge.scala b/modules/backend/src/main/scala/docspell/backend/item/Merge.scala index 9d9cbb8a..3fdfcce9 100644 --- a/modules/backend/src/main/scala/docspell/backend/item/Merge.scala +++ b/modules/backend/src/main/scala/docspell/backend/item/Merge.scala @@ -14,6 +14,7 @@ import cats.implicits._ import docspell.backend.fulltext.CreateIndex import docspell.backend.ops.OItem import docspell.common._ +import docspell.logging.Logger import docspell.store.Store import docspell.store.queries.QCustomField import docspell.store.queries.QCustomField.FieldValue diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OClientSettings.scala b/modules/backend/src/main/scala/docspell/backend/ops/OClientSettings.scala index 560e4db4..0db6ce2d 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OClientSettings.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OClientSettings.scala @@ -37,11 +37,9 @@ trait OClientSettings[F[_]] { } object OClientSettings { - private[this] val logger = org.log4s.getLogger - def apply[F[_]: Async](store: Store[F]): Resource[F, OClientSettings[F]] = Resource.pure[F, OClientSettings[F]](new OClientSettings[F] { - val log = Logger.log4s[F](logger) + val log = docspell.logging.getLogger[F] private def getUserId(account: AccountId): OptionT[F, Ident] = OptionT(store.transact(RUser.findByAccount(account))).map(_.uid) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala b/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala index 258930a9..a5416048 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala @@ -31,7 +31,6 @@ import docspell.store.records.RCustomFieldValue import docspell.store.records.RItem import doobie._ -import org.log4s.getLogger trait OCustomFields[F[_]] { @@ -153,7 +152,7 @@ object OCustomFields { ): Resource[F, OCustomFields[F]] = Resource.pure[F, OCustomFields[F]](new OCustomFields[F] { - private[this] val logger = Logger.log4s[ConnectionIO](getLogger) + private[this] val logger = docspell.logging.getLogger[ConnectionIO] def findAllValues(itemIds: Nel[Ident]): F[List[FieldValue]] = store.transact(QCustomField.findAllValues(itemIds)) @@ -224,7 +223,7 @@ object OCustomFields { .transact(RItem.existsByIdsAndCollective(items, value.collective)) .map(flag => if (flag) Right(()) else Left(SetValueResult.itemNotFound)) ) - nu <- EitherT.right[SetValueResult]( + _ <- EitherT.right[SetValueResult]( items .traverse(item => store.transact(RCustomField.setValue(field, item, fval))) .map(_.toList.sum) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OFulltext.scala b/modules/backend/src/main/scala/docspell/backend/ops/OFulltext.scala index 4cdcd0dd..9d057f1c 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OFulltext.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OFulltext.scala @@ -14,7 +14,6 @@ import fs2.Stream import docspell.backend.JobFactory import docspell.backend.ops.OItemSearch._ import docspell.common._ -import docspell.common.syntax.all._ import docspell.ftsclient._ import docspell.query.ItemQuery._ import docspell.query.ItemQueryDsl._ @@ -23,8 +22,6 @@ import docspell.store.queue.JobQueue import docspell.store.records.RJob import docspell.store.{Store, qb} -import org.log4s.getLogger - trait OFulltext[F[_]] { def findItems(maxNoteLen: Int)( @@ -59,7 +56,6 @@ trait OFulltext[F[_]] { } object OFulltext { - private[this] val logger = getLogger case class FtsInput( query: String, @@ -89,16 +85,17 @@ object OFulltext { joex: OJoex[F] ): Resource[F, OFulltext[F]] = Resource.pure[F, OFulltext[F]](new OFulltext[F] { + val logger = docspell.logging.getLogger[F] def reindexAll: F[Unit] = for { - _ <- logger.finfo(s"Re-index all.") + _ <- logger.info(s"Re-index all.") job <- JobFactory.reIndexAll[F] _ <- queue.insertIfNew(job) *> joex.notifyAllNodes } yield () def reindexCollective(account: AccountId): F[Unit] = for { - _ <- logger.fdebug(s"Re-index collective: $account") + _ <- logger.debug(s"Re-index collective: $account") exist <- store.transact( RJob.findNonFinalByTracker(DocspellSystem.migrationTaskTracker) ) @@ -123,7 +120,7 @@ object OFulltext { FtsQuery.HighlightSetting(ftsQ.highlightPre, ftsQ.highlightPost) ) for { - _ <- logger.ftrace(s"Find index only: ${ftsQ.query}/$batch") + _ <- logger.trace(s"Find index only: ${ftsQ.query}/$batch") folders <- store.transact(QFolder.getMemberFolders(account)) ftsR <- fts.search(fq.withFolders(folders)) ftsItems = ftsR.results.groupBy(_.itemId) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala index b49d09cc..f625cdfe 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala @@ -16,6 +16,7 @@ import docspell.backend.fulltext.CreateIndex import docspell.backend.item.Merge import docspell.common._ import docspell.ftsclient.FtsClient +import docspell.logging.Logger import docspell.notification.api.Event import docspell.store.queries.{QAttachment, QItem, QMoveAttachment} import docspell.store.queue.JobQueue @@ -23,7 +24,6 @@ import docspell.store.records._ import docspell.store.{AddResult, Store, UpdateResult} import doobie.implicits._ -import org.log4s.getLogger trait OItem[F[_]] { @@ -235,7 +235,7 @@ object OItem { otag <- OTag(store) oorg <- OOrganization(store) oequip <- OEquipment(store) - logger <- Resource.pure[F, Logger[F]](Logger.log4s(getLogger)) + logger <- Resource.pure[F, Logger[F]](docspell.logging.getLogger[F]) oitem <- Resource.pure[F, OItem[F]](new OItem[F] { def merge( diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OJob.scala b/modules/backend/src/main/scala/docspell/backend/ops/OJob.scala index 0c3d92ae..b4b0b7ae 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OJob.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OJob.scala @@ -59,7 +59,7 @@ object OJob { pubsub: PubSubT[F] ): Resource[F, OJob[F]] = Resource.pure[F, OJob[F]](new OJob[F] { - private[this] val logger = Logger.log4s(org.log4s.getLogger(OJob.getClass)) + private[this] val logger = docspell.logging.getLogger[F] def queueState(collective: Ident, maxResults: Int): F[CollectiveQueueState] = store diff --git a/modules/backend/src/main/scala/docspell/backend/ops/ONode.scala b/modules/backend/src/main/scala/docspell/backend/ops/ONode.scala index b8e89ee2..8b55ed29 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/ONode.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/ONode.scala @@ -9,13 +9,10 @@ package docspell.backend.ops import cats.effect.{Async, Resource} import cats.implicits._ -import docspell.common.syntax.all._ import docspell.common.{Ident, LenientUri, NodeType} import docspell.store.Store import docspell.store.records.RNode -import org.log4s._ - trait ONode[F[_]] { def register(appId: Ident, nodeType: NodeType, uri: LenientUri): F[Unit] @@ -24,20 +21,19 @@ trait ONode[F[_]] { } object ONode { - private[this] val logger = getLogger def apply[F[_]: Async](store: Store[F]): Resource[F, ONode[F]] = Resource.pure[F, ONode[F]](new ONode[F] { - + val logger = docspell.logging.getLogger[F] def register(appId: Ident, nodeType: NodeType, uri: LenientUri): F[Unit] = for { node <- RNode(appId, nodeType, uri) - _ <- logger.finfo(s"Registering node ${node.id.id}") + _ <- logger.info(s"Registering node ${node.id.id}") _ <- store.transact(RNode.set(node)) } yield () def unregister(appId: Ident): F[Unit] = - logger.finfo(s"Unregister app ${appId.id}") *> + logger.info(s"Unregister app ${appId.id}") *> store.transact(RNode.delete(appId)).map(_ => ()) }) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/ONotification.scala b/modules/backend/src/main/scala/docspell/backend/ops/ONotification.scala index 1ed08014..05b58275 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/ONotification.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/ONotification.scala @@ -6,9 +6,6 @@ package docspell.backend.ops -import java.io.PrintWriter -import java.io.StringWriter - import cats.data.OptionT import cats.data.{NonEmptyList => Nel} import cats.effect._ @@ -17,6 +14,7 @@ import cats.implicits._ import docspell.backend.ops.ONotification.Hook import docspell.common._ import docspell.jsonminiq.JsonMiniQuery +import docspell.logging.{Level, LogEvent, Logger} import docspell.notification.api._ import docspell.store.AddResult import docspell.store.Store @@ -75,14 +73,13 @@ trait ONotification[F[_]] { } object ONotification { - private[this] val logger = org.log4s.getLogger def apply[F[_]: Async]( store: Store[F], notMod: NotificationModule[F] ): Resource[F, ONotification[F]] = Resource.pure[F, ONotification[F]](new ONotification[F] { - val log = Logger.log4s[F](logger) + val log = docspell.logging.getLogger[F] def withUserId[A]( account: AccountId @@ -129,9 +126,9 @@ object ONotification { .map { case Right(res) => res case Left(ex) => - val ps = new StringWriter() - ex.printStackTrace(new PrintWriter(ps)) - SendTestResult(false, Vector(s"${ex.getMessage}\n$ps")) + val ev = + LogEvent.of(Level.Error, "Failed sending sample event").addError(ex) + SendTestResult(false, Vector(ev)) } def listChannels(account: AccountId): F[Vector[Channel]] = @@ -316,5 +313,5 @@ object ONotification { } yield h } - final case class SendTestResult(success: Boolean, logMessages: Vector[String]) + final case class SendTestResult(success: Boolean, logEvents: Vector[LogEvent]) } diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala index 75bdc27a..1f393451 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala @@ -152,7 +152,7 @@ object OShare { emil: Emil[F] ): OShare[F] = new OShare[F] { - private[this] val logger = Logger.log4s[F](org.log4s.getLogger) + private[this] val logger = docspell.logging.getLogger[F] def findAll( collective: Ident, diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OTotp.scala b/modules/backend/src/main/scala/docspell/backend/ops/OTotp.scala index a872fa53..67ffcdf1 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OTotp.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OTotp.scala @@ -5,6 +5,7 @@ */ package docspell.backend.ops + import cats.effect._ import cats.implicits._ @@ -14,8 +15,6 @@ import docspell.store.records.{RTotp, RUser} import docspell.store.{AddResult, Store, UpdateResult} import docspell.totp.{Key, OnetimePassword, Totp} -import org.log4s.getLogger - trait OTotp[F[_]] { /** Return whether TOTP is enabled for this account or not. */ @@ -38,8 +37,6 @@ trait OTotp[F[_]] { } object OTotp { - private[this] val logger = getLogger - sealed trait OtpState { def isEnabled: Boolean def isDisabled = !isEnabled @@ -86,7 +83,7 @@ object OTotp { def apply[F[_]: Async](store: Store[F], totp: Totp): Resource[F, OTotp[F]] = Resource.pure[F, OTotp[F]](new OTotp[F] { - val log = Logger.log4s[F](logger) + val log = docspell.logging.getLogger[F] def initialize(accountId: AccountId): F[InitResult] = for { diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OUpload.scala b/modules/backend/src/main/scala/docspell/backend/ops/OUpload.scala index ec959f6c..dbda65b2 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OUpload.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OUpload.scala @@ -14,13 +14,10 @@ import fs2.Stream import docspell.backend.JobFactory import docspell.common._ -import docspell.common.syntax.all._ import docspell.store.Store import docspell.store.queue.JobQueue import docspell.store.records._ -import org.log4s._ - trait OUpload[F[_]] { def submit( @@ -56,8 +53,6 @@ trait OUpload[F[_]] { } object OUpload { - private[this] val logger = getLogger - case class File[F[_]]( name: Option[String], advertisedMime: Option[MimeType], @@ -117,7 +112,7 @@ object OUpload { joex: OJoex[F] ): Resource[F, OUpload[F]] = Resource.pure[F, OUpload[F]](new OUpload[F] { - + private[this] val logger = docspell.logging.getLogger[F] def submit( data: OUpload.UploadData[F], account: AccountId, @@ -155,7 +150,7 @@ object OUpload { if (data.multiple) files.map(f => ProcessItemArgs(meta, List(f))) else Vector(ProcessItemArgs(meta, files.toList)) jobs <- right(makeJobs(args, account, data.priority, data.tracker)) - _ <- right(logger.fdebug(s"Storing jobs: $jobs")) + _ <- right(logger.debug(s"Storing jobs: $jobs")) res <- right(submitJobs(notifyJoex)(jobs)) _ <- right( store.transact( @@ -194,7 +189,7 @@ object OUpload { notifyJoex: Boolean )(jobs: Vector[RJob]): F[OUpload.UploadResult] = for { - _ <- logger.fdebug(s"Storing jobs: $jobs") + _ <- logger.debug(s"Storing jobs: $jobs") _ <- queue.insertAll(jobs) _ <- if (notifyJoex) joex.notifyAllNodes else ().pure[F] } yield UploadResult.Success @@ -203,7 +198,7 @@ object OUpload { private def saveFile( accountId: AccountId )(file: File[F]): F[Option[ProcessItemArgs.File]] = - logger.finfo(s"Receiving file $file") *> + logger.info(s"Receiving file $file") *> file.data .through( store.fileRepo.save( diff --git a/modules/backend/src/main/scala/docspell/backend/signup/OSignup.scala b/modules/backend/src/main/scala/docspell/backend/signup/OSignup.scala index 500469b5..c59bc773 100644 --- a/modules/backend/src/main/scala/docspell/backend/signup/OSignup.scala +++ b/modules/backend/src/main/scala/docspell/backend/signup/OSignup.scala @@ -11,12 +11,10 @@ import cats.implicits._ import docspell.backend.PasswordCrypt import docspell.common._ -import docspell.common.syntax.all._ import docspell.store.records.{RCollective, RInvitation, RUser} import docspell.store.{AddResult, Store} import doobie.free.connection.ConnectionIO -import org.log4s.getLogger trait OSignup[F[_]] { @@ -29,10 +27,10 @@ trait OSignup[F[_]] { } object OSignup { - private[this] val logger = getLogger def apply[F[_]: Async](store: Store[F]): Resource[F, OSignup[F]] = Resource.pure[F, OSignup[F]](new OSignup[F] { + private[this] val logger = docspell.logging.getLogger[F] def newInvite(cfg: Config)(password: Password): F[NewInviteResult] = if (cfg.mode == Config.Mode.Invite) @@ -66,7 +64,7 @@ object OSignup { _ <- if (retryInvite(res)) logger - .fdebug( + .debug( s"Adding account failed ($res). Allow retry with invite." ) *> store .transact( diff --git a/modules/common/src/main/scala/docspell/common/LogLevel.scala b/modules/common/src/main/scala/docspell/common/LogLevel.scala index efec4d73..201c7a4f 100644 --- a/modules/common/src/main/scala/docspell/common/LogLevel.scala +++ b/modules/common/src/main/scala/docspell/common/LogLevel.scala @@ -6,6 +6,8 @@ package docspell.common +import docspell.logging.Level + import io.circe.{Decoder, Encoder} sealed trait LogLevel { self: Product => @@ -40,6 +42,16 @@ object LogLevel { case _ => Left(s"Invalid log-level: $str") } + def fromLevel(level: Level): LogLevel = + level match { + case Level.Fatal => LogLevel.Error + case Level.Error => LogLevel.Error + case Level.Warn => LogLevel.Warn + case Level.Info => LogLevel.Info + case Level.Debug => LogLevel.Debug + case Level.Trace => LogLevel.Debug + } + def unsafeString(str: String): LogLevel = fromString(str).fold(sys.error, identity) diff --git a/modules/common/src/main/scala/docspell/common/Logger.scala b/modules/common/src/main/scala/docspell/common/Logger.scala deleted file mode 100644 index 66b583e2..00000000 --- a/modules/common/src/main/scala/docspell/common/Logger.scala +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2020 Eike K. & Contributors - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package docspell.common - -import java.io.{PrintWriter, StringWriter} - -import cats.Applicative -import cats.effect.{Ref, Sync} -import cats.implicits._ -import fs2.Stream - -import docspell.common.syntax.all._ - -import org.log4s.{Logger => Log4sLogger} - -trait Logger[F[_]] { self => - - def trace(msg: => String): F[Unit] - def debug(msg: => String): F[Unit] - def info(msg: => String): F[Unit] - def warn(msg: => String): F[Unit] - def error(ex: Throwable)(msg: => String): F[Unit] - def error(msg: => String): F[Unit] - - final def s: Logger[Stream[F, *]] = new Logger[Stream[F, *]] { - def trace(msg: => String): Stream[F, Unit] = - Stream.eval(self.trace(msg)) - - def debug(msg: => String): Stream[F, Unit] = - Stream.eval(self.debug(msg)) - - def info(msg: => String): Stream[F, Unit] = - Stream.eval(self.info(msg)) - - def warn(msg: => String): Stream[F, Unit] = - Stream.eval(self.warn(msg)) - - def error(msg: => String): Stream[F, Unit] = - Stream.eval(self.error(msg)) - - def error(ex: Throwable)(msg: => String): Stream[F, Unit] = - Stream.eval(self.error(ex)(msg)) - } - def andThen(other: Logger[F])(implicit F: Sync[F]): Logger[F] = { - val self = this - new Logger[F] { - def trace(msg: => String) = - self.trace(msg) >> other.trace(msg) - - override def debug(msg: => String) = - self.debug(msg) >> other.debug(msg) - - override def info(msg: => String) = - self.info(msg) >> other.info(msg) - - override def warn(msg: => String) = - self.warn(msg) >> other.warn(msg) - - override def error(ex: Throwable)(msg: => String) = - self.error(ex)(msg) >> other.error(ex)(msg) - - override def error(msg: => String) = - self.error(msg) >> other.error(msg) - } - } -} - -object Logger { - - def off[F[_]: Applicative]: Logger[F] = - new Logger[F] { - def trace(msg: => String): F[Unit] = - Applicative[F].pure(()) - - def debug(msg: => String): F[Unit] = - Applicative[F].pure(()) - - def info(msg: => String): F[Unit] = - Applicative[F].pure(()) - - def warn(msg: => String): F[Unit] = - Applicative[F].pure(()) - - def error(ex: Throwable)(msg: => String): F[Unit] = - Applicative[F].pure(()) - - def error(msg: => String): F[Unit] = - Applicative[F].pure(()) - } - - def log4s[F[_]: Sync](log: Log4sLogger): Logger[F] = - new Logger[F] { - def trace(msg: => String): F[Unit] = - log.ftrace(msg) - - def debug(msg: => String): F[Unit] = - log.fdebug(msg) - - def info(msg: => String): F[Unit] = - log.finfo(msg) - - def warn(msg: => String): F[Unit] = - log.fwarn(msg) - - def error(ex: Throwable)(msg: => String): F[Unit] = - log.ferror(ex)(msg) - - def error(msg: => String): F[Unit] = - log.ferror(msg) - } - - def buffer[F[_]: Sync](): F[(Ref[F, Vector[String]], Logger[F])] = - for { - buffer <- Ref.of[F, Vector[String]](Vector.empty[String]) - logger = new Logger[F] { - def trace(msg: => String) = - buffer.update(_.appended(s"TRACE $msg")) - - def debug(msg: => String) = - buffer.update(_.appended(s"DEBUG $msg")) - - def info(msg: => String) = - buffer.update(_.appended(s"INFO $msg")) - - def warn(msg: => String) = - buffer.update(_.appended(s"WARN $msg")) - - def error(ex: Throwable)(msg: => String) = { - val ps = new StringWriter() - ex.printStackTrace(new PrintWriter(ps)) - buffer.update(_.appended(s"ERROR $msg:\n$ps")) - } - - def error(msg: => String) = - buffer.update(_.appended(s"ERROR $msg")) - } - } yield (buffer, logger) - -} diff --git a/modules/common/src/main/scala/docspell/common/SystemCommand.scala b/modules/common/src/main/scala/docspell/common/SystemCommand.scala index 2382c36e..63f4ca40 100644 --- a/modules/common/src/main/scala/docspell/common/SystemCommand.scala +++ b/modules/common/src/main/scala/docspell/common/SystemCommand.scala @@ -17,6 +17,8 @@ import cats.implicits._ import fs2.io.file.Path import fs2.{Stream, io, text} +import docspell.logging.Logger + object SystemCommand { final case class Config(program: String, args: Seq[String], timeout: Duration) { diff --git a/modules/common/src/main/scala/docspell/common/syntax/LoggerSyntax.scala b/modules/common/src/main/scala/docspell/common/syntax/LoggerSyntax.scala deleted file mode 100644 index 54490a85..00000000 --- a/modules/common/src/main/scala/docspell/common/syntax/LoggerSyntax.scala +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2020 Eike K. & Contributors - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package docspell.common.syntax - -import cats.effect.Sync -import fs2.Stream - -import org.log4s.Logger - -trait LoggerSyntax { - - implicit final class LoggerOps(logger: Logger) { - - def ftrace[F[_]: Sync](msg: => String): F[Unit] = - Sync[F].delay(logger.trace(msg)) - - def fdebug[F[_]: Sync](msg: => String): F[Unit] = - Sync[F].delay(logger.debug(msg)) - - def sdebug[F[_]: Sync](msg: => String): Stream[F, Nothing] = - Stream.eval(fdebug(msg)).drain - - def finfo[F[_]: Sync](msg: => String): F[Unit] = - Sync[F].delay(logger.info(msg)) - - def sinfo[F[_]: Sync](msg: => String): Stream[F, Nothing] = - Stream.eval(finfo(msg)).drain - - def fwarn[F[_]: Sync](msg: => String): F[Unit] = - Sync[F].delay(logger.warn(msg)) - - def ferror[F[_]: Sync](msg: => String): F[Unit] = - Sync[F].delay(logger.error(msg)) - - def ferror[F[_]: Sync](ex: Throwable)(msg: => String): F[Unit] = - Sync[F].delay(logger.error(ex)(msg)) - } -} diff --git a/modules/common/src/main/scala/docspell/common/syntax/package.scala b/modules/common/src/main/scala/docspell/common/syntax/package.scala index 522feaf0..a295982d 100644 --- a/modules/common/src/main/scala/docspell/common/syntax/package.scala +++ b/modules/common/src/main/scala/docspell/common/syntax/package.scala @@ -8,11 +8,6 @@ package docspell.common package object syntax { - object all - extends EitherSyntax - with StreamSyntax - with StringSyntax - with LoggerSyntax - with FileSyntax + object all extends EitherSyntax with StreamSyntax with StringSyntax with FileSyntax } diff --git a/modules/config/src/main/scala/docspell/config/ConfigFactory.scala b/modules/config/src/main/scala/docspell/config/ConfigFactory.scala index 47cfac26..3522a48e 100644 --- a/modules/config/src/main/scala/docspell/config/ConfigFactory.scala +++ b/modules/config/src/main/scala/docspell/config/ConfigFactory.scala @@ -13,7 +13,7 @@ import cats.effect._ import cats.implicits._ import fs2.io.file.{Files, Path} -import docspell.common.Logger +import docspell.logging.Logger import pureconfig.{ConfigReader, ConfigSource} diff --git a/modules/config/src/main/scala/docspell/config/Implicits.scala b/modules/config/src/main/scala/docspell/config/Implicits.scala index 77136cbc..0bc7dda4 100644 --- a/modules/config/src/main/scala/docspell/config/Implicits.scala +++ b/modules/config/src/main/scala/docspell/config/Implicits.scala @@ -13,6 +13,7 @@ import scala.reflect.ClassTag import fs2.io.file.Path import docspell.common._ +import docspell.logging.{Level, LogConfig} import com.github.eikek.calev.CalEvent import pureconfig.ConfigReader @@ -63,6 +64,12 @@ object Implicits { implicit val nlpModeReader: ConfigReader[NlpMode] = ConfigReader[String].emap(reason(NlpMode.fromString)) + implicit val logFormatReader: ConfigReader[LogConfig.Format] = + ConfigReader[String].emap(reason(LogConfig.Format.fromString)) + + implicit val logLevelReader: ConfigReader[Level] = + ConfigReader[String].emap(reason(Level.fromString)) + def reason[A: ClassTag]( f: String => Either[String, A] ): String => Either[FailureReason, A] = diff --git a/modules/convert/src/main/scala/docspell/convert/Conversion.scala b/modules/convert/src/main/scala/docspell/convert/Conversion.scala index b1a05aa4..19b1279c 100644 --- a/modules/convert/src/main/scala/docspell/convert/Conversion.scala +++ b/modules/convert/src/main/scala/docspell/convert/Conversion.scala @@ -17,6 +17,7 @@ import docspell.convert.ConversionResult.Handler import docspell.convert.extern._ import docspell.convert.flexmark.Markdown import docspell.files.{ImageSize, TikaMimetype} +import docspell.logging.Logger import scodec.bits.ByteVector @@ -46,7 +47,7 @@ object Conversion { val allPass = cfg.decryptPdf.passwords ++ additionalPasswords val pdfStream = if (cfg.decryptPdf.enabled) { - logger.s + logger.stream .debug(s"Trying to read the PDF using ${allPass.size} passwords") .drain ++ in.through(RemovePdfEncryption(logger, allPass)) diff --git a/modules/convert/src/main/scala/docspell/convert/RemovePdfEncryption.scala b/modules/convert/src/main/scala/docspell/convert/RemovePdfEncryption.scala index 4d7a469f..0ea9232b 100644 --- a/modules/convert/src/main/scala/docspell/convert/RemovePdfEncryption.scala +++ b/modules/convert/src/main/scala/docspell/convert/RemovePdfEncryption.scala @@ -12,6 +12,7 @@ import cats.effect._ import fs2.{Chunk, Pipe, Stream} import docspell.common._ +import docspell.logging.Logger import org.apache.pdfbox.pdmodel.PDDocument import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException @@ -36,7 +37,7 @@ object RemovePdfEncryption { .head .flatMap { doc => if (doc.isEncrypted) { - logger.s.debug("Removing protection/encryption from PDF").drain ++ + logger.stream.debug("Removing protection/encryption from PDF").drain ++ Stream.eval(Sync[F].delay(doc.setAllSecurityToBeRemoved(true))).drain ++ toStream[F](doc) } else { @@ -44,7 +45,7 @@ object RemovePdfEncryption { } } .ifEmpty( - logger.s + logger.stream .info( s"None of the passwords helped to read the given PDF!" ) @@ -64,7 +65,8 @@ object RemovePdfEncryption { val log = if (pw.isEmpty) Stream.empty - else logger.s.debug(s"Try opening PDF with password: ${pw.pass.take(2)}***").drain + else + logger.stream.debug(s"Try opening PDF with password: ${pw.pass.take(2)}***").drain in => Stream diff --git a/modules/convert/src/main/scala/docspell/convert/extern/ExternConv.scala b/modules/convert/src/main/scala/docspell/convert/extern/ExternConv.scala index 419c3506..eff5a0fb 100644 --- a/modules/convert/src/main/scala/docspell/convert/extern/ExternConv.scala +++ b/modules/convert/src/main/scala/docspell/convert/extern/ExternConv.scala @@ -14,6 +14,7 @@ import fs2.{Pipe, Stream} import docspell.common._ import docspell.convert.ConversionResult import docspell.convert.ConversionResult.{Handler, successPdf, successPdfTxt} +import docspell.logging.Logger private[extern] object ExternConv { diff --git a/modules/convert/src/main/scala/docspell/convert/extern/OcrMyPdf.scala b/modules/convert/src/main/scala/docspell/convert/extern/OcrMyPdf.scala index 2b8cc529..d133b4dd 100644 --- a/modules/convert/src/main/scala/docspell/convert/extern/OcrMyPdf.scala +++ b/modules/convert/src/main/scala/docspell/convert/extern/OcrMyPdf.scala @@ -13,6 +13,7 @@ import fs2.io.file.Path import docspell.common._ import docspell.convert.ConversionResult import docspell.convert.ConversionResult.Handler +import docspell.logging.Logger object OcrMyPdf { diff --git a/modules/convert/src/main/scala/docspell/convert/extern/Tesseract.scala b/modules/convert/src/main/scala/docspell/convert/extern/Tesseract.scala index 7fd89f66..bd2ae95d 100644 --- a/modules/convert/src/main/scala/docspell/convert/extern/Tesseract.scala +++ b/modules/convert/src/main/scala/docspell/convert/extern/Tesseract.scala @@ -13,6 +13,7 @@ import fs2.io.file.Path import docspell.common._ import docspell.convert.ConversionResult import docspell.convert.ConversionResult.Handler +import docspell.logging.Logger object Tesseract { diff --git a/modules/convert/src/main/scala/docspell/convert/extern/Unoconv.scala b/modules/convert/src/main/scala/docspell/convert/extern/Unoconv.scala index 196f874f..efe0efa7 100644 --- a/modules/convert/src/main/scala/docspell/convert/extern/Unoconv.scala +++ b/modules/convert/src/main/scala/docspell/convert/extern/Unoconv.scala @@ -13,6 +13,7 @@ import fs2.io.file.Path import docspell.common._ import docspell.convert.ConversionResult import docspell.convert.ConversionResult.Handler +import docspell.logging.Logger object Unoconv { diff --git a/modules/convert/src/main/scala/docspell/convert/extern/WkHtmlPdf.scala b/modules/convert/src/main/scala/docspell/convert/extern/WkHtmlPdf.scala index 437978b7..d3ed16c0 100644 --- a/modules/convert/src/main/scala/docspell/convert/extern/WkHtmlPdf.scala +++ b/modules/convert/src/main/scala/docspell/convert/extern/WkHtmlPdf.scala @@ -16,6 +16,7 @@ import fs2.{Chunk, Stream} import docspell.common._ import docspell.convert.ConversionResult.Handler import docspell.convert.{ConversionResult, SanitizeHtml} +import docspell.logging.Logger object WkHtmlPdf { diff --git a/modules/convert/src/test/scala/docspell/convert/ConversionTest.scala b/modules/convert/src/test/scala/docspell/convert/ConversionTest.scala index 8f9f191f..25905afe 100644 --- a/modules/convert/src/test/scala/docspell/convert/ConversionTest.scala +++ b/modules/convert/src/test/scala/docspell/convert/ConversionTest.scala @@ -20,12 +20,13 @@ import docspell.convert.extern.OcrMyPdfConfig import docspell.convert.extern.{TesseractConfig, UnoconvConfig, WkHtmlPdfConfig} import docspell.convert.flexmark.MarkdownConfig import docspell.files.ExampleFiles +import docspell.logging.TestLoggingConfig import munit._ -class ConversionTest extends FunSuite with FileChecks { +class ConversionTest extends FunSuite with FileChecks with TestLoggingConfig { - val logger = Logger.log4s[IO](org.log4s.getLogger) + val logger = docspell.logging.getLogger[IO] val target = File.path(Paths.get("target")) val convertConfig = ConvertConfig( diff --git a/modules/convert/src/test/scala/docspell/convert/RemovePdfEncryptionTest.scala b/modules/convert/src/test/scala/docspell/convert/RemovePdfEncryptionTest.scala index 803f3174..a59465f5 100644 --- a/modules/convert/src/test/scala/docspell/convert/RemovePdfEncryptionTest.scala +++ b/modules/convert/src/test/scala/docspell/convert/RemovePdfEncryptionTest.scala @@ -11,11 +11,15 @@ import fs2.Stream import docspell.common._ import docspell.files.ExampleFiles +import docspell.logging.{Logger, TestLoggingConfig} import munit.CatsEffectSuite -class RemovePdfEncryptionTest extends CatsEffectSuite with FileChecks { - val logger: Logger[IO] = Logger.log4s(org.log4s.getLogger) +class RemovePdfEncryptionTest + extends CatsEffectSuite + with FileChecks + with TestLoggingConfig { + val logger: Logger[IO] = docspell.logging.getLogger[IO] private val protectedPdf = ExampleFiles.secured_protected_test123_pdf.readURL[IO](16 * 1024) diff --git a/modules/convert/src/test/scala/docspell/convert/extern/ExternConvTest.scala b/modules/convert/src/test/scala/docspell/convert/extern/ExternConvTest.scala index 3e5d5991..6f0ab2ab 100644 --- a/modules/convert/src/test/scala/docspell/convert/extern/ExternConvTest.scala +++ b/modules/convert/src/test/scala/docspell/convert/extern/ExternConvTest.scala @@ -16,12 +16,13 @@ import fs2.io.file.Path import docspell.common._ import docspell.convert._ import docspell.files.ExampleFiles +import docspell.logging.TestLoggingConfig import munit._ -class ExternConvTest extends FunSuite with FileChecks { +class ExternConvTest extends FunSuite with FileChecks with TestLoggingConfig { val utf8 = StandardCharsets.UTF_8 - val logger = Logger.log4s[IO](org.log4s.getLogger) + val logger = docspell.logging.getLogger[IO] val target = File.path(Paths.get("target")) test("convert html to pdf") { diff --git a/modules/extract/src/main/scala/docspell/extract/Extraction.scala b/modules/extract/src/main/scala/docspell/extract/Extraction.scala index 6bb9b794..c1a27023 100644 --- a/modules/extract/src/main/scala/docspell/extract/Extraction.scala +++ b/modules/extract/src/main/scala/docspell/extract/Extraction.scala @@ -18,6 +18,7 @@ import docspell.extract.poi.{PoiExtract, PoiType} import docspell.extract.rtf.RtfExtract import docspell.files.ImageSize import docspell.files.TikaMimetype +import docspell.logging.Logger trait Extraction[F[_]] { diff --git a/modules/extract/src/main/scala/docspell/extract/PdfExtract.scala b/modules/extract/src/main/scala/docspell/extract/PdfExtract.scala index c6672553..a9bf2858 100644 --- a/modules/extract/src/main/scala/docspell/extract/PdfExtract.scala +++ b/modules/extract/src/main/scala/docspell/extract/PdfExtract.scala @@ -10,11 +10,12 @@ import cats.effect._ import cats.implicits._ import fs2.Stream -import docspell.common.{Language, Logger} +import docspell.common.Language import docspell.extract.internal.Text import docspell.extract.ocr.{OcrConfig, TextExtract} import docspell.extract.pdfbox.PdfMetaData import docspell.extract.pdfbox.PdfboxExtract +import docspell.logging.Logger object PdfExtract { final case class Result(txt: Text, meta: Option[PdfMetaData]) diff --git a/modules/extract/src/main/scala/docspell/extract/ocr/Ocr.scala b/modules/extract/src/main/scala/docspell/extract/ocr/Ocr.scala index 9e5d6a92..727666a8 100644 --- a/modules/extract/src/main/scala/docspell/extract/ocr/Ocr.scala +++ b/modules/extract/src/main/scala/docspell/extract/ocr/Ocr.scala @@ -11,6 +11,7 @@ import fs2.Stream import fs2.io.file.Path import docspell.common._ +import docspell.logging.Logger object Ocr { diff --git a/modules/extract/src/main/scala/docspell/extract/ocr/TextExtract.scala b/modules/extract/src/main/scala/docspell/extract/ocr/TextExtract.scala index a345da13..a08a2cee 100644 --- a/modules/extract/src/main/scala/docspell/extract/ocr/TextExtract.scala +++ b/modules/extract/src/main/scala/docspell/extract/ocr/TextExtract.scala @@ -12,6 +12,7 @@ import fs2.Stream import docspell.common._ import docspell.extract.internal.Text import docspell.files._ +import docspell.logging.Logger object TextExtract { diff --git a/modules/extract/src/main/scala/docspell/extract/pdfbox/PdfboxPreview.scala b/modules/extract/src/main/scala/docspell/extract/pdfbox/PdfboxPreview.scala index b0ddadc7..32481e49 100644 --- a/modules/extract/src/main/scala/docspell/extract/pdfbox/PdfboxPreview.scala +++ b/modules/extract/src/main/scala/docspell/extract/pdfbox/PdfboxPreview.scala @@ -32,7 +32,7 @@ trait PdfboxPreview[F[_]] { } object PdfboxPreview { - private[this] val logger = org.log4s.getLogger + private[this] val logger = docspell.logging.unsafeLogger def apply[F[_]: Sync](cfg: PreviewConfig): F[PdfboxPreview[F]] = Sync[F].pure(new PdfboxPreview[F] { diff --git a/modules/extract/src/test/scala/docspell/extract/ocr/TextExtractionSuite.scala b/modules/extract/src/test/scala/docspell/extract/ocr/TextExtractionSuite.scala index d64af3b2..71d55ad8 100644 --- a/modules/extract/src/test/scala/docspell/extract/ocr/TextExtractionSuite.scala +++ b/modules/extract/src/test/scala/docspell/extract/ocr/TextExtractionSuite.scala @@ -9,15 +9,15 @@ package docspell.extract.ocr import cats.effect.IO import cats.effect.unsafe.implicits.global -import docspell.common.Logger import docspell.files.TestFiles +import docspell.logging.TestLoggingConfig import munit._ -class TextExtractionSuite extends FunSuite { +class TextExtractionSuite extends FunSuite with TestLoggingConfig { import TestFiles._ - val logger = Logger.log4s[IO](org.log4s.getLogger) + val logger = docspell.logging.getLogger[IO] test("extract english pdf".ignore) { val text = TextExtract diff --git a/modules/extract/src/test/scala/docspell/extract/odf/OdfExtractTest.scala b/modules/extract/src/test/scala/docspell/extract/odf/OdfExtractTest.scala index c2dd5089..f6d36e45 100644 --- a/modules/extract/src/test/scala/docspell/extract/odf/OdfExtractTest.scala +++ b/modules/extract/src/test/scala/docspell/extract/odf/OdfExtractTest.scala @@ -10,10 +10,11 @@ import cats.effect._ import cats.effect.unsafe.implicits.global import docspell.files.ExampleFiles +import docspell.logging.TestLoggingConfig import munit._ -class OdfExtractTest extends FunSuite { +class OdfExtractTest extends FunSuite with TestLoggingConfig { val files = List( ExampleFiles.examples_sample_odt -> 6367, diff --git a/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfMetaDataTest.scala b/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfMetaDataTest.scala index 4d2748ca..24fff241 100644 --- a/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfMetaDataTest.scala +++ b/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfMetaDataTest.scala @@ -6,9 +6,11 @@ package docspell.extract.pdfbox +import docspell.logging.TestLoggingConfig + import munit._ -class PdfMetaDataTest extends FunSuite { +class PdfMetaDataTest extends FunSuite with TestLoggingConfig { test("split keywords on comma") { val md = PdfMetaData.empty.copy(keywords = Some("a,b, c")) diff --git a/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfboxExtractTest.scala b/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfboxExtractTest.scala index 1e46bf69..db47476c 100644 --- a/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfboxExtractTest.scala +++ b/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfboxExtractTest.scala @@ -10,10 +10,11 @@ import cats.effect._ import cats.effect.unsafe.implicits.global import docspell.files.{ExampleFiles, TestFiles} +import docspell.logging.TestLoggingConfig import munit._ -class PdfboxExtractTest extends FunSuite { +class PdfboxExtractTest extends FunSuite with TestLoggingConfig { val textPDFs = List( ExampleFiles.letter_de_pdf -> TestFiles.letterDEText, diff --git a/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfboxPreviewTest.scala b/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfboxPreviewTest.scala index cae614fb..fa8f916a 100644 --- a/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfboxPreviewTest.scala +++ b/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfboxPreviewTest.scala @@ -13,10 +13,11 @@ import fs2.io.file.Files import fs2.io.file.Path import docspell.files.ExampleFiles +import docspell.logging.TestLoggingConfig import munit._ -class PdfboxPreviewTest extends FunSuite { +class PdfboxPreviewTest extends FunSuite with TestLoggingConfig { val testPDFs = List( ExampleFiles.letter_de_pdf -> "7d98be75b239816d6c751b3f3c56118ebf1a4632c43baf35a68a662f9d595ab8", diff --git a/modules/extract/src/test/scala/docspell/extract/poi/PoiExtractTest.scala b/modules/extract/src/test/scala/docspell/extract/poi/PoiExtractTest.scala index 52ef15e5..d0ff4dcc 100644 --- a/modules/extract/src/test/scala/docspell/extract/poi/PoiExtractTest.scala +++ b/modules/extract/src/test/scala/docspell/extract/poi/PoiExtractTest.scala @@ -11,10 +11,11 @@ import cats.effect.unsafe.implicits.global import docspell.common.MimeTypeHint import docspell.files.ExampleFiles +import docspell.logging.TestLoggingConfig import munit._ -class PoiExtractTest extends FunSuite { +class PoiExtractTest extends FunSuite with TestLoggingConfig { val officeFiles = List( ExampleFiles.examples_sample_doc -> 6241, diff --git a/modules/extract/src/test/scala/docspell/extract/rtf/RtfExtractTest.scala b/modules/extract/src/test/scala/docspell/extract/rtf/RtfExtractTest.scala index b277e29e..0cc12aa1 100644 --- a/modules/extract/src/test/scala/docspell/extract/rtf/RtfExtractTest.scala +++ b/modules/extract/src/test/scala/docspell/extract/rtf/RtfExtractTest.scala @@ -7,10 +7,11 @@ package docspell.extract.rtf import docspell.files.ExampleFiles +import docspell.logging.TestLoggingConfig import munit._ -class RtfExtractTest extends FunSuite { +class RtfExtractTest extends FunSuite with TestLoggingConfig { test("extract text from rtf using java input-stream") { val file = ExampleFiles.examples_sample_rtf diff --git a/modules/files/src/test/resources/logback-test.xml b/modules/files/src/test/resources/logback-test.xml deleted file mode 100644 index b1e25720..00000000 --- a/modules/files/src/test/resources/logback-test.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - true - - - level=%-5level thread=%thread logger=%logger{15} message="%replace(%msg){'"', '\\"'}"%n - - - - - - - - diff --git a/modules/fts-client/src/main/scala/docspell/ftsclient/FtsClient.scala b/modules/fts-client/src/main/scala/docspell/ftsclient/FtsClient.scala index 20311dcb..13ee23c3 100644 --- a/modules/fts-client/src/main/scala/docspell/ftsclient/FtsClient.scala +++ b/modules/fts-client/src/main/scala/docspell/ftsclient/FtsClient.scala @@ -11,8 +11,7 @@ import cats.implicits._ import fs2.Stream import docspell.common._ - -import org.log4s.getLogger +import docspell.logging.Logger /** The fts client is the interface for docspell to a fulltext search engine. * @@ -127,7 +126,7 @@ object FtsClient { def none[F[_]: Sync] = new FtsClient[F] { - private[this] val logger = Logger.log4s[F](getLogger) + private[this] val logger = docspell.logging.getLogger[F] def initialize: F[List[FtsMigration[F]]] = Sync[F].pure(Nil) diff --git a/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrFtsClient.scala b/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrFtsClient.scala index 75316900..16f7bd13 100644 --- a/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrFtsClient.scala +++ b/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrFtsClient.scala @@ -12,10 +12,10 @@ import fs2.Stream import docspell.common._ import docspell.ftsclient._ +import docspell.logging.Logger import org.http4s.client.Client -import org.http4s.client.middleware.Logger -import org.log4s.getLogger +import org.http4s.client.middleware.{Logger => Http4sLogger} final class SolrFtsClient[F[_]: Async]( solrUpdate: SolrUpdate[F], @@ -81,7 +81,6 @@ final class SolrFtsClient[F[_]: Async]( } object SolrFtsClient { - private[this] val logger = getLogger def apply[F[_]: Async]( cfg: SolrConfig, @@ -100,11 +99,13 @@ object SolrFtsClient { private def loggingMiddleware[F[_]: Async]( cfg: SolrConfig, client: Client[F] - ): Client[F] = - Logger( + ): Client[F] = { + val delegate = docspell.logging.getLogger[F] + Http4sLogger( logHeaders = true, logBody = cfg.logVerbose, - logAction = Some((msg: String) => Sync[F].delay(logger.trace(msg))) + logAction = Some((msg: String) => delegate.trace(msg)) )(client) + } } diff --git a/modules/joex/src/main/resources/logback.xml b/modules/joex/src/main/resources/logback.xml deleted file mode 100644 index df73c8c6..00000000 --- a/modules/joex/src/main/resources/logback.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - true - - - level=%-5level thread=%thread logger=%logger{15} message="%replace(%msg){'"', '\\"'}"%n - - - - - - - - diff --git a/modules/joex/src/main/resources/reference.conf b/modules/joex/src/main/resources/reference.conf index 80348f57..58347c8c 100644 --- a/modules/joex/src/main/resources/reference.conf +++ b/modules/joex/src/main/resources/reference.conf @@ -18,6 +18,17 @@ docspell.joex { port = 7878 } + # Configures logging + logging { + # The format for the log messages. Can be one of: + # Json, Logfmt, Fancy or Plain + format = "Plain" + + # The minimum level to log. From lowest to highest: + # Trace, Debug, Info, Warn, Error + minimum-level = "Info" + } + # The database connection. # # It must be the same connection as the rest server is using. diff --git a/modules/joex/src/main/scala/docspell/joex/Config.scala b/modules/joex/src/main/scala/docspell/joex/Config.scala index db9309f0..549b24ca 100644 --- a/modules/joex/src/main/scala/docspell/joex/Config.scala +++ b/modules/joex/src/main/scala/docspell/joex/Config.scala @@ -21,12 +21,14 @@ import docspell.joex.hk.HouseKeepingConfig import docspell.joex.routes.InternalHeader import docspell.joex.scheduler.{PeriodicSchedulerConfig, SchedulerConfig} import docspell.joex.updatecheck.UpdateCheckConfig +import docspell.logging.LogConfig import docspell.pubsub.naive.PubSubConfig import docspell.store.JdbcConfig case class Config( appId: Ident, baseUrl: LenientUri, + logging: LogConfig, bind: Config.Bind, jdbc: JdbcConfig, scheduler: SchedulerConfig, diff --git a/modules/joex/src/main/scala/docspell/joex/ConfigFile.scala b/modules/joex/src/main/scala/docspell/joex/ConfigFile.scala index 41541480..a3663030 100644 --- a/modules/joex/src/main/scala/docspell/joex/ConfigFile.scala +++ b/modules/joex/src/main/scala/docspell/joex/ConfigFile.scala @@ -8,7 +8,6 @@ package docspell.joex import cats.effect.Async -import docspell.common.Logger import docspell.config.Implicits._ import docspell.config.{ConfigFactory, Validation} import docspell.joex.scheduler.CountingScheme @@ -23,7 +22,7 @@ object ConfigFile { import Implicits._ def loadConfig[F[_]: Async](args: List[String]): F[Config] = { - val logger = Logger.log4s[F](org.log4s.getLogger) + val logger = docspell.logging.getLogger[F] ConfigFactory .default[F, Config](logger, "docspell.joex")(args, validate) } diff --git a/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala b/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala index 5c4a01a1..ce28e8d3 100644 --- a/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala +++ b/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala @@ -132,10 +132,8 @@ object JoexAppImpl extends MailAddressCodec { ): Resource[F, JoexApp[F]] = for { pstore <- PeriodicTaskStore.create(store) - pubSubT = PubSubT( - pubSub, - Logger.log4s(org.log4s.getLogger(s"joex-${cfg.appId.id}")) - ) + joexLogger = docspell.logging.getLogger[F](s"joex-${cfg.appId.id}") + pubSubT = PubSubT(pubSub, joexLogger) javaEmil = JavaMailEmil(Settings.defaultSettings.copy(debug = cfg.mailDebug)) notificationMod <- Resource.eval( diff --git a/modules/joex/src/main/scala/docspell/joex/Main.scala b/modules/joex/src/main/scala/docspell/joex/Main.scala index 6f96e1e0..7866d6d1 100644 --- a/modules/joex/src/main/scala/docspell/joex/Main.scala +++ b/modules/joex/src/main/scala/docspell/joex/Main.scala @@ -9,12 +9,12 @@ package docspell.joex import cats.effect._ import docspell.common._ - -import org.log4s.getLogger +import docspell.logging.Logger +import docspell.logging.impl.ScribeConfigure object Main extends IOApp { - private val logger: Logger[IO] = Logger.log4s[IO](getLogger) + private val logger: Logger[IO] = docspell.logging.getLogger[IO] private val connectEC = ThreadFactories.fixed[IO](5, ThreadFactories.ofName("docspell-joex-dbconnect")) @@ -22,6 +22,7 @@ object Main extends IOApp { def run(args: List[String]): IO[ExitCode] = for { cfg <- ConfigFile.loadConfig[IO](args) + _ <- ScribeConfigure.configure[IO](cfg.logging) banner = Banner( "JOEX", BuildInfo.version, diff --git a/modules/joex/src/main/scala/docspell/joex/analysis/RegexNerFile.scala b/modules/joex/src/main/scala/docspell/joex/analysis/RegexNerFile.scala index dddabebf..2f46234e 100644 --- a/modules/joex/src/main/scala/docspell/joex/analysis/RegexNerFile.scala +++ b/modules/joex/src/main/scala/docspell/joex/analysis/RegexNerFile.scala @@ -12,7 +12,6 @@ import cats.implicits._ import fs2.io.file.Path import docspell.common._ -import docspell.common.syntax.all._ import docspell.store.Store import docspell.store.queries.QCollective import docspell.store.records.REquipment @@ -20,7 +19,6 @@ import docspell.store.records.ROrganization import docspell.store.records.RPerson import io.circe.syntax._ -import org.log4s.getLogger /** Maintains a custom regex-ner file per collective for stanford's regexner annotator. */ trait RegexNerFile[F[_]] { @@ -30,7 +28,6 @@ trait RegexNerFile[F[_]] { } object RegexNerFile { - private[this] val logger = getLogger case class Config(maxEntries: Int, directory: Path, minTime: Duration) @@ -49,6 +46,8 @@ object RegexNerFile { writer: Semaphore[F] // TODO allow parallelism per collective ) extends RegexNerFile[F] { + private[this] val logger = docspell.logging.getLogger[F] + def makeFile(collective: Ident): F[Option[Path]] = if (cfg.maxEntries > 0) doMakeFile(collective) else (None: Option[Path]).pure[F] @@ -61,7 +60,7 @@ object RegexNerFile { case Some(nf) => val dur = Duration.between(nf.creation, now) if (dur > cfg.minTime) - logger.fdebug( + logger.debug( s"Cache time elapsed ($dur > ${cfg.minTime}). Check for new state." ) *> updateFile( collective, @@ -89,12 +88,12 @@ object RegexNerFile { case Some(cur) => val nerf = if (cur.updated == lup) - logger.fdebug(s"No state change detected.") *> updateTimestamp( + logger.debug(s"No state change detected.") *> updateTimestamp( cur, now ) *> cur.pure[F] else - logger.fdebug( + logger.debug( s"There have been state changes for collective '${collective.id}'. Reload NER file." ) *> createFile(lup, collective, now) nerf.map(_.nerFilePath(cfg.directory).some) @@ -126,7 +125,7 @@ object RegexNerFile { writer.permit.use(_ => for { jsonFile <- Sync[F].pure(nf.jsonFilePath(cfg.directory)) - _ <- logger.fdebug( + _ <- logger.debug( s"Writing custom NER file for collective '${collective.id}'" ) _ <- jsonFile.parent match { @@ -139,7 +138,7 @@ object RegexNerFile { ) for { - _ <- logger.finfo(s"Generating custom NER file for collective '${collective.id}'") + _ <- logger.info(s"Generating custom NER file for collective '${collective.id}'") names <- store.transact(QCollective.allNames(collective, cfg.maxEntries)) nerFile = NerFile(collective, lastUpdate, now) _ <- update(nerFile, NerFile.mkNerConfig(names)) diff --git a/modules/joex/src/main/scala/docspell/joex/fts/FtsContext.scala b/modules/joex/src/main/scala/docspell/joex/fts/FtsContext.scala index e2aca8c0..8e7f133b 100644 --- a/modules/joex/src/main/scala/docspell/joex/fts/FtsContext.scala +++ b/modules/joex/src/main/scala/docspell/joex/fts/FtsContext.scala @@ -7,10 +7,10 @@ package docspell.joex.fts import docspell.backend.fulltext.CreateIndex -import docspell.common.Logger import docspell.ftsclient.FtsClient import docspell.joex.Config import docspell.joex.scheduler.Context +import docspell.logging.Logger import docspell.store.Store case class FtsContext[F[_]]( diff --git a/modules/joex/src/main/scala/docspell/joex/fts/FtsWork.scala b/modules/joex/src/main/scala/docspell/joex/fts/FtsWork.scala index aeae553c..1184c0a3 100644 --- a/modules/joex/src/main/scala/docspell/joex/fts/FtsWork.scala +++ b/modules/joex/src/main/scala/docspell/joex/fts/FtsWork.scala @@ -15,6 +15,7 @@ import docspell.common._ import docspell.ftsclient._ import docspell.joex.Config import docspell.joex.scheduler.Context +import docspell.logging.Logger object FtsWork { import syntax._ diff --git a/modules/joex/src/main/scala/docspell/joex/fts/Migration.scala b/modules/joex/src/main/scala/docspell/joex/fts/Migration.scala index b0b92c35..2788e606 100644 --- a/modules/joex/src/main/scala/docspell/joex/fts/Migration.scala +++ b/modules/joex/src/main/scala/docspell/joex/fts/Migration.scala @@ -15,6 +15,7 @@ import docspell.backend.fulltext.CreateIndex import docspell.common._ import docspell.ftsclient._ import docspell.joex.Config +import docspell.logging.Logger import docspell.store.Store /** Migrating the index from the previous version to this version. diff --git a/modules/joex/src/main/scala/docspell/joex/hk/CheckNodesTask.scala b/modules/joex/src/main/scala/docspell/joex/hk/CheckNodesTask.scala index ecba752b..8e0bdb1c 100644 --- a/modules/joex/src/main/scala/docspell/joex/hk/CheckNodesTask.scala +++ b/modules/joex/src/main/scala/docspell/joex/hk/CheckNodesTask.scala @@ -11,6 +11,7 @@ import cats.implicits._ import docspell.common._ import docspell.joex.scheduler.{Context, Task} +import docspell.logging.Logger import docspell.store.records._ import org.http4s.blaze.client.BlazeClientBuilder diff --git a/modules/joex/src/main/scala/docspell/joex/learn/Classify.scala b/modules/joex/src/main/scala/docspell/joex/learn/Classify.scala index e208c79d..1d61f4fd 100644 --- a/modules/joex/src/main/scala/docspell/joex/learn/Classify.scala +++ b/modules/joex/src/main/scala/docspell/joex/learn/Classify.scala @@ -14,6 +14,7 @@ import fs2.io.file.Path import docspell.analysis.classifier.{ClassifierModel, TextClassifier} import docspell.common._ +import docspell.logging.Logger import docspell.store.Store import docspell.store.records.RClassifierModel diff --git a/modules/joex/src/main/scala/docspell/joex/learn/LearnClassifierTask.scala b/modules/joex/src/main/scala/docspell/joex/learn/LearnClassifierTask.scala index 92fbf401..129afc5a 100644 --- a/modules/joex/src/main/scala/docspell/joex/learn/LearnClassifierTask.scala +++ b/modules/joex/src/main/scala/docspell/joex/learn/LearnClassifierTask.scala @@ -15,6 +15,7 @@ import docspell.backend.ops.OCollective import docspell.common._ import docspell.joex.Config import docspell.joex.scheduler._ +import docspell.logging.Logger import docspell.store.records.{RClassifierModel, RClassifierSetting} object LearnClassifierTask { diff --git a/modules/joex/src/main/scala/docspell/joex/learn/StoreClassifierModel.scala b/modules/joex/src/main/scala/docspell/joex/learn/StoreClassifierModel.scala index af614e8b..e0e7eabc 100644 --- a/modules/joex/src/main/scala/docspell/joex/learn/StoreClassifierModel.scala +++ b/modules/joex/src/main/scala/docspell/joex/learn/StoreClassifierModel.scala @@ -13,6 +13,7 @@ import fs2.io.file.Files import docspell.analysis.classifier.ClassifierModel import docspell.common._ import docspell.joex.scheduler._ +import docspell.logging.Logger import docspell.store.Store import docspell.store.records.RClassifierModel diff --git a/modules/joex/src/main/scala/docspell/joex/mail/ReadMail.scala b/modules/joex/src/main/scala/docspell/joex/mail/ReadMail.scala index 3d9aa623..9b2db148 100644 --- a/modules/joex/src/main/scala/docspell/joex/mail/ReadMail.scala +++ b/modules/joex/src/main/scala/docspell/joex/mail/ReadMail.scala @@ -11,6 +11,7 @@ import cats.implicits._ import fs2.{Pipe, Stream} import docspell.common._ +import docspell.logging.Logger import docspell.store.syntax.MimeTypes._ import emil.javamail.syntax._ diff --git a/modules/joex/src/main/scala/docspell/joex/notify/TaskOperations.scala b/modules/joex/src/main/scala/docspell/joex/notify/TaskOperations.scala index d57ba05e..da91c374 100644 --- a/modules/joex/src/main/scala/docspell/joex/notify/TaskOperations.scala +++ b/modules/joex/src/main/scala/docspell/joex/notify/TaskOperations.scala @@ -12,6 +12,7 @@ import cats.implicits._ import docspell.backend.ops.ONotification import docspell.common._ +import docspell.logging.Logger import docspell.notification.api.ChannelRef import docspell.notification.api.Event import docspell.notification.api.EventContext diff --git a/modules/joex/src/main/scala/docspell/joex/process/CrossCheckProposals.scala b/modules/joex/src/main/scala/docspell/joex/process/CrossCheckProposals.scala index cccd2b90..3cd9d3ad 100644 --- a/modules/joex/src/main/scala/docspell/joex/process/CrossCheckProposals.scala +++ b/modules/joex/src/main/scala/docspell/joex/process/CrossCheckProposals.scala @@ -13,6 +13,7 @@ import cats.implicits._ import docspell.common._ import docspell.joex.scheduler.Task +import docspell.logging.Logger /** After candidates have been determined, the set is reduced by doing some cross checks. * For example: if a organization is suggested as correspondent, the correspondent person diff --git a/modules/joex/src/main/scala/docspell/joex/process/ExtractArchive.scala b/modules/joex/src/main/scala/docspell/joex/process/ExtractArchive.scala index ef98b43d..17f90b59 100644 --- a/modules/joex/src/main/scala/docspell/joex/process/ExtractArchive.scala +++ b/modules/joex/src/main/scala/docspell/joex/process/ExtractArchive.scala @@ -93,7 +93,7 @@ object ExtractArchive { archive: Option[RAttachmentArchive] )(ra: RAttachment, pos: Int, mime: MimeType): F[Extracted] = mime match { - case MimeType.ZipMatch(_) if ra.name.exists(_.endsWith(".zip")) => + case MimeType.ZipMatch(_) if ra.name.exists(_.toLowerCase.endsWith(".zip")) => ctx.logger.info(s"Extracting zip archive ${ra.name.getOrElse("")}.") *> extractZip(ctx, archive)(ra, pos) .flatMap(cleanupParents(ctx, ra, archive)) diff --git a/modules/joex/src/main/scala/docspell/joex/process/ReProcessItem.scala b/modules/joex/src/main/scala/docspell/joex/process/ReProcessItem.scala index b35815f8..15aa939d 100644 --- a/modules/joex/src/main/scala/docspell/joex/process/ReProcessItem.scala +++ b/modules/joex/src/main/scala/docspell/joex/process/ReProcessItem.scala @@ -49,7 +49,7 @@ object ReProcessItem { private def contains[F[_]](ctx: Context[F, Args]): RAttachment => Boolean = { val selection = ctx.args.attachments.toSet - if (selection.isEmpty) (_ => true) + if (selection.isEmpty) _ => true else ra => selection.contains(ra.id) } diff --git a/modules/joex/src/main/scala/docspell/joex/process/TestTasks.scala b/modules/joex/src/main/scala/docspell/joex/process/TestTasks.scala deleted file mode 100644 index 95eb3423..00000000 --- a/modules/joex/src/main/scala/docspell/joex/process/TestTasks.scala +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 Eike K. & Contributors - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package docspell.joex.process - -import cats.effect.Sync -import cats.implicits._ - -import docspell.common.ProcessItemArgs -import docspell.common.syntax.all._ -import docspell.joex.scheduler.Task - -import org.log4s._ - -object TestTasks { - private[this] val logger = getLogger - - def success[F[_]]: Task[F, ProcessItemArgs, Unit] = - Task(ctx => ctx.logger.info(s"Running task now: ${ctx.args}")) - - def failing[F[_]: Sync]: Task[F, ProcessItemArgs, Unit] = - Task { ctx => - ctx.logger - .info(s"Failing the task run :(") - .map(_ => sys.error("Oh, cannot extract gold from this document")) - } - - def longRunning[F[_]: Sync]: Task[F, ProcessItemArgs, Unit] = - Task { ctx => - logger.fwarn(s"${Thread.currentThread()} From executing long running task") >> - ctx.logger.info(s"${Thread.currentThread()} Running task now: ${ctx.args}") >> - sleep(2400) >> - ctx.logger.debug("doing things") >> - sleep(2400) >> - ctx.logger.debug("doing more things") >> - sleep(2400) >> - ctx.logger.info("doing more things") - } - - private def sleep[F[_]: Sync](ms: Long): F[Unit] = - Sync[F].delay(Thread.sleep(ms)) -} diff --git a/modules/joex/src/main/scala/docspell/joex/scanmailbox/ScanMailboxTask.scala b/modules/joex/src/main/scala/docspell/joex/scanmailbox/ScanMailboxTask.scala index c866490c..869362e3 100644 --- a/modules/joex/src/main/scala/docspell/joex/scanmailbox/ScanMailboxTask.scala +++ b/modules/joex/src/main/scala/docspell/joex/scanmailbox/ScanMailboxTask.scala @@ -17,6 +17,7 @@ import docspell.backend.ops.{OJoex, OUpload} import docspell.common._ import docspell.joex.Config import docspell.joex.scheduler.{Context, Task} +import docspell.logging.Logger import docspell.store.queries.QOrganization import docspell.store.records._ diff --git a/modules/joex/src/main/scala/docspell/joex/scheduler/Context.scala b/modules/joex/src/main/scala/docspell/joex/scheduler/Context.scala index 2989d887..2fb2a529 100644 --- a/modules/joex/src/main/scala/docspell/joex/scheduler/Context.scala +++ b/modules/joex/src/main/scala/docspell/joex/scheduler/Context.scala @@ -11,12 +11,10 @@ import cats.implicits._ import cats.{Applicative, Functor} import docspell.common._ -import docspell.common.syntax.all._ +import docspell.logging.Logger import docspell.store.Store import docspell.store.records.RJob -import org.log4s.{Logger => _, _} - trait Context[F[_], A] { self => def jobId: Ident @@ -42,7 +40,6 @@ trait Context[F[_], A] { self => } object Context { - private[this] val log = getLogger def create[F[_]: Async, A]( jobId: Ident, @@ -59,13 +56,15 @@ object Context { config: SchedulerConfig, logSink: LogSink[F], store: Store[F] - ): F[Context[F, A]] = + ): F[Context[F, A]] = { + val log = docspell.logging.getLogger[F] for { - _ <- log.ftrace("Creating logger for task run") + _ <- log.trace("Creating logger for task run") logger <- QueueLogger(job.id, job.info, config.logBufferSize, logSink) - _ <- log.ftrace("Logger created, instantiating context") + _ <- log.trace("Logger created, instantiating context") ctx = create[F, A](job.id, arg, config, logger, store) } yield ctx + } final private class ContextImpl[F[_]: Functor, A]( val args: A, diff --git a/modules/joex/src/main/scala/docspell/joex/scheduler/LogSink.scala b/modules/joex/src/main/scala/docspell/joex/scheduler/LogSink.scala index 1117e780..bf01a050 100644 --- a/modules/joex/src/main/scala/docspell/joex/scheduler/LogSink.scala +++ b/modules/joex/src/main/scala/docspell/joex/scheduler/LogSink.scala @@ -11,12 +11,10 @@ import cats.implicits._ import fs2.Pipe import docspell.common._ -import docspell.common.syntax.all._ +import docspell.logging import docspell.store.Store import docspell.store.records.RJobLog -import org.log4s.{LogLevel => _, _} - trait LogSink[F[_]] { def receive: Pipe[F, LogEvent, Unit] @@ -24,29 +22,33 @@ trait LogSink[F[_]] { } object LogSink { - private[this] val logger = getLogger def apply[F[_]](sink: Pipe[F, LogEvent, Unit]): LogSink[F] = new LogSink[F] { val receive = sink } - def logInternal[F[_]: Sync](e: LogEvent): F[Unit] = + def logInternal[F[_]: Sync](e: LogEvent): F[Unit] = { + val logger = docspell.logging.getLogger[F] + val addData: logging.LogEvent => logging.LogEvent = + _.data("jobId", e.jobId).data("jobInfo", e.jobInfo) + e.level match { case LogLevel.Info => - logger.finfo(e.logLine) + logger.infoWith(e.logLine)(addData) case LogLevel.Debug => - logger.fdebug(e.logLine) + logger.debugWith(e.logLine)(addData) case LogLevel.Warn => - logger.fwarn(e.logLine) + logger.warnWith(e.logLine)(addData) case LogLevel.Error => e.ex match { case Some(exc) => - logger.ferror(exc)(e.logLine) + logger.errorWith(e.logLine)(addData.andThen(_.addError(exc))) case None => - logger.ferror(e.logLine) + logger.errorWith(e.logLine)(addData) } } + } def printer[F[_]: Sync]: LogSink[F] = LogSink(_.evalMap(e => logInternal(e))) diff --git a/modules/joex/src/main/scala/docspell/joex/scheduler/PeriodicSchedulerImpl.scala b/modules/joex/src/main/scala/docspell/joex/scheduler/PeriodicSchedulerImpl.scala index b44ee6c4..39761a74 100644 --- a/modules/joex/src/main/scala/docspell/joex/scheduler/PeriodicSchedulerImpl.scala +++ b/modules/joex/src/main/scala/docspell/joex/scheduler/PeriodicSchedulerImpl.scala @@ -13,13 +13,11 @@ import fs2.concurrent.SignallingRef import docspell.backend.ops.OJoex import docspell.common._ -import docspell.common.syntax.all._ import docspell.joex.scheduler.PeriodicSchedulerImpl.State import docspell.store.queue._ import docspell.store.records.RPeriodicTask import eu.timepit.fs2cron.calev.CalevScheduler -import org.log4s.getLogger final class PeriodicSchedulerImpl[F[_]: Async]( val config: PeriodicSchedulerConfig, @@ -30,10 +28,10 @@ final class PeriodicSchedulerImpl[F[_]: Async]( waiter: SignallingRef[F, Boolean], state: SignallingRef[F, State[F]] ) extends PeriodicScheduler[F] { - private[this] val logger = getLogger + private[this] val logger = docspell.logging.getLogger[F] def start: Stream[F, Nothing] = - logger.sinfo("Starting periodic scheduler") ++ + logger.stream.info("Starting periodic scheduler").drain ++ mainLoop def shutdown: F[Unit] = @@ -43,7 +41,7 @@ final class PeriodicSchedulerImpl[F[_]: Async]( Async[F].start( Stream .awakeEvery[F](config.wakeupPeriod.toScala) - .evalMap(_ => logger.fdebug("Periodic awake reached") *> notifyChange) + .evalMap(_ => logger.debug("Periodic awake reached") *> notifyChange) .compile .drain ) @@ -62,22 +60,22 @@ final class PeriodicSchedulerImpl[F[_]: Async]( def mainLoop: Stream[F, Nothing] = { val body: F[Boolean] = for { - _ <- logger.fdebug(s"Going into main loop") + _ <- logger.debug(s"Going into main loop") now <- Timestamp.current[F] - _ <- logger.fdebug(s"Looking for next periodic task") + _ <- logger.debug(s"Looking for next periodic task") go <- logThrow("Error getting next task")( store .takeNext(config.name, None) .use { case Marked.Found(pj) => logger - .fdebug(s"Found periodic task '${pj.subject}/${pj.timer.asString}'") *> + .debug(s"Found periodic task '${pj.subject}/${pj.timer.asString}'") *> (if (isTriggered(pj, now)) submitJob(pj) else scheduleNotify(pj).map(_ => false)) case Marked.NotFound => - logger.fdebug("No periodic task found") *> false.pure[F] + logger.debug("No periodic task found") *> false.pure[F] case Marked.NotMarkable => - logger.fdebug("Periodic job cannot be marked. Trying again.") *> true + logger.debug("Periodic job cannot be marked. Trying again.") *> true .pure[F] } ) @@ -86,7 +84,7 @@ final class PeriodicSchedulerImpl[F[_]: Async]( Stream .eval(state.get.map(_.shutdownRequest)) .evalTap( - if (_) logger.finfo[F]("Stopping main loop due to shutdown request.") + if (_) logger.info("Stopping main loop due to shutdown request.") else ().pure[F] ) .flatMap(if (_) Stream.empty else Stream.eval(cancelNotify *> body)) @@ -94,9 +92,9 @@ final class PeriodicSchedulerImpl[F[_]: Async]( case true => mainLoop case false => - logger.sdebug(s"Waiting for notify") ++ + logger.stream.debug(s"Waiting for notify").drain ++ waiter.discrete.take(2).drain ++ - logger.sdebug(s"Notify signal, going into main loop") ++ + logger.stream.debug(s"Notify signal, going into main loop").drain ++ mainLoop } } @@ -109,12 +107,12 @@ final class PeriodicSchedulerImpl[F[_]: Async]( .findNonFinalJob(pj.id) .flatMap { case Some(job) => - logger.finfo[F]( + logger.info( s"There is already a job with non-final state '${job.state}' in the queue" ) *> scheduleNotify(pj) *> false.pure[F] case None => - logger.finfo[F](s"Submitting job for periodic task '${pj.task.id}'") *> + logger.info(s"Submitting job for periodic task '${pj.task.id}'") *> pj.toJob.flatMap(queue.insert) *> notifyJoex *> true.pure[F] } @@ -125,7 +123,7 @@ final class PeriodicSchedulerImpl[F[_]: Async]( Timestamp .current[F] .flatMap(now => - logger.fdebug( + logger.debug( s"Scheduling next notify for timer ${pj.timer.asString} -> ${pj.timer.nextElapse(now.toUtcDateTime)}" ) ) *> @@ -153,13 +151,13 @@ final class PeriodicSchedulerImpl[F[_]: Async]( private def logError(msg: => String)(fa: F[Unit]): F[Unit] = fa.attempt.flatMap { case Right(_) => ().pure[F] - case Left(ex) => logger.ferror(ex)(msg).map(_ => ()) + case Left(ex) => logger.error(ex)(msg).map(_ => ()) } private def logThrow[A](msg: => String)(fa: F[A]): F[A] = fa.attempt.flatMap { case r @ Right(_) => (r: Either[Throwable, A]).pure[F] - case l @ Left(ex) => logger.ferror(ex)(msg).map(_ => (l: Either[Throwable, A])) + case l @ Left(ex) => logger.error(ex)(msg).map(_ => (l: Either[Throwable, A])) }.rethrow } diff --git a/modules/joex/src/main/scala/docspell/joex/scheduler/QueueLogger.scala b/modules/joex/src/main/scala/docspell/joex/scheduler/QueueLogger.scala index b1d7067b..357a1e83 100644 --- a/modules/joex/src/main/scala/docspell/joex/scheduler/QueueLogger.scala +++ b/modules/joex/src/main/scala/docspell/joex/scheduler/QueueLogger.scala @@ -12,6 +12,8 @@ import cats.implicits._ import fs2.Stream import docspell.common._ +import docspell.logging +import docspell.logging.{Level, Logger} object QueueLogger { @@ -21,26 +23,20 @@ object QueueLogger { q: Queue[F, LogEvent] ): Logger[F] = new Logger[F] { - def trace(msg: => String): F[Unit] = - LogEvent.create[F](jobId, jobInfo, LogLevel.Debug, msg).flatMap(q.offer) - def debug(msg: => String): F[Unit] = - LogEvent.create[F](jobId, jobInfo, LogLevel.Debug, msg).flatMap(q.offer) - - def info(msg: => String): F[Unit] = - LogEvent.create[F](jobId, jobInfo, LogLevel.Info, msg).flatMap(q.offer) - - def warn(msg: => String): F[Unit] = - LogEvent.create[F](jobId, jobInfo, LogLevel.Warn, msg).flatMap(q.offer) - - def error(ex: Throwable)(msg: => String): F[Unit] = + def log(logEvent: logging.LogEvent) = LogEvent - .create[F](jobId, jobInfo, LogLevel.Error, msg) - .map(le => le.copy(ex = Some(ex))) - .flatMap(q.offer) + .create[F](jobId, jobInfo, level2Level(logEvent.level), logEvent.msg()) + .flatMap { ev => + val event = + logEvent.findErrors.headOption + .map(ex => ev.copy(ex = Some(ex))) + .getOrElse(ev) - def error(msg: => String): F[Unit] = - LogEvent.create[F](jobId, jobInfo, LogLevel.Error, msg).flatMap(q.offer) + q.offer(event) + } + + def asUnsafe = Logger.off } def apply[F[_]: Async]( @@ -57,4 +53,6 @@ object QueueLogger { ) } yield log + private def level2Level(level: Level): LogLevel = + LogLevel.fromLevel(level) } diff --git a/modules/joex/src/main/scala/docspell/joex/scheduler/SchedulerImpl.scala b/modules/joex/src/main/scala/docspell/joex/scheduler/SchedulerImpl.scala index d30c43eb..be83f9d6 100644 --- a/modules/joex/src/main/scala/docspell/joex/scheduler/SchedulerImpl.scala +++ b/modules/joex/src/main/scala/docspell/joex/scheduler/SchedulerImpl.scala @@ -15,7 +15,6 @@ import fs2.concurrent.SignallingRef import docspell.backend.msg.JobDone import docspell.common._ -import docspell.common.syntax.all._ import docspell.joex.scheduler.SchedulerImpl._ import docspell.notification.api.Event import docspell.notification.api.EventSink @@ -26,7 +25,6 @@ import docspell.store.queue.JobQueue import docspell.store.records.RJob import io.circe.Json -import org.log4s.getLogger final class SchedulerImpl[F[_]: Async]( val config: SchedulerConfig, @@ -41,7 +39,7 @@ final class SchedulerImpl[F[_]: Async]( permits: Semaphore[F] ) extends Scheduler[F] { - private[this] val logger = getLogger + private[this] val logger = docspell.logging.getLogger[F] /** On startup, get all jobs in state running from this scheduler and put them into * waiting state, so they get picked up again. @@ -53,7 +51,7 @@ final class SchedulerImpl[F[_]: Async]( Async[F].start( Stream .awakeEvery[F](config.wakeupPeriod.toScala) - .evalMap(_ => logger.fdebug("Periodic awake reached") *> notifyChange) + .evalMap(_ => logger.debug("Periodic awake reached") *> notifyChange) .compile .drain ) @@ -62,7 +60,7 @@ final class SchedulerImpl[F[_]: Async]( state.get.flatMap(s => QJob.findAll(s.getRunning, store)) def requestCancel(jobId: Ident): F[Boolean] = - logger.finfo(s"Scheduler requested to cancel job: ${jobId.id}") *> + logger.info(s"Scheduler requested to cancel job: ${jobId.id}") *> state.get.flatMap(_.cancelRequest(jobId) match { case Some(ct) => ct.map(_ => true) case None => @@ -74,7 +72,7 @@ final class SchedulerImpl[F[_]: Async]( ) } yield true) .getOrElseF( - logger.fwarn(s"Job ${jobId.id} not found, cannot cancel.").map(_ => false) + logger.warn(s"Job ${jobId.id} not found, cannot cancel.").map(_ => false) ) }) @@ -90,16 +88,16 @@ final class SchedulerImpl[F[_]: Async]( val wait = Stream .eval(runShutdown) - .evalMap(_ => logger.finfo("Scheduler is shutting down now.")) + .evalMap(_ => logger.info("Scheduler is shutting down now.")) .flatMap(_ => Stream.eval(state.get) ++ Stream .suspend(state.discrete.takeWhile(_.getRunning.nonEmpty)) ) .flatMap { state => - if (state.getRunning.isEmpty) Stream.eval(logger.finfo("No jobs running.")) + if (state.getRunning.isEmpty) Stream.eval(logger.info("No jobs running.")) else Stream.eval( - logger.finfo(s"Waiting for ${state.getRunning.size} jobs to finish.") + logger.info(s"Waiting for ${state.getRunning.size} jobs to finish.") ) ++ Stream.emit(state) } @@ -108,35 +106,35 @@ final class SchedulerImpl[F[_]: Async]( } def start: Stream[F, Nothing] = - logger.sinfo("Starting scheduler") ++ + logger.stream.info("Starting scheduler").drain ++ mainLoop def mainLoop: Stream[F, Nothing] = { val body: F[Boolean] = for { _ <- permits.available.flatMap(a => - logger.fdebug(s"Try to acquire permit ($a free)") + logger.debug(s"Try to acquire permit ($a free)") ) _ <- permits.acquire - _ <- logger.fdebug("New permit acquired") + _ <- logger.debug("New permit acquired") down <- state.get.map(_.shutdownRequest) rjob <- if (down) - logger.finfo("") *> permits.release *> (None: Option[RJob]).pure[F] + logger.info("") *> permits.release *> (None: Option[RJob]).pure[F] else queue.nextJob( group => state.modify(_.nextPrio(group, config.countingScheme)), config.name, config.retryDelay ) - _ <- logger.fdebug(s"Next job found: ${rjob.map(_.info)}") + _ <- logger.debug(s"Next job found: ${rjob.map(_.info)}") _ <- rjob.map(execute).getOrElse(permits.release) } yield rjob.isDefined Stream .eval(state.get.map(_.shutdownRequest)) .evalTap( - if (_) logger.finfo[F]("Stopping main loop due to shutdown request.") + if (_) logger.info("Stopping main loop due to shutdown request.") else ().pure[F] ) .flatMap(if (_) Stream.empty else Stream.eval(body)) @@ -144,9 +142,9 @@ final class SchedulerImpl[F[_]: Async]( case true => mainLoop case false => - logger.sdebug(s"Waiting for notify") ++ + logger.stream.debug(s"Waiting for notify").drain ++ waiter.discrete.take(2).drain ++ - logger.sdebug(s"Notify signal, going into main loop") ++ + logger.stream.debug(s"Notify signal, going into main loop").drain ++ mainLoop } } @@ -161,17 +159,17 @@ final class SchedulerImpl[F[_]: Async]( task match { case Left(err) => - logger.ferror(s"Unable to run cancellation task for job ${job.info}: $err") + logger.error(s"Unable to run cancellation task for job ${job.info}: $err") case Right(t) => for { _ <- - logger.fdebug(s"Creating context for job ${job.info} to run cancellation $t") + logger.debug(s"Creating context for job ${job.info} to run cancellation $t") ctx <- Context[F, String](job, job.args, config, logSink, store) _ <- t.onCancel.run(ctx) _ <- state.modify(_.markCancelled(job)) _ <- onFinish(job, Json.Null, JobState.Cancelled) _ <- ctx.logger.warn("Job has been cancelled.") - _ <- logger.fdebug(s"Job ${job.info} has been cancelled.") + _ <- logger.debug(s"Job ${job.info} has been cancelled.") } yield () } } @@ -186,10 +184,10 @@ final class SchedulerImpl[F[_]: Async]( task match { case Left(err) => - logger.ferror(s"Unable to start a task for job ${job.info}: $err") + logger.error(s"Unable to start a task for job ${job.info}: $err") case Right(t) => for { - _ <- logger.fdebug(s"Creating context for job ${job.info} to run $t") + _ <- logger.debug(s"Creating context for job ${job.info} to run $t") ctx <- Context[F, String](job, job.args, config, logSink, store) jot = wrapTask(job, t.task, ctx) tok <- forkRun(job, jot.run(ctx), t.onCancel.run(ctx), ctx) @@ -200,9 +198,9 @@ final class SchedulerImpl[F[_]: Async]( def onFinish(job: RJob, result: Json, finishState: JobState): F[Unit] = for { - _ <- logger.fdebug(s"Job ${job.info} done $finishState. Releasing resources.") + _ <- logger.debug(s"Job ${job.info} done $finishState. Releasing resources.") _ <- permits.release *> permits.available.flatMap(a => - logger.fdebug(s"Permit released ($a free)") + logger.debug(s"Permit released ($a free)") ) _ <- state.modify(_.removeRunning(job)) _ <- QJob.setFinalState(job.id, finishState, store) @@ -241,7 +239,7 @@ final class SchedulerImpl[F[_]: Async]( ctx: Context[F, String] ): Task[F, String, Unit] = task - .mapF(fa => onStart(job) *> logger.fdebug("Starting task now") *> fa) + .mapF(fa => onStart(job) *> logger.debug("Starting task now") *> fa) .mapF(_.attempt.flatMap { case Right(result) => logger.info(s"Job execution successful: ${job.info}") @@ -284,11 +282,11 @@ final class SchedulerImpl[F[_]: Async]( onCancel: F[Unit], ctx: Context[F, String] ): F[F[Unit]] = - logger.fdebug(s"Forking job ${job.info}") *> + logger.debug(s"Forking job ${job.info}") *> Async[F] .start(code) .map(fiber => - logger.fdebug(s"Cancelling job ${job.info}") *> + logger.debug(s"Cancelling job ${job.info}") *> fiber.cancel *> onCancel.attempt.map { case Right(_) => () @@ -299,7 +297,7 @@ final class SchedulerImpl[F[_]: Async]( state.modify(_.markCancelled(job)) *> onFinish(job, Json.Null, JobState.Cancelled) *> ctx.logger.warn("Job has been cancelled.") *> - logger.fdebug(s"Job ${job.info} has been cancelled.") + logger.debug(s"Job ${job.info} has been cancelled.") ) } diff --git a/modules/joex/src/main/scala/docspell/joex/scheduler/Task.scala b/modules/joex/src/main/scala/docspell/joex/scheduler/Task.scala index 04015b80..d211d5a0 100644 --- a/modules/joex/src/main/scala/docspell/joex/scheduler/Task.scala +++ b/modules/joex/src/main/scala/docspell/joex/scheduler/Task.scala @@ -11,7 +11,7 @@ import cats.data.Kleisli import cats.effect.Sync import cats.implicits._ -import docspell.common.Logger +import docspell.logging.Logger /** The code that is executed by the scheduler */ trait Task[F[_], A, B] { diff --git a/modules/joexapi/src/main/scala/docspell/joexapi/client/JoexClient.scala b/modules/joexapi/src/main/scala/docspell/joexapi/client/JoexClient.scala index d6a6a5c7..c623eba0 100644 --- a/modules/joexapi/src/main/scala/docspell/joexapi/client/JoexClient.scala +++ b/modules/joexapi/src/main/scala/docspell/joexapi/client/JoexClient.scala @@ -9,7 +9,6 @@ package docspell.joexapi.client import cats.effect._ import cats.implicits._ -import docspell.common.syntax.all._ import docspell.common.{Ident, LenientUri} import docspell.joexapi.model.BasicResult @@ -17,7 +16,6 @@ import org.http4s.blaze.client.BlazeClientBuilder import org.http4s.circe.CirceEntityDecoder import org.http4s.client.Client import org.http4s.{Method, Request, Uri} -import org.log4s.getLogger trait JoexClient[F[_]] { @@ -31,22 +29,21 @@ trait JoexClient[F[_]] { object JoexClient { - private[this] val logger = getLogger - def apply[F[_]: Async](client: Client[F]): JoexClient[F] = new JoexClient[F] with CirceEntityDecoder { + private[this] val logger = docspell.logging.getLogger[F] def notifyJoex(base: LenientUri): F[BasicResult] = { val notifyUrl = base / "api" / "v1" / "notify" val req = Request[F](Method.POST, uri(notifyUrl)) - logger.fdebug(s"Notify joex at ${notifyUrl.asString}") *> + logger.debug(s"Notify joex at ${notifyUrl.asString}") *> client.expect[BasicResult](req) } def notifyJoexIgnoreErrors(base: LenientUri): F[Unit] = - notifyJoex(base).attempt.map { + notifyJoex(base).attempt.flatMap { case Right(BasicResult(succ, msg)) => - if (succ) () + if (succ) ().pure[F] else logger.warn( s"Notifying Joex instance '${base.asString}' returned with failure: $msg" diff --git a/modules/logging/api/src/main/scala/docspell/logging/AndThenLogger.scala b/modules/logging/api/src/main/scala/docspell/logging/AndThenLogger.scala new file mode 100644 index 00000000..e5109504 --- /dev/null +++ b/modules/logging/api/src/main/scala/docspell/logging/AndThenLogger.scala @@ -0,0 +1,39 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.logging + +import cats.data.NonEmptyList +import cats.syntax.all._ +import cats.{Applicative, Id} + +final private[logging] class AndThenLogger[F[_]: Applicative]( + val loggers: NonEmptyList[Logger[F]] +) extends Logger[F] { + def log(ev: LogEvent): F[Unit] = + loggers.traverse(_.log(ev)).as(()) + + def asUnsafe: Logger[Id] = + new Logger[Id] { self => + def log(ev: LogEvent): Unit = + loggers.toList.foreach(_.asUnsafe.log(ev)) + def asUnsafe = self + } +} + +private[logging] object AndThenLogger { + def combine[F[_]: Applicative](a: Logger[F], b: Logger[F]): Logger[F] = + (a, b) match { + case (aa: AndThenLogger[F], bb: AndThenLogger[F]) => + new AndThenLogger[F](aa.loggers ++ bb.loggers.toList) + case (aa: AndThenLogger[F], _) => + new AndThenLogger[F](aa.loggers.prepend(b)) + case (_, bb: AndThenLogger[F]) => + new AndThenLogger[F](bb.loggers.prepend(a)) + case _ => + new AndThenLogger[F](NonEmptyList.of(a, b)) + } +} diff --git a/modules/logging/api/src/main/scala/docspell/logging/Level.scala b/modules/logging/api/src/main/scala/docspell/logging/Level.scala new file mode 100644 index 00000000..3600ea93 --- /dev/null +++ b/modules/logging/api/src/main/scala/docspell/logging/Level.scala @@ -0,0 +1,57 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.logging + +import cats.Order +import cats.data.NonEmptyList + +import io.circe.{Decoder, Encoder} + +sealed trait Level { self: Product => + val name: String = + productPrefix.toUpperCase + + val value: Double +} + +object Level { + case object Fatal extends Level { + val value = 600.0 + } + case object Error extends Level { + val value = 500.0 + } + case object Warn extends Level { + val value = 400.0 + } + case object Info extends Level { + val value = 300.0 + } + case object Debug extends Level { + val value = 200.0 + } + case object Trace extends Level { + val value = 100.0 + } + + val all: NonEmptyList[Level] = + NonEmptyList.of(Fatal, Error, Warn, Info, Debug, Trace) + + def fromString(str: String): Either[String, Level] = { + val s = str.toUpperCase + all.find(_.name == s).toRight(s"Invalid level name: $str") + } + + implicit val order: Order[Level] = + Order.by(_.value) + + implicit val jsonEncoder: Encoder[Level] = + Encoder.encodeString.contramap(_.name) + + implicit val jsonDecoder: Decoder[Level] = + Decoder.decodeString.emap(fromString) +} diff --git a/modules/logging/api/src/main/scala/docspell/logging/LogConfig.scala b/modules/logging/api/src/main/scala/docspell/logging/LogConfig.scala new file mode 100644 index 00000000..daaae3e3 --- /dev/null +++ b/modules/logging/api/src/main/scala/docspell/logging/LogConfig.scala @@ -0,0 +1,45 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.logging + +import cats.data.NonEmptyList + +import io.circe.{Decoder, Encoder} + +final case class LogConfig(minimumLevel: Level, format: LogConfig.Format) {} + +object LogConfig { + + sealed trait Format { self: Product => + def name: String = + productPrefix.toLowerCase + } + object Format { + case object Plain extends Format + case object Fancy extends Format + case object Json extends Format + case object Logfmt extends Format + + val all: NonEmptyList[Format] = + NonEmptyList.of(Plain, Fancy, Json, Logfmt) + + def fromString(str: String): Either[String, Format] = + all.find(_.name.equalsIgnoreCase(str)).toRight(s"Invalid format name: $str") + + implicit val jsonDecoder: Decoder[Format] = + Decoder.decodeString.emap(fromString) + + implicit val jsonEncoder: Encoder[Format] = + Encoder.encodeString.contramap(_.name) + } + + implicit val jsonDecoder: Decoder[LogConfig] = + Decoder.forProduct2("minimumLevel", "format")(LogConfig.apply) + + implicit val jsonEncoder: Encoder[LogConfig] = + Encoder.forProduct2("minimumLevel", "format")(r => (r.minimumLevel, r.format)) +} diff --git a/modules/logging/api/src/main/scala/docspell/logging/LogEvent.scala b/modules/logging/api/src/main/scala/docspell/logging/LogEvent.scala new file mode 100644 index 00000000..888fe584 --- /dev/null +++ b/modules/logging/api/src/main/scala/docspell/logging/LogEvent.scala @@ -0,0 +1,51 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.logging + +import io.circe.{Encoder, Json} +import sourcecode._ + +final case class LogEvent( + level: Level, + msg: () => String, + additional: List[() => LogEvent.AdditionalMsg], + data: Map[String, () => Json], + pkg: Pkg, + fileName: FileName, + name: Name, + line: Line +) { + + def asString = + s"${level.name} ${name.value}/${fileName}:${line.value} - ${msg()}" + + def data[A: Encoder](key: String, value: => A): LogEvent = + copy(data = data.updated(key, () => Encoder[A].apply(value))) + + def addMessage(msg: => String): LogEvent = + copy(additional = (() => Left(msg)) :: additional) + + def addError(ex: Throwable): LogEvent = + copy(additional = (() => Right(ex)) :: additional) + + def findErrors: List[Throwable] = + additional.map(a => a()).collect { case Right(ex) => + ex + } +} + +object LogEvent { + + type AdditionalMsg = Either[String, Throwable] + + def of(l: Level, m: => String)(implicit + pkg: Pkg, + fileName: FileName, + name: Name, + line: Line + ): LogEvent = LogEvent(l, () => m, Nil, Map.empty, pkg, fileName, name, line) +} diff --git a/modules/logging/api/src/main/scala/docspell/logging/Logger.scala b/modules/logging/api/src/main/scala/docspell/logging/Logger.scala new file mode 100644 index 00000000..18359294 --- /dev/null +++ b/modules/logging/api/src/main/scala/docspell/logging/Logger.scala @@ -0,0 +1,169 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.logging + +import java.io.PrintStream +import java.time.Instant + +import cats.effect.{Ref, Sync} +import cats.syntax.applicative._ +import cats.syntax.functor._ +import cats.syntax.order._ +import cats.{Applicative, Id} + +import sourcecode._ + +trait Logger[F[_]] extends LoggerExtension[F] { + + def log(ev: LogEvent): F[Unit] + + def asUnsafe: Logger[Id] + + def trace(msg: => String)(implicit + pkg: Pkg, + fileName: FileName, + name: Name, + line: Line + ): F[Unit] = + log(LogEvent.of(Level.Trace, msg)) + + def traceWith(msg: => String)(modify: LogEvent => LogEvent)(implicit + pkg: Pkg, + fileName: FileName, + name: Name, + line: Line + ): F[Unit] = + log(modify(LogEvent.of(Level.Trace, msg))) + + def debug(msg: => String)(implicit + pkg: Pkg, + fileName: FileName, + name: Name, + line: Line + ): F[Unit] = + log(LogEvent.of(Level.Debug, msg)) + + def debugWith(msg: => String)(modify: LogEvent => LogEvent)(implicit + pkg: Pkg, + fileName: FileName, + name: Name, + line: Line + ): F[Unit] = + log(modify(LogEvent.of(Level.Debug, msg))) + + def info(msg: => String)(implicit + pkg: Pkg, + fileName: FileName, + name: Name, + line: Line + ): F[Unit] = + log(LogEvent.of(Level.Info, msg)) + + def infoWith(msg: => String)(modify: LogEvent => LogEvent)(implicit + pkg: Pkg, + fileName: FileName, + name: Name, + line: Line + ): F[Unit] = + log(modify(LogEvent.of(Level.Info, msg))) + + def warn(msg: => String)(implicit + pkg: Pkg, + fileName: FileName, + name: Name, + line: Line + ): F[Unit] = + log(LogEvent.of(Level.Warn, msg)) + + def warnWith(msg: => String)(modify: LogEvent => LogEvent)(implicit + pkg: Pkg, + fileName: FileName, + name: Name, + line: Line + ): F[Unit] = + log(modify(LogEvent.of(Level.Warn, msg))) + + def warn(ex: Throwable)(msg: => String)(implicit + pkg: Pkg, + fileName: FileName, + name: Name, + line: Line + ): F[Unit] = + log(LogEvent.of(Level.Warn, msg).addError(ex)) + + def error(msg: => String)(implicit + pkg: Pkg, + fileName: FileName, + name: Name, + line: Line + ): F[Unit] = + log(LogEvent.of(Level.Error, msg)) + + def errorWith(msg: => String)(modify: LogEvent => LogEvent)(implicit + pkg: Pkg, + fileName: FileName, + name: Name, + line: Line + ): F[Unit] = + log(modify(LogEvent.of(Level.Error, msg))) + + def error(ex: Throwable)(msg: => String)(implicit + pkg: Pkg, + fileName: FileName, + name: Name, + line: Line + ): F[Unit] = + log(LogEvent.of(Level.Error, msg).addError(ex)) +} + +object Logger { + def off: Logger[Id] = + new Logger[Id] { + def log(ev: LogEvent): Unit = () + def asUnsafe = this + } + + def offF[F[_]: Applicative]: Logger[F] = + new Logger[F] { + def log(ev: LogEvent) = ().pure[F] + def asUnsafe = off + } + + def buffer[F[_]: Sync](): F[(Ref[F, Vector[LogEvent]], Logger[F])] = + for { + buffer <- Ref.of[F, Vector[LogEvent]](Vector.empty[LogEvent]) + logger = + new Logger[F] { + def log(ev: LogEvent) = + buffer.update(_.appended(ev)) + def asUnsafe = off + } + } yield (buffer, logger) + + /** Just prints to the given print stream. Useful for testing. */ + def simple(ps: PrintStream, minimumLevel: Level): Logger[Id] = + new Logger[Id] { + def log(ev: LogEvent): Unit = + if (ev.level >= minimumLevel) + ps.println(s"${Instant.now()} [${Thread.currentThread()}] ${ev.asString}") + else + () + + def asUnsafe = this + } + + def simpleF[F[_]: Sync](ps: PrintStream, minimumLevel: Level): Logger[F] = + new Logger[F] { + def log(ev: LogEvent) = + Sync[F].delay(asUnsafe.log(ev)) + + val asUnsafe = simple(ps, minimumLevel) + } + + def simpleDefault[F[_]: Sync](minimumLevel: Level = Level.Info): Logger[F] = + simpleF[F](System.err, minimumLevel) +} diff --git a/modules/logging/api/src/main/scala/docspell/logging/LoggerExtension.scala b/modules/logging/api/src/main/scala/docspell/logging/LoggerExtension.scala new file mode 100644 index 00000000..0e2597af --- /dev/null +++ b/modules/logging/api/src/main/scala/docspell/logging/LoggerExtension.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.logging + +import cats.Applicative +import fs2.Stream + +trait LoggerExtension[F[_]] { self: Logger[F] => + + def stream: Logger[Stream[F, *]] = + new Logger[Stream[F, *]] { + def log(ev: LogEvent) = + Stream.eval(self.log(ev)) + + def asUnsafe = self.asUnsafe + } + + def andThen(other: Logger[F])(implicit F: Applicative[F]): Logger[F] = + AndThenLogger.combine(self, other) + + def >>(other: Logger[F])(implicit F: Applicative[F]): Logger[F] = + AndThenLogger.combine(self, other) +} diff --git a/modules/logging/scribe/src/main/scala/docspell/logging/impl/JsonWriter.scala b/modules/logging/scribe/src/main/scala/docspell/logging/impl/JsonWriter.scala new file mode 100644 index 00000000..020fc9f9 --- /dev/null +++ b/modules/logging/scribe/src/main/scala/docspell/logging/impl/JsonWriter.scala @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.logging.impl + +import io.circe.syntax._ +import scribe._ +import scribe.output._ +import scribe.output.format.OutputFormat +import scribe.writer._ + +final case class JsonWriter(writer: Writer, compact: Boolean = true) extends Writer { + override def write[M]( + record: LogRecord[M], + output: LogOutput, + outputFormat: OutputFormat + ): Unit = { + val r = Record.fromLogRecord(record) + val json = r.asJson + val jsonString = if (compact) json.noSpaces else json.spaces2 + writer.write(record, new TextOutput(jsonString), outputFormat) + } +} diff --git a/modules/logging/scribe/src/main/scala/docspell/logging/impl/LogfmtWriter.scala b/modules/logging/scribe/src/main/scala/docspell/logging/impl/LogfmtWriter.scala new file mode 100644 index 00000000..f9f4db12 --- /dev/null +++ b/modules/logging/scribe/src/main/scala/docspell/logging/impl/LogfmtWriter.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.logging.impl + +import io.circe.syntax._ +import scribe._ +import scribe.output._ +import scribe.output.format.OutputFormat +import scribe.writer._ + +// https://brandur.org/logfmt +final case class LogfmtWriter(writer: Writer) extends Writer { + override def write[M]( + record: LogRecord[M], + output: LogOutput, + outputFormat: OutputFormat + ): Unit = { + val r = Record.fromLogRecord(record) + val data = r.data + .map { case (k, v) => + s"$k=${v.noSpaces}" + } + .mkString(" ") + val logfmtStr = + s"""level=${r.level.asJson.noSpaces} levelValue=${r.levelValue} message=${r.message.asJson.noSpaces} fileName=${r.fileName.asJson.noSpaces} className=${r.className.asJson.noSpaces} methodName=${r.methodName.asJson.noSpaces} line=${r.line.asJson.noSpaces} column=${r.column.asJson.noSpaces} $data timestamp=${r.timeStamp} date=${r.date} time=${r.time}""" + writer.write(record, new TextOutput(logfmtStr), outputFormat) + } +} diff --git a/modules/logging/scribe/src/main/scala/docspell/logging/impl/Record.scala b/modules/logging/scribe/src/main/scala/docspell/logging/impl/Record.scala new file mode 100644 index 00000000..68123a5f --- /dev/null +++ b/modules/logging/scribe/src/main/scala/docspell/logging/impl/Record.scala @@ -0,0 +1,120 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.logging.impl + +import docspell.logging.impl.Record._ + +import io.circe.syntax._ +import io.circe.{Encoder, Json} +import perfolation._ +import scribe.LogRecord +import scribe.data.MDC +import scribe.message.Message + +// From: https://github.com/outr/scribe/blob/8e99521e1ee1f0c421629764dd96e4eb193d84bd/json/shared/src/main/scala/scribe/json/JsonWriter.scala +// which would introduce jackson and other dependencies. Modified to work with circe. +// Original licensed under MIT. + +private[impl] case class Record( + level: String, + levelValue: Double, + message: String, + additionalMessages: List[String], + fileName: String, + className: String, + methodName: Option[String], + line: Option[Int], + column: Option[Int], + data: Map[String, Json], + traces: List[Trace], + timeStamp: Long, + date: String, + time: String +) + +private[impl] object Record { + + def fromLogRecord[M](record: LogRecord[M]): Record = { + val l = record.timeStamp + val traces = record.additionalMessages.collect { + case message: Message[_] if message.value.isInstanceOf[Throwable] => + throwable2Trace(message.value.asInstanceOf[Throwable]) + } + val additionalMessages = record.additionalMessages.map(_.logOutput.plainText) + + Record( + level = record.level.name, + levelValue = record.levelValue, + message = record.logOutput.plainText, + additionalMessages = additionalMessages, + fileName = record.fileName, + className = record.className, + methodName = record.methodName, + line = record.line, + column = record.column, + data = (record.data ++ MDC.map).map { case (key, value) => + value() match { + case value: Json => key -> value + case value: Int => key -> value.asJson + case value: Long => key -> value.asJson + case value: Double => key -> value.asJson + case any => key -> Json.fromString(any.toString) + } + }, + traces = traces, + timeStamp = l, + date = l.t.F, + time = s"${l.t.T}.${l.t.L}${l.t.z}" + ) + } + + private def throwable2Trace(throwable: Throwable): Trace = { + val elements = throwable.getStackTrace.toList.map { e => + TraceElement(e.getClassName, e.getMethodName, e.getLineNumber) + } + Trace( + throwable.getLocalizedMessage, + elements, + Option(throwable.getCause).map(throwable2Trace) + ) + } + + implicit val jsonEncoder: Encoder[Record] = + Encoder.forProduct14( + "level", + "levelValue", + "message", + "additionalMessages", + "fileName", + "className", + "methodName", + "line", + "column", + "data", + "traces", + "timestamp", + "date", + "time" + )(r => Record.unapply(r).get) + + case class Trace(message: String, elements: List[TraceElement], cause: Option[Trace]) + + object Trace { + implicit def jsonEncoder: Encoder[Trace] = + Encoder.forProduct3("message", "elements", "cause")(r => Trace.unapply(r).get) + + implicit def openEncoder: Encoder[Option[Trace]] = + Encoder.instance(opt => opt.map(jsonEncoder.apply).getOrElse(Json.Null)) + } + + case class TraceElement(`class`: String, method: String, line: Int) + + object TraceElement { + implicit val jsonEncoder: Encoder[TraceElement] = + Encoder.forProduct3("class", "method", "line")(r => TraceElement.unapply(r).get) + } +} diff --git a/modules/logging/scribe/src/main/scala/docspell/logging/impl/ScribeConfigure.scala b/modules/logging/scribe/src/main/scala/docspell/logging/impl/ScribeConfigure.scala new file mode 100644 index 00000000..1b8b6c79 --- /dev/null +++ b/modules/logging/scribe/src/main/scala/docspell/logging/impl/ScribeConfigure.scala @@ -0,0 +1,69 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.logging.impl + +import cats.effect.Sync + +import docspell.logging.LogConfig.Format +import docspell.logging.{Level, LogConfig} + +import scribe.format.Formatter +import scribe.jul.JULHandler + +object ScribeConfigure { + private[this] val docspellRootVerbose = "DOCSPELL_ROOT_LOGGER_LEVEL" + + def configure[F[_]: Sync](cfg: LogConfig): F[Unit] = + Sync[F].delay { + replaceJUL() + val docspellLogger = scribe.Logger("docspell") + unsafeConfigure(scribe.Logger.root, cfg.copy(minimumLevel = getRootMinimumLevel)) + unsafeConfigure(docspellLogger, cfg) + } + + private[this] def getRootMinimumLevel: Level = + Option(System.getenv(docspellRootVerbose)) + .map(Level.fromString) + .flatMap { + 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 = { + val mods = List[scribe.Logger => scribe.Logger]( + _.clearHandlers(), + _.withMinimumLevel(ScribeWrapper.convertLevel(cfg.minimumLevel)), + l => + cfg.format match { + case Format.Fancy => + l.withHandler(formatter = Formatter.enhanced, writer = StdoutWriter) + case Format.Plain => + l.withHandler(formatter = Formatter.classic, writer = StdoutWriter) + case Format.Json => + l.withHandler(writer = JsonWriter(StdoutWriter)) + case Format.Logfmt => + l.withHandler(writer = LogfmtWriter(StdoutWriter)) + }, + _.replace() + ) + + mods.foldLeft(logger)((l, mod) => mod(l)) + () + } + + private def replaceJUL(): Unit = { + scribe.Logger.system // just to load effects in Logger singleton + val julRoot = java.util.logging.LogManager.getLogManager.getLogger("") + julRoot.getHandlers.foreach(julRoot.removeHandler) + julRoot.addHandler(JULHandler) + } +} diff --git a/modules/logging/scribe/src/main/scala/docspell/logging/impl/ScribeWrapper.scala b/modules/logging/scribe/src/main/scala/docspell/logging/impl/ScribeWrapper.scala new file mode 100644 index 00000000..1eeadaed --- /dev/null +++ b/modules/logging/scribe/src/main/scala/docspell/logging/impl/ScribeWrapper.scala @@ -0,0 +1,52 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.logging.impl + +import cats.Id +import cats.effect.Sync + +import docspell.logging.{Level, LogEvent, Logger} + +import scribe.LoggerSupport +import scribe.message.{LoggableMessage, Message} + +private[logging] object ScribeWrapper { + final class ImplUnsafe(log: scribe.Logger) extends Logger[Id] { + override def asUnsafe = this + + override def log(ev: LogEvent): Unit = + log.log(convert(ev)) + } + final class Impl[F[_]: Sync](log: scribe.Logger) extends Logger[F] { + override def asUnsafe = new ImplUnsafe(log) + + override def log(ev: LogEvent) = + Sync[F].delay(log.log(convert(ev))) + } + + private[impl] def convertLevel(l: Level): scribe.Level = + l match { + case Level.Fatal => scribe.Level.Fatal + case Level.Error => scribe.Level.Error + case Level.Warn => scribe.Level.Warn + case Level.Info => scribe.Level.Info + case Level.Debug => scribe.Level.Debug + case Level.Trace => scribe.Level.Trace + } + + private[this] def convert(ev: LogEvent) = { + val level = convertLevel(ev.level) + val additional: List[LoggableMessage] = ev.additional.map { x => + x() match { + case Right(ex) => Message.static(ex) + case Left(msg) => Message.static(msg) + } + } + LoggerSupport(level, ev.msg(), additional, ev.pkg, ev.fileName, ev.name, ev.line) + .copy(data = ev.data) + } +} diff --git a/modules/logging/scribe/src/main/scala/docspell/logging/impl/StdoutWriter.scala b/modules/logging/scribe/src/main/scala/docspell/logging/impl/StdoutWriter.scala new file mode 100644 index 00000000..ba79d892 --- /dev/null +++ b/modules/logging/scribe/src/main/scala/docspell/logging/impl/StdoutWriter.scala @@ -0,0 +1,56 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.logging.impl + +import scribe._ +import scribe.output.LogOutput +import scribe.output.format.OutputFormat +import scribe.writer.Writer + +// From: https://github.com/outr/scribe/blob/8e99521e1ee1f0c421629764dd96e4eb193d84bd/core/shared/src/main/scala/scribe/writer/SystemOutputWriter.scala +// Modified to always log to stdout. The original code was logging to stdout and stderr +// depending on the log level. +// Original code licensed under MIT + +private[impl] object StdoutWriter extends Writer { + + /** If true, will always synchronize writing to the console to avoid interleaved text. + * Most native consoles will handle this automatically, but IntelliJ and Eclipse are + * notorious about not properly handling this. Defaults to true. + */ + val synchronizeWriting: Boolean = true + + /** Workaround for some consoles that don't play nicely with asynchronous calls */ + val alwaysFlush: Boolean = false + + private val stringBuilders = new ThreadLocal[StringBuilder] { + override def initialValue(): StringBuilder = new StringBuilder(512) + } + + @annotation.nowarn + override def write[M]( + record: LogRecord[M], + output: LogOutput, + outputFormat: OutputFormat + ): Unit = { + val stream = Logger.system.out + val sb = stringBuilders.get() + outputFormat.begin(sb.append(_)) + outputFormat(output, s => sb.append(s)) + outputFormat.end(sb.append(_)) + if (synchronizeWriting) { + synchronized { + stream.println(sb.toString()) + if (alwaysFlush) stream.flush() + } + } else { + stream.println(sb.toString()) + if (alwaysFlush) stream.flush() + } + sb.clear() + } +} diff --git a/modules/logging/scribe/src/main/scala/docspell/logging/package.scala b/modules/logging/scribe/src/main/scala/docspell/logging/package.scala new file mode 100644 index 00000000..4037dccf --- /dev/null +++ b/modules/logging/scribe/src/main/scala/docspell/logging/package.scala @@ -0,0 +1,33 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell + +import cats.Id +import cats.effect._ + +import docspell.logging.impl.ScribeWrapper + +import sourcecode.Enclosing + +package object logging { + + def unsafeLogger(name: String): Logger[Id] = + new ScribeWrapper.ImplUnsafe(scribe.Logger(name)) + + def unsafeLogger(implicit e: Enclosing): Logger[Id] = + unsafeLogger(e.value) + + def getLogger[F[_]: Sync](implicit e: Enclosing): Logger[F] = + getLogger(e.value) + + def getLogger[F[_]: Sync](name: String): Logger[F] = + new ScribeWrapper.Impl[F](scribe.Logger(name)) + + def getLogger[F[_]: Sync](clazz: Class[_]): Logger[F] = + new ScribeWrapper.Impl[F](scribe.Logger(clazz.getName)) + +} diff --git a/modules/logging/scribe/src/test/scala/docspell/logging/TestLoggingConfig.scala b/modules/logging/scribe/src/test/scala/docspell/logging/TestLoggingConfig.scala new file mode 100644 index 00000000..5556c040 --- /dev/null +++ b/modules/logging/scribe/src/test/scala/docspell/logging/TestLoggingConfig.scala @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.logging + +import docspell.logging.impl.ScribeConfigure + +import munit.Suite + +trait TestLoggingConfig extends Suite { + def docspellLogConfig: LogConfig = LogConfig(Level.Warn, LogConfig.Format.Fancy) + def rootMinimumLevel: Level = Level.Error + + override def beforeAll(): Unit = { + super.beforeAll() + val docspellLogger = scribe.Logger("docspell") + ScribeConfigure.unsafeConfigure(docspellLogger, docspellLogConfig) + val rootCfg = docspellLogConfig.copy(minimumLevel = rootMinimumLevel) + ScribeConfigure.unsafeConfigure(scribe.Logger.root, rootCfg) + () + } + +} diff --git a/modules/notification/api/src/main/scala/docspell/notification/api/EventExchange.scala b/modules/notification/api/src/main/scala/docspell/notification/api/EventExchange.scala index 6eb54387..7d3cdad5 100644 --- a/modules/notification/api/src/main/scala/docspell/notification/api/EventExchange.scala +++ b/modules/notification/api/src/main/scala/docspell/notification/api/EventExchange.scala @@ -13,7 +13,7 @@ import cats.effect.std.Queue import cats.implicits._ import fs2.Stream -import docspell.common.Logger +import docspell.logging.Logger /** Combines a sink and reader to a place where events can be submitted and processed in a * producer-consumer manner. @@ -21,8 +21,6 @@ import docspell.common.Logger trait EventExchange[F[_]] extends EventSink[F] with EventReader[F] {} object EventExchange { - private[this] val logger = org.log4s.getLogger - def silent[F[_]: Applicative]: EventExchange[F] = new EventExchange[F] { def offer(event: Event): F[Unit] = @@ -36,7 +34,7 @@ object EventExchange { Queue.circularBuffer[F, Event](queueSize).map(q => new Impl(q)) final class Impl[F[_]: Async](queue: Queue[F, Event]) extends EventExchange[F] { - private[this] val log = Logger.log4s[F](logger) + private[this] val log: Logger[F] = docspell.logging.getLogger[F] def offer(event: Event): F[Unit] = log.debug(s"Pushing event to queue: $event") *> @@ -47,7 +45,7 @@ object EventExchange { def consume(maxConcurrent: Int)(run: Kleisli[F, Event, Unit]): Stream[F, Nothing] = { val stream = Stream.repeatEval(queue.take).evalMap((logEvent >> run).run) - log.s.info(s"Starting up $maxConcurrent notification event consumers").drain ++ + log.stream.info(s"Starting up $maxConcurrent notification event consumers").drain ++ Stream(stream).repeat.take(maxConcurrent.toLong).parJoin(maxConcurrent).drain } } diff --git a/modules/notification/api/src/main/scala/docspell/notification/api/NotificationBackend.scala b/modules/notification/api/src/main/scala/docspell/notification/api/NotificationBackend.scala index c543b806..5433c34b 100644 --- a/modules/notification/api/src/main/scala/docspell/notification/api/NotificationBackend.scala +++ b/modules/notification/api/src/main/scala/docspell/notification/api/NotificationBackend.scala @@ -13,7 +13,7 @@ import cats.implicits._ import cats.kernel.Monoid import fs2.Stream -import docspell.common._ +import docspell.logging.Logger /** Pushes notification messages/events to an external system */ trait NotificationBackend[F[_]] { diff --git a/modules/notification/api/src/main/scala/docspell/notification/api/NotificationModule.scala b/modules/notification/api/src/main/scala/docspell/notification/api/NotificationModule.scala index a87e819d..c10e24f1 100644 --- a/modules/notification/api/src/main/scala/docspell/notification/api/NotificationModule.scala +++ b/modules/notification/api/src/main/scala/docspell/notification/api/NotificationModule.scala @@ -11,7 +11,7 @@ import cats.data.{Kleisli, OptionT} import cats.implicits._ import fs2.Stream -import docspell.common.Logger +import docspell.logging.Logger trait NotificationModule[F[_]] extends EventSink[F] diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/EmailBackend.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/EmailBackend.scala index 81a016c2..bfe944bb 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/EmailBackend.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/EmailBackend.scala @@ -9,7 +9,7 @@ package docspell.notification.impl import cats.effect._ import cats.implicits._ -import docspell.common._ +import docspell.logging.Logger import docspell.notification.api._ import emil.Emil diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/EventContextSyntax.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/EventContextSyntax.scala index 79fa66a6..1609105c 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/EventContextSyntax.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/EventContextSyntax.scala @@ -6,7 +6,7 @@ package docspell.notification.impl -import docspell.common.Logger +import docspell.logging.Logger import docspell.notification.api.EventContext import io.circe.Json diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/EventNotify.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/EventNotify.scala index 1b7e95a2..e3fcd63b 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/EventNotify.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/EventNotify.scala @@ -10,7 +10,6 @@ import cats.data.Kleisli import cats.data.OptionT import cats.effect._ -import docspell.common.Logger import docspell.notification.api.Event import docspell.notification.api.NotificationBackend import docspell.store.Store @@ -21,13 +20,13 @@ import org.http4s.client.Client /** Represents the actual work done for each event. */ object EventNotify { - private[this] val log4sLogger = org.log4s.getLogger def apply[F[_]: Async]( store: Store[F], mailService: Emil[F], client: Client[F] - ): Kleisli[F, Event, Unit] = + ): Kleisli[F, Event, Unit] = { + val logger = docspell.logging.getLogger[F] Kleisli { event => (for { hooks <- OptionT.liftF(store.transact(QNotification.findChannelsForEvent(event))) @@ -43,10 +42,11 @@ object EventNotify { NotificationBackendImpl.forChannelsIgnoreErrors( client, mailService, - Logger.log4s(log4sLogger) + logger )(channels) _ <- OptionT.liftF(backend.send(evctx)) } yield ()).getOrElse(()) } + } } diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/GotifyBackend.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/GotifyBackend.scala index da81f6a9..7f6b8c8a 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/GotifyBackend.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/GotifyBackend.scala @@ -9,7 +9,7 @@ package docspell.notification.impl import cats.effect._ import cats.implicits._ -import docspell.common.Logger +import docspell.logging.Logger import docspell.notification.api._ import io.circe.Json diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/HttpPostBackend.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/HttpPostBackend.scala index 0a5acb21..14078db3 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/HttpPostBackend.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/HttpPostBackend.scala @@ -9,7 +9,7 @@ package docspell.notification.impl import cats.effect._ import cats.implicits._ -import docspell.common.Logger +import docspell.logging.Logger import docspell.notification.api._ import org.http4s.Uri diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/HttpSend.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/HttpSend.scala index 38fcd520..276a6bf9 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/HttpSend.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/HttpSend.scala @@ -9,7 +9,7 @@ package docspell.notification.impl import cats.effect._ import cats.implicits._ -import docspell.common._ +import docspell.logging.Logger import docspell.notification.api.NotificationChannel import org.http4s.Request diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/MatrixBackend.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/MatrixBackend.scala index 41f0cb9b..8f6d0eea 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/MatrixBackend.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/MatrixBackend.scala @@ -8,7 +8,7 @@ package docspell.notification.impl import cats.effect._ -import docspell.common.Logger +import docspell.logging.Logger import docspell.notification.api._ import org.http4s.Uri diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/NotificationBackendImpl.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/NotificationBackendImpl.scala index 48a3406c..b77f938c 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/NotificationBackendImpl.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/NotificationBackendImpl.scala @@ -9,7 +9,7 @@ package docspell.notification.impl import cats.data.NonEmptyList import cats.effect._ -import docspell.common.Logger +import docspell.logging.Logger import docspell.notification.api.NotificationBackend.{combineAll, ignoreErrors, silent} import docspell.notification.api.{NotificationBackend, NotificationChannel} diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/NotificationModuleImpl.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/NotificationModuleImpl.scala index 9c49e9da..8ae214da 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/NotificationModuleImpl.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/NotificationModuleImpl.scala @@ -10,7 +10,7 @@ import cats.data.Kleisli import cats.effect.kernel.Async import cats.implicits._ -import docspell.common._ +import docspell.logging.Logger import docspell.notification.api._ import docspell.store.Store diff --git a/modules/oidc/src/main/scala/docspell/oidc/CodeFlow.scala b/modules/oidc/src/main/scala/docspell/oidc/CodeFlow.scala index 9a40d9ae..c1a01995 100644 --- a/modules/oidc/src/main/scala/docspell/oidc/CodeFlow.scala +++ b/modules/oidc/src/main/scala/docspell/oidc/CodeFlow.scala @@ -22,7 +22,6 @@ import org.http4s.client.middleware.RequestLogger import org.http4s.client.middleware.ResponseLogger import org.http4s.headers.Accept import org.http4s.headers.Authorization -import org.log4s.getLogger /** https://openid.net/specs/openid-connect-core-1_0.html (OIDC) * https://openid.net/specs/openid-connect-basic-1_0.html#TokenRequest (OIDC) @@ -30,7 +29,6 @@ import org.log4s.getLogger * https://datatracker.ietf.org/doc/html/rfc7519 (JWT) */ object CodeFlow { - private[this] val log4sLogger = getLogger def apply[F[_]: Async, A]( client: Client[F], @@ -39,7 +37,7 @@ object CodeFlow { )( code: String ): OptionT[F, Json] = { - val logger = Logger.log4s[F](log4sLogger) + val logger = docspell.logging.getLogger[F] val dsl = new Http4sClientDsl[F] {} val c = logRequests[F](logResponses[F](client)) @@ -93,7 +91,7 @@ object CodeFlow { code: String ): OptionT[F, AccessToken] = { import dsl._ - val logger = Logger.log4s[F](log4sLogger) + val logger = docspell.logging.getLogger[F] val req = POST( UrlForm( @@ -133,7 +131,7 @@ object CodeFlow { token: AccessToken ): OptionT[F, Json] = { import dsl._ - val logger = Logger.log4s[F](log4sLogger) + val logger = docspell.logging.getLogger[F] val req = GET( Uri.unsafeFromString(endpointUrl.asString), @@ -162,18 +160,22 @@ object CodeFlow { OptionT(resp) } - private def logRequests[F[_]: Async](c: Client[F]): Client[F] = + private def logRequests[F[_]: Async](c: Client[F]): Client[F] = { + val logger = docspell.logging.getLogger[F] RequestLogger( logHeaders = true, logBody = true, - logAction = Some((msg: String) => Logger.log4s(log4sLogger).trace(msg)) + logAction = Some((msg: String) => logger.trace(msg)) )(c) + } - private def logResponses[F[_]: Async](c: Client[F]): Client[F] = + private def logResponses[F[_]: Async](c: Client[F]): Client[F] = { + val logger = docspell.logging.getLogger[F] ResponseLogger( logHeaders = true, logBody = true, - logAction = Some((msg: String) => Logger.log4s(log4sLogger).trace(msg)) + logAction = Some((msg: String) => logger.trace(msg)) )(c) + } } diff --git a/modules/oidc/src/main/scala/docspell/oidc/CodeFlowRoutes.scala b/modules/oidc/src/main/scala/docspell/oidc/CodeFlowRoutes.scala index eee50d96..531b0103 100644 --- a/modules/oidc/src/main/scala/docspell/oidc/CodeFlowRoutes.scala +++ b/modules/oidc/src/main/scala/docspell/oidc/CodeFlowRoutes.scala @@ -17,10 +17,8 @@ import org.http4s._ import org.http4s.client.Client import org.http4s.dsl.Http4sDsl import org.http4s.headers.Location -import org.log4s.getLogger object CodeFlowRoutes { - private[this] val log4sLogger = getLogger def apply[F[_]: Async]( enabled: Boolean, @@ -38,7 +36,7 @@ object CodeFlowRoutes { ): HttpRoutes[F] = { val dsl: Http4sDsl[F] = new Http4sDsl[F] {} import dsl._ - val logger = Logger.log4s[F](log4sLogger) + val logger = docspell.logging.getLogger[F] HttpRoutes.of[F] { case req @ GET -> Root / Ident(id) => config.findProvider(id) match { diff --git a/modules/oidc/src/main/scala/docspell/oidc/OnUserInfo.scala b/modules/oidc/src/main/scala/docspell/oidc/OnUserInfo.scala index 384a9913..5e827356 100644 --- a/modules/oidc/src/main/scala/docspell/oidc/OnUserInfo.scala +++ b/modules/oidc/src/main/scala/docspell/oidc/OnUserInfo.scala @@ -10,13 +10,10 @@ import cats.effect._ import cats.implicits._ import fs2.Stream -import docspell.common.Logger - import io.circe.Json import org.http4s._ import org.http4s.headers.`Content-Type` import org.http4s.implicits._ -import org.log4s.getLogger /** Once the authentication flow is completed, we get "some" json structure that contains * a claim about the user. From here it's to the user of this small library to complete @@ -44,18 +41,16 @@ trait OnUserInfo[F[_]] { } object OnUserInfo { - private[this] val log = getLogger - def apply[F[_]]( f: (Request[F], ProviderConfig, Option[Json]) => F[Response[F]] ): OnUserInfo[F] = (req: Request[F], cfg: ProviderConfig, userInfo: Option[Json]) => f(req, cfg, userInfo) - def logInfo[F[_]: Sync]: OnUserInfo[F] = + def logInfo[F[_]: Sync]: OnUserInfo[F] = { + val logger = docspell.logging.getLogger[F] OnUserInfo((_, _, json) => - Logger - .log4s(log) + logger .info(s"Got data: ${json.map(_.spaces2)}") .map(_ => Response[F](Status.Ok) @@ -65,4 +60,5 @@ object OnUserInfo { ) ) ) + } } diff --git a/modules/pubsub/api/src/main/scala/docspell/pubsub/api/PubSubT.scala b/modules/pubsub/api/src/main/scala/docspell/pubsub/api/PubSubT.scala index 2e51922d..20c964dd 100644 --- a/modules/pubsub/api/src/main/scala/docspell/pubsub/api/PubSubT.scala +++ b/modules/pubsub/api/src/main/scala/docspell/pubsub/api/PubSubT.scala @@ -12,7 +12,7 @@ import cats.implicits._ import fs2.concurrent.SignallingRef import fs2.{Pipe, Stream} -import docspell.common.Logger +import docspell.logging.Logger trait PubSubT[F[_]] { @@ -33,7 +33,7 @@ trait PubSubT[F[_]] { object PubSubT { def noop[F[_]: Async]: PubSubT[F] = - PubSubT(PubSub.noop[F], Logger.off[F]) + PubSubT(PubSub.noop[F], Logger.offF[F]) def apply[F[_]: Async](pubSub: PubSub[F], logger: Logger[F]): PubSubT[F] = new PubSubT[F] { @@ -57,7 +57,7 @@ object PubSubT { m.body.as[A](topic.codec) match { case Right(a) => Stream.emit(Message(m.head, a)) case Left(err) => - logger.s + logger.stream .error(err)( s"Could not decode message to topic ${topic.name} to ${topic.msgClass}: ${m.body.noSpaces}" ) diff --git a/modules/pubsub/naive/src/main/scala/docspell/pubsub/naive/NaivePubSub.scala b/modules/pubsub/naive/src/main/scala/docspell/pubsub/naive/NaivePubSub.scala index 00385ad0..8379e724 100644 --- a/modules/pubsub/naive/src/main/scala/docspell/pubsub/naive/NaivePubSub.scala +++ b/modules/pubsub/naive/src/main/scala/docspell/pubsub/naive/NaivePubSub.scala @@ -14,6 +14,7 @@ import fs2.Stream import fs2.concurrent.{Topic => Fs2Topic} import docspell.common._ +import docspell.logging.Logger import docspell.pubsub.api._ import docspell.pubsub.naive.NaivePubSub.State import docspell.store.Store @@ -60,7 +61,7 @@ final class NaivePubSub[F[_]: Async]( store: Store[F], client: Client[F] ) extends PubSub[F] { - private val logger: Logger[F] = Logger.log4s(org.log4s.getLogger) + private val logger: Logger[F] = docspell.logging.getLogger[F] def withClient(client: Client[F]): NaivePubSub[F] = new NaivePubSub[F](cfg, state, store, client) @@ -85,7 +86,7 @@ final class NaivePubSub[F[_]: Async]( def subscribe(topics: NonEmptyList[Topic]): Stream[F, Message[Json]] = (for { - _ <- logger.s.info(s"Adding subscriber for topics: $topics") + _ <- logger.stream.info(s"Adding subscriber for topics: $topics") _ <- Stream.resource[F, Unit](addRemote(topics)) m <- Stream.eval(addLocal(topics)) } yield m).flatten diff --git a/modules/pubsub/naive/src/test/scala/docspell/pubsub/naive/Fixtures.scala b/modules/pubsub/naive/src/test/scala/docspell/pubsub/naive/Fixtures.scala index 15594d72..848fc387 100644 --- a/modules/pubsub/naive/src/test/scala/docspell/pubsub/naive/Fixtures.scala +++ b/modules/pubsub/naive/src/test/scala/docspell/pubsub/naive/Fixtures.scala @@ -9,6 +9,7 @@ package docspell.pubsub.naive import cats.effect._ import docspell.common._ +import docspell.logging.Logger import docspell.pubsub.api._ import docspell.store.{Store, StoreFixture} @@ -45,7 +46,7 @@ trait Fixtures extends HttpClientOps { self: CatsEffectSuite => } object Fixtures { - private val loggerIO: Logger[IO] = Logger.log4s(org.log4s.getLogger) + private val loggerIO: Logger[IO] = Logger.simpleDefault[IO]() final case class Env(store: Store[IO], cfg: PubSubConfig) { def pubSub: Resource[IO, NaivePubSub[IO]] = { diff --git a/modules/pubsub/naive/src/test/scala/docspell/pubsub/naive/HttpClientOps.scala b/modules/pubsub/naive/src/test/scala/docspell/pubsub/naive/HttpClientOps.scala index 3659f88e..30084d0b 100644 --- a/modules/pubsub/naive/src/test/scala/docspell/pubsub/naive/HttpClientOps.scala +++ b/modules/pubsub/naive/src/test/scala/docspell/pubsub/naive/HttpClientOps.scala @@ -9,6 +9,7 @@ package docspell.pubsub.naive import cats.effect._ import docspell.common._ +import docspell.logging.Logger import docspell.pubsub.api._ import io.circe.Encoder @@ -55,5 +56,5 @@ trait HttpClientOps { } object HttpClientOps { - private val logger: Logger[IO] = Logger.log4s(org.log4s.getLogger) + private val logger = Logger.simpleDefault[IO]() } diff --git a/modules/pubsub/naive/src/test/scala/docspell/pubsub/naive/NaivePubSubTest.scala b/modules/pubsub/naive/src/test/scala/docspell/pubsub/naive/NaivePubSubTest.scala index 8567e0ec..bdcb45a9 100644 --- a/modules/pubsub/naive/src/test/scala/docspell/pubsub/naive/NaivePubSubTest.scala +++ b/modules/pubsub/naive/src/test/scala/docspell/pubsub/naive/NaivePubSubTest.scala @@ -12,14 +12,14 @@ import cats.effect._ import cats.implicits._ import fs2.concurrent.SignallingRef -import docspell.common._ +import docspell.logging.{Logger, TestLoggingConfig} import docspell.pubsub.api._ import docspell.pubsub.naive.Topics._ import munit.CatsEffectSuite -class NaivePubSubTest extends CatsEffectSuite with Fixtures { - private[this] val logger = Logger.log4s[IO](org.log4s.getLogger) +class NaivePubSubTest extends CatsEffectSuite with Fixtures with TestLoggingConfig { + private[this] val logger = Logger.simpleDefault[IO]() def subscribe[A](ps: PubSubT[IO], topic: TypedTopic[A]) = for { diff --git a/modules/restserver/src/main/resources/logback.xml b/modules/restserver/src/main/resources/logback.xml deleted file mode 100644 index 406afe6e..00000000 --- a/modules/restserver/src/main/resources/logback.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - true - - - level=%-5level thread=%thread logger=%logger{15} message="%replace(%msg){'"', '\\"'}"%n - - - - - - - - - - diff --git a/modules/restserver/src/main/resources/reference.conf b/modules/restserver/src/main/resources/reference.conf index cb60e0dd..afd77ae7 100644 --- a/modules/restserver/src/main/resources/reference.conf +++ b/modules/restserver/src/main/resources/reference.conf @@ -21,6 +21,17 @@ docspell.server { # other nodes can reach this server. internal-url = "http://localhost:7880" + # Configures logging + logging { + # The format for the log messages. Can be one of: + # Json, Logfmt, Fancy or Plain + format = "Plain" + + # The minimum level to log. From lowest to highest: + # Trace, Debug, Info, Warn, Error + minimum-level = "Info" + } + # Where the server binds to. bind { address = "localhost" diff --git a/modules/restserver/src/main/scala/docspell/restserver/Config.scala b/modules/restserver/src/main/scala/docspell/restserver/Config.scala index 6d30e1e1..bfce9652 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/Config.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/Config.scala @@ -10,6 +10,7 @@ import docspell.backend.auth.Login import docspell.backend.{Config => BackendConfig} import docspell.common._ import docspell.ftssolr.SolrConfig +import docspell.logging.LogConfig import docspell.oidc.ProviderConfig import docspell.pubsub.naive.PubSubConfig import docspell.restserver.Config.OpenIdConfig @@ -23,6 +24,7 @@ case class Config( appId: Ident, baseUrl: LenientUri, internalUrl: LenientUri, + logging: LogConfig, bind: Config.Bind, backend: BackendConfig, auth: Login.Config, diff --git a/modules/restserver/src/main/scala/docspell/restserver/ConfigFile.scala b/modules/restserver/src/main/scala/docspell/restserver/ConfigFile.scala index 7a893671..5e72a1d4 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/ConfigFile.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/ConfigFile.scala @@ -12,7 +12,6 @@ import cats.Monoid import cats.effect.Async import docspell.backend.signup.{Config => SignupConfig} -import docspell.common.Logger import docspell.config.Implicits._ import docspell.config.{ConfigFactory, Validation} import docspell.oidc.{ProviderConfig, SignatureAlgo} @@ -23,11 +22,12 @@ import pureconfig.generic.auto._ import scodec.bits.ByteVector object ConfigFile { - private[this] val unsafeLogger = org.log4s.getLogger + private[this] val unsafeLogger = docspell.logging.unsafeLogger + import Implicits._ def loadConfig[F[_]: Async](args: List[String]): F[Config] = { - val logger = Logger.log4s(unsafeLogger) + val logger = docspell.logging.getLogger[F] val validate = Validation.of(generateSecretIfEmpty, duplicateOpenIdProvider, signKeyVsUserUrl) ConfigFactory diff --git a/modules/restserver/src/main/scala/docspell/restserver/Main.scala b/modules/restserver/src/main/scala/docspell/restserver/Main.scala index 61844bdd..c8a6a003 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/Main.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/Main.scala @@ -9,17 +9,17 @@ package docspell.restserver import cats.effect._ import docspell.common._ - -import org.log4s.getLogger +import docspell.logging.impl.ScribeConfigure object Main extends IOApp { - private[this] val logger: Logger[IO] = Logger.log4s(getLogger) private val connectEC = ThreadFactories.fixed[IO](5, ThreadFactories.ofName("docspell-dbconnect")) def run(args: List[String]) = for { cfg <- ConfigFile.loadConfig[IO](args) + _ <- ScribeConfigure.configure[IO](cfg.logging) + logger = docspell.logging.getLogger[IO] banner = Banner( "REST Server", BuildInfo.version, diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala b/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala index 902bb8b4..ce14ecd3 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala @@ -11,7 +11,6 @@ import fs2.Stream import fs2.concurrent.Topic import docspell.backend.BackendApp -import docspell.common.Logger import docspell.ftsclient.FtsClient import docspell.ftssolr.SolrFtsClient import docspell.notification.api.NotificationModule @@ -47,7 +46,8 @@ object RestAppImpl { pubSub: PubSub[F], wsTopic: Topic[F, OutputEvent] ): Resource[F, RestApp[F]] = { - val logger = Logger.log4s(org.log4s.getLogger(s"restserver-${cfg.appId.id}")) + val logger = docspell.logging.getLogger[F](s"restserver-${cfg.appId.id}") + for { ftsClient <- createFtsClient(cfg)(httpClient) pubSubT = PubSubT(pubSub, logger) diff --git a/modules/restserver/src/main/scala/docspell/restserver/auth/OpenId.scala b/modules/restserver/src/main/scala/docspell/restserver/auth/OpenId.scala index 668a405a..9ec8e7ea 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/auth/OpenId.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/auth/OpenId.scala @@ -22,10 +22,8 @@ import io.circe.Json import org.http4s.dsl.Http4sDsl import org.http4s.headers.Location import org.http4s.{Response, Uri} -import org.log4s.getLogger object OpenId { - private[this] val log = getLogger def codeFlowConfig[F[_]](config: Config): CodeFlowConfig[F] = CodeFlowConfig( @@ -38,9 +36,9 @@ object OpenId { def handle[F[_]: Async](backend: BackendApp[F], config: Config): OnUserInfo[F] = OnUserInfo { (req, provider, userInfo) => + val logger = docspell.logging.getLogger[F] val dsl = new Http4sDsl[F] {} import dsl._ - val logger = Logger.log4s(log) val baseUrl = ClientRequestInfo.getBaseUrl(config, req) val uri = baseUrl.withQuery("openid", "1") / "app" / "login" val location = Location(Uri.unsafeFromString(uri.asString)) @@ -101,6 +99,7 @@ object OpenId { location: Location, baseUrl: LenientUri ): F[Response[F]] = { + val logger = docspell.logging.getLogger[F] val dsl = new Http4sDsl[F] {} import dsl._ @@ -108,7 +107,6 @@ object OpenId { setup <- backend.signup.setupExternal(cfg.backend.signup)( ExternalAccount(accountId) ) - logger = Logger.log4s(log) res <- setup match { case SignupResult.Failure(ex) => logger.error(ex)(s"Error when creating external account!") *> @@ -141,6 +139,7 @@ object OpenId { location: Location, baseUrl: LenientUri ): F[Response[F]] = { + val logger = docspell.logging.getLogger[F] val dsl = new Http4sDsl[F] {} import dsl._ @@ -160,7 +159,7 @@ object OpenId { .map(_.addCookie(CookieData(session).asCookie(baseUrl))) case failed => - Logger.log4s(log).error(s"External login failed: $failed") *> + logger.error(s"External login failed: $failed") *> SeeOther(location) } } yield resp diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ClientSettingsRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ClientSettingsRoutes.scala index 96dad4b4..86ea63b4 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ClientSettingsRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ClientSettingsRoutes.scala @@ -27,8 +27,7 @@ object ClientSettingsRoutes { backend: BackendApp[F], token: ShareToken ): HttpRoutes[F] = { - val logger = Logger.log4s[F](org.log4s.getLogger) - + val logger = docspell.logging.getLogger[F] val dsl = new Http4sDsl[F] {} import dsl._ diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala index 7c2887fb..373ac8af 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala @@ -22,16 +22,15 @@ import org.http4s.HttpRoutes import org.http4s.circe.CirceEntityDecoder._ import org.http4s.circe.CirceEntityEncoder._ import org.http4s.dsl.Http4sDsl -import org.log4s.getLogger object ItemMultiRoutes extends NonEmptyListSupport with MultiIdSupport { - private[this] val log4sLogger = getLogger def apply[F[_]: Async]( cfg: Config, backend: BackendApp[F], user: AuthToken ): HttpRoutes[F] = { + val logger = docspell.logging.getLogger[F] val dsl = new Http4sDsl[F] {} import dsl._ @@ -236,7 +235,6 @@ object ItemMultiRoutes extends NonEmptyListSupport with MultiIdSupport { for { json <- req.as[IdList] items <- requireNonEmpty(json.ids) - logger = Logger.log4s(log4sLogger) res <- backend.item.merge(logger, items, user.account.collective) resp <- Ok(Conversions.basicResult(res, "Items merged")) } yield resp diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala index b4b3c0f6..7d46e83e 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala @@ -18,7 +18,6 @@ import docspell.backend.ops.OItemSearch.{Batch, Query} import docspell.backend.ops.OSimpleSearch import docspell.backend.ops.OSimpleSearch.StringSearchResult import docspell.common._ -import docspell.common.syntax.all._ import docspell.query.FulltextExtract.Result.TooMany import docspell.query.FulltextExtract.Result.UnsupportedPosition import docspell.restapi.model._ @@ -34,16 +33,14 @@ import org.http4s.circe.CirceEntityEncoder._ import org.http4s.dsl.Http4sDsl import org.http4s.headers._ import org.http4s.{HttpRoutes, Response} -import org.log4s._ object ItemRoutes { - private[this] val logger = getLogger - def apply[F[_]: Async]( cfg: Config, backend: BackendApp[F], user: AuthToken ): HttpRoutes[F] = { + val logger = docspell.logging.getLogger[F] val dsl = new Http4sDsl[F] {} import dsl._ @@ -322,7 +319,7 @@ object ItemRoutes { case req @ PUT -> Root / Ident(id) / "duedate" => for { date <- req.as[OptionalDate] - _ <- logger.fdebug(s"Setting item due date to ${date.date}") + _ <- logger.debug(s"Setting item due date to ${date.date}") res <- backend.item.setItemDueDate( NonEmptyList.of(id), date.date, @@ -334,7 +331,7 @@ object ItemRoutes { case req @ PUT -> Root / Ident(id) / "date" => for { date <- req.as[OptionalDate] - _ <- logger.fdebug(s"Setting item date to ${date.date}") + _ <- logger.debug(s"Setting item date to ${date.date}") res <- backend.item.setItemDate( NonEmptyList.of(id), date.date, @@ -353,7 +350,7 @@ object ItemRoutes { case req @ POST -> Root / Ident(id) / "attachment" / "movebefore" => for { data <- req.as[MoveAttachment] - _ <- logger.fdebug(s"Move item (${id.id}) attachment $data") + _ <- logger.debug(s"Move item (${id.id}) attachment $data") res <- backend.item.moveAttachmentBefore(id, data.source, data.target) resp <- Ok(Conversions.basicResult(res, "Attachment moved.")) } yield resp @@ -390,7 +387,7 @@ object ItemRoutes { case req @ POST -> Root / Ident(id) / "reprocess" => for { data <- req.as[IdList] - _ <- logger.fdebug(s"Re-process item ${id.id}") + _ <- logger.debug(s"Re-process item ${id.id}") res <- backend.item.reprocess(id, data.ids, user.account, true) resp <- Ok(Conversions.basicResult(res, "Re-process task submitted.")) } yield resp diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/NotificationRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/NotificationRoutes.scala index cdd4370c..fd244fc2 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/NotificationRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/NotificationRoutes.scala @@ -135,7 +135,8 @@ object NotificationRoutes extends NonEmptyListSupport { user.account, baseUrl.some ) - resp <- Ok(NotificationChannelTestResult(res.success, res.logMessages.toList)) + messages = res.logEvents.map(_.asString) + resp <- Ok(NotificationChannelTestResult(res.success, messages.toList)) } yield resp } } diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/PersonRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/PersonRoutes.scala index ab2046f3..0f893571 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/PersonRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/PersonRoutes.scala @@ -14,7 +14,6 @@ import docspell.backend.BackendApp import docspell.backend.auth.AuthToken import docspell.backend.ops.OOrganization import docspell.common.Ident -import docspell.common.syntax.all._ import docspell.restapi.model._ import docspell.restserver.conv.Conversions._ import docspell.restserver.http4s.QueryParam @@ -23,12 +22,11 @@ import org.http4s.HttpRoutes import org.http4s.circe.CirceEntityDecoder._ import org.http4s.circe.CirceEntityEncoder._ import org.http4s.dsl.Http4sDsl -import org.log4s._ object PersonRoutes { - private[this] val logger = getLogger def apply[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = { + val logger = docspell.logging.getLogger[F] val dsl = new Http4sDsl[F] {} import dsl._ @@ -73,7 +71,7 @@ object PersonRoutes { case DELETE -> Root / Ident(id) => for { - _ <- logger.fdebug(s"Deleting person ${id.id}") + _ <- logger.debug(s"Deleting person ${id.id}") delOrg <- backend.organization.deletePerson(id, user.account.collective) resp <- Ok(basicResult(delOrg, "Person deleted.")) } yield resp diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala index 64e14a5e..806691b1 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala @@ -33,7 +33,7 @@ object ShareSearchRoutes { cfg: Config, token: ShareToken ): HttpRoutes[F] = { - val logger = Logger.log4s[F](org.log4s.getLogger) + val logger = docspell.logging.getLogger[F] val dsl = new Http4sDsl[F] {} import dsl._ diff --git a/modules/restserver/src/main/scala/docspell/restserver/webapp/TemplateRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/webapp/TemplateRoutes.scala index 43ed9a5b..85a43fda 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/webapp/TemplateRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/webapp/TemplateRoutes.scala @@ -21,12 +21,10 @@ import org.http4s._ import org.http4s.dsl.Http4sDsl import org.http4s.headers._ import org.http4s.implicits._ -import org.log4s._ import yamusca.implicits._ import yamusca.imports._ object TemplateRoutes { - private[this] val logger = getLogger private val textHtml = mediaType"text/html" private val appJavascript = mediaType"application/javascript" @@ -99,11 +97,12 @@ object TemplateRoutes { def parseTemplate[F[_]: Sync](str: String): F[Template] = Sync[F].pure(mustache.parse(str).leftMap(err => new Exception(err._2))).rethrow - def loadTemplate[F[_]: Sync](url: URL): F[Template] = - loadUrl[F](url).flatMap(parseTemplate[F]).map { t => - logger.info(s"Compiled template $url") - t + def loadTemplate[F[_]: Sync](url: URL): F[Template] = { + val logger = docspell.logging.getLogger[F] + loadUrl[F](url).flatMap(parseTemplate[F]).flatMap { t => + logger.info(s"Compiled template $url") *> t.pure[F] } + } case class DocData(swaggerRoot: String, openapiSpec: String) object DocData { diff --git a/modules/store/src/main/scala/docspell/store/file/BinnyUtils.scala b/modules/store/src/main/scala/docspell/store/file/BinnyUtils.scala index 71d426d5..eef07da3 100644 --- a/modules/store/src/main/scala/docspell/store/file/BinnyUtils.scala +++ b/modules/store/src/main/scala/docspell/store/file/BinnyUtils.scala @@ -9,6 +9,7 @@ package docspell.store.file import docspell.common import docspell.common._ import docspell.files.TikaMimetype +import docspell.logging.Logger import binny._ import scodec.bits.ByteVector diff --git a/modules/store/src/main/scala/docspell/store/file/FileRepository.scala b/modules/store/src/main/scala/docspell/store/file/FileRepository.scala index 7eb73f12..b3da6da3 100644 --- a/modules/store/src/main/scala/docspell/store/file/FileRepository.scala +++ b/modules/store/src/main/scala/docspell/store/file/FileRepository.scala @@ -32,7 +32,6 @@ trait FileRepository[F[_]] { } object FileRepository { - private[this] val logger = org.log4s.getLogger def genericJDBC[F[_]: Sync]( xa: Transactor[F], @@ -41,7 +40,7 @@ object FileRepository { ): FileRepository[F] = { val attrStore = new AttributeStore[F](xa) val cfg = JdbcStoreConfig("filechunk", chunkSize, BinnyUtils.TikaContentTypeDetect) - val log = Logger.log4s[F](logger) + val log = docspell.logging.getLogger[F] val binStore = GenericJdbcStore[F](ds, BinnyUtils.LoggerAdapter(log), cfg, attrStore) val keyFun: FileKey => BinaryId = BinnyUtils.fileKeyToBinaryId diff --git a/modules/store/src/main/scala/docspell/store/queries/QAttachment.scala b/modules/store/src/main/scala/docspell/store/queries/QAttachment.scala index 7768a25c..5cdcafb3 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QAttachment.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QAttachment.scala @@ -13,7 +13,6 @@ import cats.implicits._ import fs2.Stream import docspell.common._ -import docspell.common.syntax.all._ import docspell.store.Store import docspell.store.qb.DSL._ import docspell.store.qb._ @@ -22,8 +21,6 @@ import docspell.store.records._ import doobie._ object QAttachment { - private[this] val logger = org.log4s.getLogger - private val a = RAttachment.as("a") private val item = RItem.as("i") private val am = RAttachmentMeta.as("am") @@ -79,13 +76,14 @@ object QAttachment { * item and should not be used to delete a *single* attachment where the item should * stay. */ - private def deleteAttachment[F[_]: Sync](store: Store[F])(ra: RAttachment): F[Int] = + private def deleteAttachment[F[_]: Sync](store: Store[F])(ra: RAttachment): F[Int] = { + val logger = docspell.logging.getLogger[F] for { - _ <- logger.fdebug[F](s"Deleting attachment: ${ra.id.id}") + _ <- logger.debug(s"Deleting attachment: ${ra.id.id}") s <- store.transact(RAttachmentSource.findById(ra.id)) p <- store.transact(RAttachmentPreview.findById(ra.id)) n <- store.transact(RAttachment.delete(ra.id)) - _ <- logger.fdebug[F]( + _ <- logger.debug( s"Deleted $n meta records (source, meta, preview, archive). Deleting binaries now." ) f <- @@ -96,6 +94,7 @@ object QAttachment { .compile .foldMonoid } yield n + f + } def deleteArchive[F[_]: Sync](store: Store[F])(attachId: Ident): F[Int] = (for { @@ -112,16 +111,18 @@ object QAttachment { def deleteItemAttachments[F[_]: Sync]( store: Store[F] - )(itemId: Ident, coll: Ident): F[Int] = + )(itemId: Ident, coll: Ident): F[Int] = { + val logger = docspell.logging.getLogger[F] for { ras <- store.transact(RAttachment.findByItemAndCollective(itemId, coll)) - _ <- logger.finfo[F]( + _ <- logger.info( s"Have ${ras.size} attachments to delete. Must first delete archive entries" ) a <- ras.traverse(a => deleteArchive(store)(a.id)) - _ <- logger.fdebug[F](s"Deleted ${a.sum} archive entries") + _ <- logger.debug(s"Deleted ${a.sum} archive entries") ns <- ras.traverse(deleteAttachment[F](store)) } yield ns.sum + } def getMetaProposals(itemId: Ident, coll: Ident): ConnectionIO[MetaProposalList] = { val qa = Select( diff --git a/modules/store/src/main/scala/docspell/store/queries/QItem.scala b/modules/store/src/main/scala/docspell/store/queries/QItem.scala index 3b5d0c19..66203292 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QItem.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QItem.scala @@ -14,7 +14,6 @@ import cats.effect.Sync import cats.implicits._ import fs2.Stream -import docspell.common.syntax.all._ import docspell.common.{FileKey, IdRef, _} import docspell.query.ItemQuery import docspell.store.Store @@ -25,10 +24,9 @@ import docspell.store.records._ import doobie.implicits._ import doobie.{Query => _, _} -import org.log4s.getLogger object QItem { - private[this] val logger = getLogger + private[this] val logger = docspell.logging.getLogger[ConnectionIO] private val equip = REquipment.as("e") private val org = ROrganization.as("o") @@ -81,7 +79,7 @@ object QItem { ) ] .option - logger.trace(s"Find item query: $cq") + logger.asUnsafe.trace(s"Find item query: $cq") val attachs = RAttachment.findByItemWithMeta(id) val sources = RAttachmentSource.findByItemWithMeta(id) val archives = RAttachmentArchive.findByItemWithMeta(id) @@ -181,8 +179,8 @@ object QItem { .changeWhere(c => c && queryCondition(today, q.fix.account.collective, q.cond)) .limit(batch) .build - logger.trace(s"List $batch items: $sql") - sql.query[ListItem].stream + logger.stream.trace(s"List $batch items: $sql").drain ++ + sql.query[ListItem].stream } def searchStats(today: LocalDate)(q: Query): ConnectionIO[SearchSummary] = @@ -359,8 +357,7 @@ object QItem { query.attemptSql.flatMap { case Right(res) => res.pure[ConnectionIO] case Left(ex) => - Logger - .log4s[ConnectionIO](logger) + logger .error(ex)( s"Calculating custom field summary failed. You may have invalid custom field values according to their type." ) *> @@ -405,8 +402,8 @@ object QItem { .orderBy(Tids.weight.desc) .build - logger.trace(s"fts query: $from") - from.query[ListItem].stream + logger.stream.trace(s"fts query: $from").drain ++ + from.query[ListItem].stream } /** Same as `findItems` but resolves the tags for each item. Note that this is @@ -515,8 +512,8 @@ object QItem { excludeFileMeta: Set[FileKey] ): ConnectionIO[Vector[RItem]] = { val qq = findByChecksumQuery(checksum, collective, excludeFileMeta).build - logger.debug(s"FindByChecksum: $qq") - qq.query[RItem].to[Vector] + logger.debug(s"FindByChecksum: $qq") *> + qq.query[RItem].to[Vector] } def findByChecksumQuery( @@ -695,7 +692,7 @@ object QItem { private def contentMax(maxLen: Int): SelectExpr = if (maxLen <= 0) { - logger.debug("Max text length limit disabled") + logger.asUnsafe.debug("Max text length limit disabled") m.content.s } else substring(m.content.s, 0, maxLen).s @@ -703,11 +700,11 @@ object QItem { q: Select ): ConnectionIO[TextAndTag] = for { - _ <- logger.ftrace[ConnectionIO]( + _ <- logger.trace( s"query: $q (${itemId.id}, ${collective.id})" ) texts <- q.build.query[(String, Option[TextAndTag.TagName])].to[List] - _ <- logger.ftrace[ConnectionIO]( + _ <- logger.trace( s"Got ${texts.size} text and tag entries for item ${itemId.id}" ) tag = texts.headOption.flatMap(_._2) diff --git a/modules/store/src/main/scala/docspell/store/queries/QJob.scala b/modules/store/src/main/scala/docspell/store/queries/QJob.scala index f005b40c..a172540b 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QJob.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QJob.scala @@ -12,7 +12,6 @@ import cats.implicits._ import fs2.Stream import docspell.common._ -import docspell.common.syntax.all._ import docspell.store.Store import docspell.store.qb.DSL._ import docspell.store.qb._ @@ -20,10 +19,9 @@ import docspell.store.records.{RJob, RJobGroupUse, RJobLog} import doobie._ import doobie.implicits._ -import org.log4s._ object QJob { - private[this] val logger = getLogger + private[this] val cioLogger = docspell.logging.getLogger[ConnectionIO] def takeNextJob[F[_]: Async]( store: Store[F] @@ -31,13 +29,14 @@ object QJob { priority: Ident => F[Priority], worker: Ident, retryPause: Duration - ): F[Option[RJob]] = + ): F[Option[RJob]] = { + val logger = docspell.logging.getLogger[F] Stream .range(0, 10) .evalMap(n => takeNextJob1(store)(priority, worker, retryPause, n)) .evalTap { x => if (x.isLeft) - logger.fdebug[F]( + logger.debug( "Cannot mark job, probably due to concurrent updates. Will retry." ) else ().pure[F] @@ -48,12 +47,13 @@ object QJob { Stream.emit(job) case Left(_) => Stream - .eval(logger.fwarn[F]("Cannot mark job, even after retrying. Give up.")) + .eval(logger.warn("Cannot mark job, even after retrying. Give up.")) .map(_ => None) } .compile .last .map(_.flatten) + } private def takeNextJob1[F[_]: Async](store: Store[F])( priority: Ident => F[Priority], @@ -61,6 +61,7 @@ object QJob { retryPause: Duration, currentTry: Int ): F[Either[Unit, Option[RJob]]] = { + val logger = docspell.logging.getLogger[F] // if this fails, we have to restart takeNextJob def markJob(job: RJob): F[Either[Unit, RJob]] = store.transact(for { @@ -68,25 +69,25 @@ object QJob { _ <- if (n == 1) RJobGroupUse.setGroup(RJobGroupUse(job.group, worker)) else 0.pure[ConnectionIO] - _ <- logger.fdebug[ConnectionIO]( + _ <- cioLogger.debug( s"Scheduled job ${job.info} to worker ${worker.id}" ) } yield if (n == 1) Right(job) else Left(())) for { - _ <- logger.ftrace[F]( + _ <- logger.trace( s"About to take next job (worker ${worker.id}), try $currentTry" ) now <- Timestamp.current[F] group <- store.transact(selectNextGroup(worker, now, retryPause)) - _ <- logger.ftrace[F](s"Choose group ${group.map(_.id)}") + _ <- logger.trace(s"Choose group ${group.map(_.id)}") prio <- group.map(priority).getOrElse((Priority.Low: Priority).pure[F]) - _ <- logger.ftrace[F](s"Looking for job of prio $prio") + _ <- logger.trace(s"Looking for job of prio $prio") job <- group .map(g => store.transact(selectNextJob(g, prio, retryPause, now))) .getOrElse((None: Option[RJob]).pure[F]) - _ <- logger.ftrace[F](s"Found job: ${job.map(_.info)}") + _ <- logger.trace(s"Found job: ${job.map(_.info)}") res <- job.traverse(j => markJob(j)) } yield res.map(_.map(_.some)).getOrElse { if (group.isDefined) @@ -138,7 +139,7 @@ object QJob { .limit(1) val frag = groups.build - logger.trace( + cioLogger.trace( s"nextGroupQuery: $frag (now=${now.toMillis}, pause=${initialPause.millis})" ) @@ -206,7 +207,8 @@ object QJob { _ <- store.transact(RJob.setRunning(id, workerId, now)) } yield () - def setFinalState[F[_]: Async](id: Ident, state: JobState, store: Store[F]): F[Unit] = + def setFinalState[F[_]: Async](id: Ident, state: JobState, store: Store[F]): F[Unit] = { + val logger = docspell.logging.getLogger[F] state match { case JobState.Success => setSuccess(id, store) @@ -217,8 +219,9 @@ object QJob { case JobState.Stuck => setStuck(id, store) case _ => - logger.ferror[F](s"Invalid final state: $state.") + logger.error(s"Invalid final state: $state.") } + } def exceedsRetries[F[_]: Async](id: Ident, max: Int, store: Store[F]): F[Boolean] = store.transact(RJob.getRetries(id)).map(n => n.forall(_ >= max)) diff --git a/modules/store/src/main/scala/docspell/store/queries/QUser.scala b/modules/store/src/main/scala/docspell/store/queries/QUser.scala index 6000016a..28c817c4 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QUser.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QUser.scala @@ -16,7 +16,7 @@ import docspell.store.records._ import doobie._ object QUser { - private val logger = Logger.log4s[ConnectionIO](org.log4s.getLogger) + private[this] val logger = docspell.logging.getLogger[ConnectionIO] final case class UserData( ownedFolders: List[Ident], diff --git a/modules/store/src/main/scala/docspell/store/queue/JobQueue.scala b/modules/store/src/main/scala/docspell/store/queue/JobQueue.scala index feb59f60..9b777086 100644 --- a/modules/store/src/main/scala/docspell/store/queue/JobQueue.scala +++ b/modules/store/src/main/scala/docspell/store/queue/JobQueue.scala @@ -14,8 +14,6 @@ import docspell.store.Store import docspell.store.queries.QJob import docspell.store.records.RJob -import org.log4s.getLogger - trait JobQueue[F[_]] { /** Inserts the job into the queue to get picked up as soon as possible. The job must @@ -44,7 +42,7 @@ trait JobQueue[F[_]] { object JobQueue { def apply[F[_]: Async](store: Store[F]): Resource[F, JobQueue[F]] = Resource.pure[F, JobQueue[F]](new JobQueue[F] { - private[this] val logger = Logger.log4s(getLogger) + private[this] val logger = docspell.logging.getLogger[F] def nextJob( prio: Ident => F[Priority], diff --git a/modules/store/src/main/scala/docspell/store/queue/PeriodicTaskStore.scala b/modules/store/src/main/scala/docspell/store/queue/PeriodicTaskStore.scala index 8213f319..f1fad91f 100644 --- a/modules/store/src/main/scala/docspell/store/queue/PeriodicTaskStore.scala +++ b/modules/store/src/main/scala/docspell/store/queue/PeriodicTaskStore.scala @@ -10,13 +10,10 @@ import cats.effect._ import cats.implicits._ import docspell.common._ -import docspell.common.syntax.all._ import docspell.store.queries.QPeriodicTask import docspell.store.records._ import docspell.store.{AddResult, Store} -import org.log4s.getLogger - trait PeriodicTaskStore[F[_]] { /** Get the free periodic task due next and reserve it to the given worker. @@ -44,11 +41,10 @@ trait PeriodicTaskStore[F[_]] { } object PeriodicTaskStore { - private[this] val logger = getLogger def create[F[_]: Sync](store: Store[F]): Resource[F, PeriodicTaskStore[F]] = Resource.pure[F, PeriodicTaskStore[F]](new PeriodicTaskStore[F] { - + private[this] val logger = docspell.logging.getLogger[F] def takeNext( worker: Ident, excludeId: Option[Ident] @@ -91,7 +87,7 @@ object PeriodicTaskStore { store .transact(QPeriodicTask.clearWorkers(name)) .flatMap { n => - if (n > 0) logger.finfo(s"Clearing $n periodic tasks from worker ${name.id}") + if (n > 0) logger.info(s"Clearing $n periodic tasks from worker ${name.id}") else ().pure[F] } diff --git a/modules/store/src/main/scala/docspell/store/records/RNotificationChannel.scala b/modules/store/src/main/scala/docspell/store/records/RNotificationChannel.scala index 586bd82a..94aa89c4 100644 --- a/modules/store/src/main/scala/docspell/store/records/RNotificationChannel.scala +++ b/modules/store/src/main/scala/docspell/store/records/RNotificationChannel.scala @@ -192,7 +192,7 @@ object RNotificationChannel { ): OptionT[ConnectionIO, RNotificationChannel] = for { time <- OptionT.liftF(Timestamp.current[ConnectionIO]) - logger = Logger.log4s[ConnectionIO](org.log4s.getLogger) + logger = docspell.logging.getLogger[ConnectionIO] r <- channel match { case Channel.Mail(_, name, conn, recipients) => diff --git a/modules/store/src/test/resources/logback-test.xml b/modules/store/src/test/resources/logback-test.xml deleted file mode 100644 index 5a9588a0..00000000 --- a/modules/store/src/test/resources/logback-test.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - true - - - level=%-5level thread=%thread logger=%logger{15} message="%replace(%msg){'"', '\\"'}"%n - - - - - - - - diff --git a/modules/store/src/test/scala/docspell/store/migrate/H2MigrateTest.scala b/modules/store/src/test/scala/docspell/store/migrate/H2MigrateTest.scala index 569f6b0f..df03453f 100644 --- a/modules/store/src/test/scala/docspell/store/migrate/H2MigrateTest.scala +++ b/modules/store/src/test/scala/docspell/store/migrate/H2MigrateTest.scala @@ -9,11 +9,12 @@ package docspell.store.migrate import cats.effect.IO import cats.effect.unsafe.implicits._ +import docspell.logging.TestLoggingConfig import docspell.store.StoreFixture import munit.FunSuite -class H2MigrateTest extends FunSuite { +class H2MigrateTest extends FunSuite with TestLoggingConfig { test("h2 empty schema migration") { val jdbc = StoreFixture.memoryDB("h2test") diff --git a/modules/store/src/test/scala/docspell/store/migrate/MariaDbMigrateTest.scala b/modules/store/src/test/scala/docspell/store/migrate/MariaDbMigrateTest.scala index 76d443fd..321a1b4d 100644 --- a/modules/store/src/test/scala/docspell/store/migrate/MariaDbMigrateTest.scala +++ b/modules/store/src/test/scala/docspell/store/migrate/MariaDbMigrateTest.scala @@ -10,6 +10,7 @@ import cats.effect._ import cats.effect.unsafe.implicits._ import docspell.common.LenientUri +import docspell.logging.TestLoggingConfig import docspell.store.JdbcConfig import com.dimafeng.testcontainers.MariaDBContainer @@ -17,7 +18,10 @@ import com.dimafeng.testcontainers.munit.TestContainerForAll import munit._ import org.testcontainers.utility.DockerImageName -class MariaDbMigrateTest extends FunSuite with TestContainerForAll { +class MariaDbMigrateTest + extends FunSuite + with TestContainerForAll + with TestLoggingConfig { override val containerDef: MariaDBContainer.Def = MariaDBContainer.Def(DockerImageName.parse("mariadb:10.5")) diff --git a/modules/store/src/test/scala/docspell/store/migrate/PostgresqlMigrateTest.scala b/modules/store/src/test/scala/docspell/store/migrate/PostgresqlMigrateTest.scala index 1125f69a..9decab2f 100644 --- a/modules/store/src/test/scala/docspell/store/migrate/PostgresqlMigrateTest.scala +++ b/modules/store/src/test/scala/docspell/store/migrate/PostgresqlMigrateTest.scala @@ -10,6 +10,7 @@ import cats.effect._ import cats.effect.unsafe.implicits._ import docspell.common.LenientUri +import docspell.logging.TestLoggingConfig import docspell.store.JdbcConfig import com.dimafeng.testcontainers.PostgreSQLContainer @@ -17,7 +18,10 @@ import com.dimafeng.testcontainers.munit.TestContainerForAll import munit._ import org.testcontainers.utility.DockerImageName -class PostgresqlMigrateTest extends FunSuite with TestContainerForAll { +class PostgresqlMigrateTest + extends FunSuite + with TestContainerForAll + with TestLoggingConfig { override val containerDef: PostgreSQLContainer.Def = PostgreSQLContainer.Def(DockerImageName.parse("postgres:13")) diff --git a/modules/store/src/test/scala/docspell/store/qb/QueryBuilderTest.scala b/modules/store/src/test/scala/docspell/store/qb/QueryBuilderTest.scala index d42d263f..a36afeff 100644 --- a/modules/store/src/test/scala/docspell/store/qb/QueryBuilderTest.scala +++ b/modules/store/src/test/scala/docspell/store/qb/QueryBuilderTest.scala @@ -6,12 +6,13 @@ package docspell.store.qb +import docspell.logging.TestLoggingConfig import docspell.store.qb.DSL._ import docspell.store.qb.model._ import munit._ -class QueryBuilderTest extends FunSuite { +class QueryBuilderTest extends FunSuite with TestLoggingConfig { test("simple") { val c = CourseRecord.as("c") diff --git a/modules/store/src/test/scala/docspell/store/qb/impl/SelectBuilderTest.scala b/modules/store/src/test/scala/docspell/store/qb/impl/SelectBuilderTest.scala index 55a8f601..0ba3c7a5 100644 --- a/modules/store/src/test/scala/docspell/store/qb/impl/SelectBuilderTest.scala +++ b/modules/store/src/test/scala/docspell/store/qb/impl/SelectBuilderTest.scala @@ -6,13 +6,14 @@ package docspell.store.qb.impl +import docspell.logging.TestLoggingConfig import docspell.store.qb.DSL._ import docspell.store.qb._ import docspell.store.qb.model._ import munit._ -class SelectBuilderTest extends FunSuite { +class SelectBuilderTest extends FunSuite with TestLoggingConfig { test("basic fragment") { val c = CourseRecord.as("c") diff --git a/modules/store/src/test/scala/docspell/store/queries/QJobTest.scala b/modules/store/src/test/scala/docspell/store/queries/QJobTest.scala index cd439777..8c60f240 100644 --- a/modules/store/src/test/scala/docspell/store/queries/QJobTest.scala +++ b/modules/store/src/test/scala/docspell/store/queries/QJobTest.scala @@ -12,6 +12,7 @@ import java.util.concurrent.atomic.AtomicLong import cats.implicits._ import docspell.common._ +import docspell.logging.TestLoggingConfig import docspell.store.StoreFixture import docspell.store.records.RJob import docspell.store.records.RJobGroupUse @@ -19,7 +20,7 @@ import docspell.store.records.RJobGroupUse import doobie.implicits._ import munit._ -class QJobTest extends CatsEffectSuite with StoreFixture { +class QJobTest extends CatsEffectSuite with StoreFixture with TestLoggingConfig { private[this] val c = new AtomicLong(0) private val worker = Ident.unsafe("joex1") diff --git a/modules/webapp/src/main/elm/Comp/ItemCardList.elm b/modules/webapp/src/main/elm/Comp/ItemCardList.elm index 7899ac7c..f9f2967b 100644 --- a/modules/webapp/src/main/elm/Comp/ItemCardList.elm +++ b/modules/webapp/src/main/elm/Comp/ItemCardList.elm @@ -208,7 +208,9 @@ itemContainerCss : ViewConfig -> String itemContainerCss cfg = case cfg.arrange of Data.ItemArrange.Cards -> - "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-2" + "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 " + ++ "xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 4xl:grid-cols-6 " + ++ "5xl:grid-cols-8 6xl:grid-cols-10 gap-2" Data.ItemArrange.List -> "flex flex-col divide-y" diff --git a/modules/webapp/tailwind.config.js b/modules/webapp/tailwind.config.js index 5b8acf26..3e25d8b8 100644 --- a/modules/webapp/tailwind.config.js +++ b/modules/webapp/tailwind.config.js @@ -8,6 +8,17 @@ module.exports = { "./src/main/styles/keep.txt", "../restserver/src/main/templates/*.html" ], + theme: { + extend: { + screens: { + '3xl': '1792px', + '4xl': '2048px', + '5xl': '2560px', + '6xl': '3072px', + '7xl': '3584px' + } + } + }, variants: { extend: { backgroundOpacity: ['dark'] diff --git a/nix/module-joex.nix b/nix/module-joex.nix index d9285bee..99387dd1 100644 --- a/nix/module-joex.nix +++ b/nix/module-joex.nix @@ -16,6 +16,10 @@ let address = "localhost"; port = 7878; }; + logging = { + minimum-level = "Info"; + format = "Plain"; + }; mail-debug = false; jdbc = { url = "jdbc:h2:///tmp/docspell-demo.db;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;AUTO_SERVER=TRUE"; @@ -286,6 +290,26 @@ in { default = defaults.bind; description = "Address and port bind the rest server."; }; + + logging = mkOption { + type = types.submodule({ + options = { + minimum-level = mkOption { + type = types.str; + default = defaults.logging.minimum-level; + description = "The minimum level for logging to control verbosity."; + }; + format = mkOption { + type = types.str; + default = defaults.logging.format; + description = "The log format. One of: Fancy, Plain, Json or Logfmt"; + }; + }; + }); + default = defaults.logging; + description = "Settings for logging"; + }; + mail-debug = mkOption { type = types.bool; default = defaults.mail-debug; diff --git a/nix/module-server.nix b/nix/module-server.nix index a8a886f8..f0413a4f 100644 --- a/nix/module-server.nix +++ b/nix/module-server.nix @@ -21,6 +21,10 @@ let address = "localhost"; port = 7880; }; + logging = { + minimum-level = "Info"; + format = "Plain"; + }; integration-endpoint = { enabled = false; priority = "low"; @@ -210,6 +214,25 @@ in { description = "Address and port bind the rest server."; }; + logging = mkOption { + type = types.submodule({ + options = { + minimum-level = mkOption { + type = types.str; + default = defaults.logging.minimum-level; + description = "The minimum level for logging to control verbosity."; + }; + format = mkOption { + type = types.str; + default = defaults.logging.format; + description = "The log format. One of: Fancy, Plain, Json or Logfmt"; + }; + }; + }); + default = defaults.logging; + description = "Settings for logging"; + }; + auth = mkOption { type = types.submodule({ options = { diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 3c734a39..28bf6aeb 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -9,6 +9,8 @@ object Dependencies { val BetterMonadicForVersion = "0.3.1" val BinnyVersion = "0.3.0" val CalevVersion = "0.6.1" + val CatsVersion = "2.7.0" + val CatsEffectVersion = "3.3.5" val CatsParseVersion = "0.3.6" val CirceVersion = "0.14.1" val ClipboardJsVersion = "2.0.6" @@ -40,7 +42,9 @@ object Dependencies { val PureConfigVersion = "0.17.1" val ScalaJavaTimeVersion = "2.3.0" val ScodecBitsVersion = "1.1.30" + val ScribeVersion = "3.7.1" val Slf4jVersion = "1.7.36" + val SourcecodeVersion = "0.2.8" val StanfordNlpVersion = "4.4.0" val TikaVersion = "2.3.0" val YamuscaVersion = "0.8.2" @@ -49,6 +53,15 @@ object Dependencies { val TwelveMonkeysVersion = "3.8.1" val JQueryVersion = "3.5.1" + val scribe = Seq( + "com.outr" %% "scribe" % ScribeVersion, + "com.outr" %% "scribe-slf4j" % ScribeVersion + ) + + val sourcecode = Seq( + "com.lihaoyi" %% "sourcecode" % SourcecodeVersion + ) + val jwtScala = Seq( "com.github.jwt-scala" %% "jwt-circe" % JwtScalaVersion ) @@ -67,6 +80,14 @@ object Dependencies { "com.dimafeng" %% "testcontainers-scala-postgresql" % TestContainerVersion ) + val cats = Seq( + "org.typelevel" %% "cats-core" % CatsVersion + ) + + val catsEffect = Seq( + "org.typelevel" %% "cats-effect" % CatsEffectVersion + ) + val catsParse = Seq( "org.typelevel" %% "cats-parse" % CatsParseVersion ) @@ -98,9 +119,9 @@ object Dependencies { val jclOverSlf4j = Seq( "org.slf4j" % "jcl-over-slf4j" % Slf4jVersion ) - val julOverSlf4j = Seq( - "org.slf4j" % "jul-to-slf4j" % Slf4jVersion - ) + // val julOverSlf4j = Seq( + // "org.slf4j" % "jul-to-slf4j" % Slf4jVersion + // ) val poi = Seq( "org.apache.poi" % "poi" % PoiVersion, @@ -210,10 +231,13 @@ object Dependencies { "org.mindrot" % "jbcrypt" % BcryptVersion ) - val fs2 = Seq( - "co.fs2" %% "fs2-core" % Fs2Version, + val fs2Core = Seq( + "co.fs2" %% "fs2-core" % Fs2Version + ) + val fs2Io = Seq( "co.fs2" %% "fs2-io" % Fs2Version ) + val fs2 = fs2Core ++ fs2Io val http4sClient = Seq( "org.http4s" %% "http4s-blaze-client" % Http4sVersion @@ -245,14 +269,14 @@ object Dependencies { "io.circe" %% "circe-generic-extras" % CirceVersion ) - // https://github.com/Log4s/log4s;ASL 2.0 - val loggingApi = Seq( - "org.log4s" %% "log4s" % Log4sVersion - ) + // // https://github.com/Log4s/log4s;ASL 2.0 + // val loggingApi = Seq( + // "org.log4s" %% "log4s" % Log4sVersion + // ) - val logging = Seq( - "ch.qos.logback" % "logback-classic" % LogbackVersion - ) + // val logging = Seq( + // "ch.qos.logback" % "logback-classic" % LogbackVersion + // ) // https://github.com/melrief/pureconfig // MPL 2.0 diff --git a/project/TestSettings.scala b/project/TestSettings.scala new file mode 100644 index 00000000..cf8909e1 --- /dev/null +++ b/project/TestSettings.scala @@ -0,0 +1,36 @@ +import sbt._ +import sbt.Keys._ +import docspell.build._ +import sbtcrossproject.CrossProject + +object TestSettingsPlugin extends AutoPlugin { + + object autoImport { + def inTest(d0: Seq[ModuleID], ds: Seq[ModuleID]*) = + ds.fold(d0)(_ ++ _).map(_ % Test) + + implicit class ProjectTestSettingsSyntax(project: Project) { + def withTestSettings = + project.settings(testSettings) + + def withTestSettingsDependsOn(p: Project, ps: Project*) = + (p :: ps.toList).foldLeft(project) { (cur, dep) => + cur.dependsOn(dep % "test->test,compile") + } + } + + implicit class CrossprojectTestSettingsSyntax(project: CrossProject) { + def withTestSettings = + project.settings(testSettings) + } + + } + + import autoImport._ + + val testSettings = Seq( + libraryDependencies ++= inTest(Dependencies.munit, Dependencies.scribe), + testFrameworks += new TestFramework("munit.Framework") + ) + +} diff --git a/project/plugins.sbt b/project/plugins.sbt index b0c4ba13..834d5717 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,7 +4,7 @@ addSbtPlugin("com.github.eikek" % "sbt-openapi-schema" % "0.9.0") addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0") addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.2") -addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.8") +addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.9") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.6.5") addSbtPlugin("io.kevinlee" % "sbt-github-pages" % "0.8.1") addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1") diff --git a/website/site/content/docs/configure/_index.md b/website/site/content/docs/configure/_index.md index 932b39e7..b57cf1ea 100644 --- a/website/site/content/docs/configure/_index.md +++ b/website/site/content/docs/configure/_index.md @@ -604,41 +604,18 @@ Please have a look at the corresponding [section](@/docs/configure/_index.md#mem # Logging By default, docspell logs to stdout. This works well, when managed by -systemd or other inits. Logging is done by -[logback](https://logback.qos.ch/). Please refer to its documentation -for how to configure logging. +systemd or other inits. Logging can be configured in the configuration +file or via environment variables. There are only two settings: -If you created your logback config file, it can be added as argument -to the executable using this syntax: - -``` bash -/path/to/docspell -Dlogback.configurationFile=/path/to/your/logging-config-file -``` - -To get started, the default config looks like this: - -``` xml - - - true - - - [%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n - - - - - - - - -``` - -The `` means, that only log statements with level -"INFO" will be printed. But the `` above says, that for loggers with name "docspell" -statements with level "DEBUG" will be printed, too. +- `minimum-level` specifies the log level to control the verbosity. + Levels are ordered from: *Trace*, *Debug*, *Info*, *Warn* and + *Error* +- `format` this defines how the logs are formatted. There are two + formats for humans: *Plain* and *Fancy*. And two more suited for + machine consumption: *Json* and *Logfmt*. The *Json* format contains + all details, while the others may omit some for readability +These settings are the same for joex and the restserver component. # Default Config ## Rest Server