Adopt to new loggin api

This commit is contained in:
eikek 2022-02-19 14:00:47 +01:00
parent 6442771270
commit e483a97de7
130 changed files with 634 additions and 662 deletions
build.sbt
modules
analysis/src
backend/src/main/scala/docspell/backend
common/src/main/scala/docspell/common
config/src/main/scala/docspell/config
convert/src
extract/src
fts-client/src/main/scala/docspell/ftsclient
fts-solr/src/main/scala/docspell/ftssolr
joex/src/main
joexapi/src/main/scala/docspell/joexapi/client
logging
notification
oidc/src/main/scala/docspell/oidc

133
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,44 +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
)
val logging = project
.in(file("modules/logging"))
.disablePlugins(RevolverPlugin)
.settings(sharedSettings)
.settings(testSettingsMUnit)
.settings(
name := "docspell-logging",
libraryDependencies ++=
Dependencies.scribe ++
Dependencies.catsEffect ++
Dependencies.circeCore
)
.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/
@ -346,7 +353,7 @@ val files = project
.in(file("modules/files"))
.disablePlugins(RevolverPlugin)
.settings(sharedSettings)
.settings(testSettingsMUnit)
.withTestSettings
.settings(
name := "docspell-files",
libraryDependencies ++=
@ -385,7 +392,7 @@ val query =
.in(file("modules/query"))
.disablePlugins(RevolverPlugin)
.settings(sharedSettings)
.settings(testSettingsMUnit)
.withTestSettings
.settings(
name := "docspell-query",
libraryDependencies +=
@ -405,7 +412,7 @@ val totp = project
.in(file("modules/totp"))
.disablePlugins(RevolverPlugin)
.settings(sharedSettings)
.settings(testSettingsMUnit)
.withTestSettings
.settings(
name := "docspell-totp",
libraryDependencies ++=
@ -419,7 +426,7 @@ val jsonminiq = project
.in(file("modules/jsonminiq"))
.disablePlugins(RevolverPlugin)
.settings(sharedSettings)
.settings(testSettingsMUnit)
.withTestSettings
.settings(
name := "docspell-jsonminiq",
libraryDependencies ++=
@ -432,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)
.withTestSettings
.settings(
name := "docspell-store",
addCompilerPlugin(Dependencies.kindProjectorPlugin),
libraryDependencies ++=
Dependencies.doobie ++
Dependencies.binny ++
@ -466,16 +471,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 ++
@ -492,10 +496,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
)
@ -505,10 +508,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 ++
@ -522,7 +524,7 @@ val extract = project
.in(file("modules/extract"))
.disablePlugins(RevolverPlugin)
.settings(sharedSettings)
.settings(testSettingsMUnit)
.withTestSettings
.settings(
name := "docspell-extract",
libraryDependencies ++=
@ -533,13 +535,13 @@ val extract = project
Dependencies.commonsIO ++
Dependencies.julOverSlf4j
)
.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)
.withTestSettings
.settings(
name := "docspell-convert",
libraryDependencies ++=
@ -554,7 +556,7 @@ val analysis = project
.disablePlugins(RevolverPlugin)
.enablePlugins(NerModelsPlugin)
.settings(sharedSettings)
.settings(testSettingsMUnit)
.withTestSettings
.settings(NerModelsPlugin.nerClassifierSettings)
.settings(
name := "docspell-analysis",
@ -562,24 +564,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 ++=
@ -595,7 +597,7 @@ val restapi = project
.disablePlugins(RevolverPlugin)
.enablePlugins(OpenApiSchema)
.settings(sharedSettings)
.settings(testSettingsMUnit)
.withTestSettings
.settings(openapiScalaSettings)
.settings(
name := "docspell-restapi",
@ -613,7 +615,7 @@ val joexapi = project
.disablePlugins(RevolverPlugin)
.enablePlugins(OpenApiSchema)
.settings(sharedSettings)
.settings(testSettingsMUnit)
.withTestSettings
.settings(openapiScalaSettings)
.settings(
name := "docspell-joexapi",
@ -626,13 +628,13 @@ 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 ++=
@ -642,13 +644,13 @@ val backend = project
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 ++=
@ -660,7 +662,7 @@ val oidc = project
Dependencies.circe ++
Dependencies.jwtScala
)
.dependsOn(common)
.dependsOn(common, loggingScribe)
val webapp = project
.in(file("modules/webapp"))
@ -691,7 +693,7 @@ val joex = project
ClasspathJarPlugin
)
.settings(sharedSettings)
.settings(testSettingsMUnit)
.withTestSettings
.settings(debianSettings("docspell-joex"))
.settings(buildInfoSettings)
.settings(
@ -714,7 +716,6 @@ val joex = project
Dependencies.yamusca ++
Dependencies.loggingApi ++
Dependencies.logging.map(_ % Runtime),
addCompilerPlugin(Dependencies.kindProjectorPlugin),
addCompilerPlugin(Dependencies.betterMonadicFor),
buildInfoPackage := "docspell.joex",
reStart / javaOptions ++= Seq(
@ -726,6 +727,8 @@ val joex = project
)
.dependsOn(
config,
loggingApi,
loggingScribe,
store,
backend,
extract,
@ -748,7 +751,7 @@ val restserver = project
ClasspathJarPlugin
)
.settings(sharedSettings)
.settings(testSettingsMUnit)
.withTestSettings
.settings(debianSettings("docspell-server"))
.settings(buildInfoSettings)
.settings(
@ -767,7 +770,6 @@ val restserver = project
Dependencies.webjars ++
Dependencies.loggingApi ++
Dependencies.logging.map(_ % Runtime),
addCompilerPlugin(Dependencies.kindProjectorPlugin),
addCompilerPlugin(Dependencies.betterMonadicFor),
buildInfoPackage := "docspell.restserver",
Compile / sourceGenerators += Def.task {
@ -801,6 +803,8 @@ val restserver = project
)
.dependsOn(
config,
loggingApi,
loggingScribe,
restapi,
joexapi,
backend,
@ -882,7 +886,8 @@ val root = project
)
.aggregate(
common,
logging,
loggingApi,
loggingScribe,
config,
extract,
convert,

@ -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,

@ -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

@ -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[_]] {

@ -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

@ -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)

@ -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)(_ =>

@ -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`.
*

@ -21,7 +21,7 @@ import docspell.common._
import munit._
class StanfordTextClassifierSuite extends FunSuite {
val logger = Logger.log4s[IO](org.log4s.getLogger)
val logger = docspell.logging.getLogger[IO]
test("learn from data") {
val cfg = TextClassifierConfig(File.path(Paths.get("target")), NonEmptyList.of(Map()))

@ -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]

@ -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

@ -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

@ -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)

@ -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)

@ -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)

@ -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(

@ -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

@ -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(_ => ())
})

@ -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])
}

@ -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,

@ -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 {

@ -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(

@ -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(

@ -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)

@ -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)
}

@ -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) {

@ -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))
}
}

@ -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
}

@ -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}

@ -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] =

@ -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))

@ -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

@ -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 {

@ -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 {

@ -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 {

@ -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 {

@ -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 {

@ -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.{Level, Logger}
import munit._
class ConversionTest extends FunSuite with FileChecks {
val logger = Logger.log4s[IO](org.log4s.getLogger)
val logger = Logger.simpleF[IO](System.err, Level.Info)
val target = File.path(Paths.get("target"))
val convertConfig = ConvertConfig(

@ -11,11 +11,12 @@ import fs2.Stream
import docspell.common._
import docspell.files.ExampleFiles
import docspell.logging.{Level, Logger}
import munit.CatsEffectSuite
class RemovePdfEncryptionTest extends CatsEffectSuite with FileChecks {
val logger: Logger[IO] = Logger.log4s(org.log4s.getLogger)
val logger: Logger[IO] = Logger.simpleF[IO](System.err, Level.Info)
private val protectedPdf =
ExampleFiles.secured_protected_test123_pdf.readURL[IO](16 * 1024)

@ -16,12 +16,13 @@ import fs2.io.file.Path
import docspell.common._
import docspell.convert._
import docspell.files.ExampleFiles
import docspell.logging.{Level, Logger}
import munit._
class ExternConvTest extends FunSuite with FileChecks {
val utf8 = StandardCharsets.UTF_8
val logger = Logger.log4s[IO](org.log4s.getLogger)
val logger = Logger.simpleF[IO](System.err, Level.Info)
val target = File.path(Paths.get("target"))
test("convert html to pdf") {

@ -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[_]] {

@ -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])

@ -11,6 +11,7 @@ import fs2.Stream
import fs2.io.file.Path
import docspell.common._
import docspell.logging.Logger
object Ocr {

@ -12,6 +12,7 @@ import fs2.Stream
import docspell.common._
import docspell.extract.internal.Text
import docspell.files._
import docspell.logging.Logger
object TextExtract {

@ -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] {

@ -9,7 +9,6 @@ package docspell.extract.ocr
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import docspell.common.Logger
import docspell.files.TestFiles
import munit._
@ -17,7 +16,7 @@ import munit._
class TextExtractionSuite extends FunSuite {
import TestFiles._
val logger = Logger.log4s[IO](org.log4s.getLogger)
val logger = docspell.logging.getLogger[IO]
test("extract english pdf".ignore) {
val text = TextExtract

@ -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)

@ -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)
}
}

@ -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 = "Json"
# The minimum level to log. From lowest to highest:
# Trace, Debug, Info, Warn, Error
minimumLevel = "Debug"
}
# The database connection.
#
# It must be the same connection as the rest server is using.

@ -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,

@ -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)
}

@ -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(

@ -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,

@ -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))

@ -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[_]](

@ -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._

@ -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.

@ -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

@ -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

@ -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 {

@ -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

@ -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._

@ -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

@ -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

@ -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)
}

@ -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))
}

@ -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._

@ -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,

@ -11,12 +11,9 @@ import cats.implicits._
import fs2.Pipe
import docspell.common._
import docspell.common.syntax.all._
import docspell.store.Store
import docspell.store.records.RJobLog
import org.log4s.{LogLevel => _, _}
trait LogSink[F[_]] {
def receive: Pipe[F, LogEvent, Unit]
@ -24,29 +21,30 @@ 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]
e.level match {
case LogLevel.Info =>
logger.finfo(e.logLine)
logger.info(e.logLine)
case LogLevel.Debug =>
logger.fdebug(e.logLine)
logger.debug(e.logLine)
case LogLevel.Warn =>
logger.fwarn(e.logLine)
logger.warn(e.logLine)
case LogLevel.Error =>
e.ex match {
case Some(exc) =>
logger.ferror(exc)(e.logLine)
logger.error(exc)(e.logLine)
case None =>
logger.ferror(e.logLine)
logger.error(e.logLine)
}
}
}
def printer[F[_]: Sync]: LogSink[F] =
LogSink(_.evalMap(e => logInternal(e)))

@ -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
}

@ -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)
}

@ -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.")
)
}

@ -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] {

@ -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"

@ -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))
}
}

@ -20,6 +20,9 @@ final case class LogEvent(
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)))
@ -28,6 +31,11 @@ final case class LogEvent(
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 {

@ -6,14 +6,18 @@
package docspell.logging
import cats.Id
import cats.effect.Sync
import java.io.PrintStream
import java.time.Instant
import docspell.logging.impl.LoggerWrapper
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[_]] {
trait Logger[F[_]] extends LoggerExtension[F] {
def log(ev: LogEvent): F[Unit]
@ -117,12 +121,46 @@ trait Logger[F[_]] {
}
object Logger {
def unsafe(name: String): Logger[Id] =
new LoggerWrapper.ImplUnsafe(scribe.Logger(name))
def off: Logger[Id] =
new Logger[Id] {
def log(ev: LogEvent): Unit = ()
def asUnsafe = this
}
def apply[F[_]: Sync](name: String): Logger[F] =
new LoggerWrapper.Impl[F](scribe.Logger(name))
def offF[F[_]: Applicative]: Logger[F] =
new Logger[F] {
def log(ev: LogEvent) = ().pure[F]
def asUnsafe = off
}
def apply[F[_]: Sync](clazz: Class[_]): Logger[F] =
new LoggerWrapper.Impl[F](scribe.Logger(clazz.getName))
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)
}
}

@ -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)
}

@ -7,10 +7,10 @@
package docspell.logging.impl
import io.circe.syntax._
import scribe.LogRecord
import scribe._
import scribe.output._
import scribe.output.format.OutputFormat
import scribe.output.{LogOutput, TextOutput}
import scribe.writer.Writer
import scribe.writer._
final case class JsonWriter(writer: Writer, compact: Boolean = true) extends Writer {
override def write[M](

@ -7,10 +7,10 @@
package docspell.logging.impl
import io.circe.syntax._
import scribe.LogRecord
import scribe._
import scribe.output._
import scribe.output.format.OutputFormat
import scribe.output.{LogOutput, TextOutput}
import scribe.writer.Writer
import scribe.writer._
// https://brandur.org/logfmt
final case class LogfmtWriter(writer: Writer) extends Writer {

@ -6,7 +6,7 @@
package docspell.logging.impl
import cats.effect._
import cats.effect.Sync
import docspell.logging.LogConfig
import docspell.logging.LogConfig.Format
@ -26,7 +26,7 @@ object ScribeConfigure {
def unsafeConfigure(logger: scribe.Logger, cfg: LogConfig): Unit = {
val mods = List[scribe.Logger => scribe.Logger](
_.clearHandlers(),
_.withMinimumLevel(LoggerWrapper.convertLevel(cfg.minimumLevel)),
_.withMinimumLevel(ScribeWrapper.convertLevel(cfg.minimumLevel)),
l =>
cfg.format match {
case Format.Fancy =>

@ -7,14 +7,14 @@
package docspell.logging.impl
import cats.Id
import cats.effect._
import cats.effect.Sync
import docspell.logging._
import docspell.logging.{Level, LogEvent, Logger}
import scribe.LoggerSupport
import scribe.message.{LoggableMessage, Message}
private[logging] object LoggerWrapper {
private[logging] object ScribeWrapper {
final class ImplUnsafe(log: scribe.Logger) extends Logger[Id] {
override def asUnsafe = this

@ -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))
}

@ -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
}
}

@ -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[_]] {

@ -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]

@ -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

@ -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

@ -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(())
}
}
}

@ -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

@ -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

@ -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

@ -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

@ -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}

@ -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

@ -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)
}
}

@ -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 {

Some files were not shown because too many files have changed in this diff Show More