This is an automated email from the ASF dual-hosted git repository.

slawrence pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/daffodil.git


The following commit(s) were added to refs/heads/main by this push:
     new dede0cf7d Use SLF4J for Daffodil logging, with custom backend for 
CLI/TDML
dede0cf7d is described below

commit dede0cf7dfaa3a7e67af5dabc7389192eb92c4d5
Author: Steve Lawrence <[email protected]>
AuthorDate: Thu Feb 9 08:57:59 2023 -0500

    Use SLF4J for Daffodil logging, with custom backend for CLI/TDML
    
    - Remove all uses of log4j. Instead, Daffodil now uses SLF4J (MIT
      license) via the scala-logging library (ALv2 licnse). This makes
      Daffodil more flexible as to which logging backend is used.
      Additionally, SLF4J has a simpler backend API, so creating custom
      backends is much more straightforward.
    
    - Implement a custom SLF4J backend via a custom ServiceProvider,
      LoggerFactory, and Logger that supports thread specific log Levels and
      PrintStreams. Note that each thread must explicitly configure the
      logger with the level and stream, or else this new backend silently
      drops log messages.
    
    - Put this new SLF4J backend on the CLI classpath so the CLI uses this
      thread safe logging, needed for parallel tests.
    
    - Put this new SLF4J backend on the TDML processor classpath. The TDML
      Runner does not currently configure this new logger, so logs are just
      dropped (essentially the same behavior as we have now), but this
      avoids error messages from SLF4J about not having a logger available.
      It also means in the future we can easily configure this logger to
      give the TDML runner the ability to capture and expect log messages.
      Additionally, this resolves errors when the log4j-api version did not
      match a log4j-core version defined in a DFDL schema project. The TDML
      runner now requires this custom backend, so users do no need to
      specify one.
    
    - Put this new SLF4J backend on all other subprojects in the test
      configuration. This way, if a unit test runs code hat logs, then it
      ensures a logger is available and avoids an SLF4J warning. Note
      however that since no tests configure the logger, the logs will be
      dropped. Also note that this is only for the "test" configuration, not
      "compile" or "runtime", and so API users must add a custom SFL4J
      backend or they will get a warning from SLF4J. This is similar to the
      existing behavior where users needed to add a log4j backend, but
      avoids API mismatches since SLF4J has better backwards compatibility.
    
    - Because the custom logger is used for the TDML runner as well, there
      are no longer any issues with DFDL schema projects depending on a
      different version of log4j than is used by Daffodil. If they already
      depend on log4j with this update, it will simply not be used.
    
    - Also correct subprojets made up of tdml tests to depend on tdmlProc in
      the "test" configuration. This doesn't really matter but is
      technically correct.
    
    - Change the CLI Main object to a class that can be instantiated with
      custom streams instead of sharing global mutable state. The Main
      object now just creates an instance using normal stdin/out/err, but
      tests create a Main instance using test/thread specific streams.
    
    - Modify the CLI to configure the custom SLF4J logger so different
      instances that are run in different threads can log to a different
      streams and levels.
    
    - These changes now make CLI tests completely thread safe, so parallel
      CLI tests are enabled.
    
    Backwards/Compatibility:
    
    Daffodil now uses SLF4J for logging instead of log4j. API users may need
    to add a new dependency to specify which logging backend to use, for
    example slf4j-log4j for the previous behavior. If no backend is found, a
    warning will be output to stderr and log messages will be dropped.
    
    DFDL schema projects that previously added log4j-core (or another log4j
    implementation) as a dependency to avoid warnings no longer need that
    dependency--a custom Daffodil specific logger is now used for TDML tests
    and is automatically pulled in as dependency.
    
    DAFFODIL-2778, DAFFODIL-2787
---
 DEVELOP.md                                         |  11 +-
 build.sbt                                          |  65 +++++--
 daffodil-cli/bin.LICENSE                           |   2 +-
 daffodil-cli/bin.NOTICE                            |  22 +--
 daffodil-cli/build.sbt                             |   7 +-
 daffodil-cli/src/conf/.keep                        |  14 ++
 daffodil-cli/src/conf/log4j2.xml                   |  30 ---
 .../main/scala/org/apache/daffodil/cli/Main.scala  | 110 +++++------
 daffodil-cli/src/templates/bash-template           |   1 -
 daffodil-cli/src/templates/bat-template            |   1 -
 .../org/apache/daffodil/cli/cliTest/Util.scala     |  54 +-----
 .../org/apache/daffodil/lib/util/Logger.scala      |  28 +--
 .../scala/org/apache/daffodil/lib/util/Timer.scala |   8 +-
 .../src/main/resources/META-INF/LICENSE            | 202 +++++++++++++++++++++
 .../src/main/resources/META-INF/NOTICE             |  10 +
 .../services/org.slf4j.spi.SLF4JServiceProvider    |  16 ++
 .../apahe/daffodil/slf4j/DaffodilSLF4JLogger.scala | 170 +++++++++++++++++
 project/Dependencies.scala                         |   9 +-
 18 files changed, 546 insertions(+), 214 deletions(-)

diff --git a/DEVELOP.md b/DEVELOP.md
index cd9eff5e8..42efcd445 100644
--- a/DEVELOP.md
+++ b/DEVELOP.md
@@ -257,11 +257,10 @@ daffodil/
 └── tutorials/                  - Contains Daffodil's TDML test tutorials
 ```
 
-When compiled into a command line interface, Daffodil is composed of a
-script, a log4j2 configuration file, and a lib directory containing
-all Daffodil jars and third-party dependency jars needed by the
-Daffodil CLI.  Daffodil's command line interface is released in binary
-form as follows:
+When compiled into a command line interface, Daffodil is composed of a script
+and a lib directory containing all Daffodil jars and third-party dependency
+jars needed by the Daffodil CLI.  Daffodil's command line interface is released
+in binary form as follows:
 
 ```text
 apache-daffodil-3.4.0-bin/
@@ -271,8 +270,6 @@ apache-daffodil-3.4.0-bin/
 ├── bin/
 │   ├── daffodil     - Starts Daffodil on Linux
 │   └── daffodil.bat - Starts Daffodil on Windows
-├── conf/
-│   └── log4j2.xml   - Configures log4j logger output
 └── lib/             - Contains Daffodil jars and third party jars
 ```
 
diff --git a/build.sbt b/build.sbt
index 43e848c63..c0b1886bf 100644
--- a/build.sbt
+++ b/build.sbt
@@ -36,7 +36,29 @@ lazy val genExamples = taskKey[Seq[File]]("Generate runtime2 
example files")
 
 lazy val daffodil         = project.in(file(".")).configs(IntegrationTest)
                               .enablePlugins(JavaUnidocPlugin, 
ScalaUnidocPlugin)
-                              .aggregate(macroLib, propgen, lib, io, runtime1, 
runtime1Unparser, runtime1Layers, runtime2, core, japi, sapi, tdmlLib, 
tdmlProc, cli, udf, schematron, test, testIBM1, tutorials, testStdLayout)
+                              .aggregate(
+                                 cli,
+                                 core,
+                                 io,
+                                 japi,
+                                 lib,
+                                 macroLib,
+                                 propgen,
+                                 runtime1,
+                                 runtime1Layers,
+                                 runtime1Unparser,
+                                 runtime2,
+                                 sapi,
+                                 schematron,
+                                 slf4jLogger,
+                                 tdmlLib,
+                                 tdmlProc,
+                                 test,
+                                 testIBM1,
+                                 testStdLayout,
+                                 tutorials,
+                                 udf,
+                               )
                               .settings(commonSettings, nopublish, 
ratSettings, unidocSettings, genExamplesSettings)
 
 lazy val macroLib         = Project("daffodil-macro-lib", 
file("daffodil-macro-lib")).configs(IntegrationTest)
@@ -47,30 +69,34 @@ lazy val macroLib         = Project("daffodil-macro-lib", 
file("daffodil-macro-l
 lazy val propgen          = Project("daffodil-propgen", 
file("daffodil-propgen")).configs(IntegrationTest)
                               .settings(commonSettings, nopublish)
 
+lazy val slf4jLogger      = Project("daffodil-slf4j-logger", 
file("daffodil-slf4j-logger")).configs(IntegrationTest)
+                              .settings(commonSettings)
+                              .settings(libraryDependencies ++= 
Dependencies.slf4jAPI)
+
 lazy val lib              = Project("daffodil-lib", 
file("daffodil-lib")).configs(IntegrationTest)
-                              .dependsOn(macroLib % "compile-internal, 
test-internal")
+                              .dependsOn(macroLib % "compile-internal, 
test-internal", slf4jLogger % "test")
                               .settings(commonSettings, libManagedSettings, 
usesMacros)
 
 lazy val io               = Project("daffodil-io", 
file("daffodil-io")).configs(IntegrationTest)
-                              .dependsOn(lib, macroLib % "compile-internal, 
test-internal")
+                              .dependsOn(lib, macroLib % "compile-internal, 
test-internal", slf4jLogger % "test")
                               .settings(commonSettings, usesMacros)
 
 lazy val runtime1         = Project("daffodil-runtime1", 
file("daffodil-runtime1")).configs(IntegrationTest)
-                              .dependsOn(io, lib % "test->test", udf, macroLib 
% "compile-internal, test-internal")
+                              .dependsOn(io, lib % "test->test", udf, macroLib 
% "compile-internal, test-internal", slf4jLogger % "test")
                               .settings(commonSettings, usesMacros)
 
 lazy val runtime1Unparser = Project("daffodil-runtime1-unparser", 
file("daffodil-runtime1-unparser")).configs(IntegrationTest)
-                              .dependsOn(runtime1, lib % "test->test", 
runtime1 % "test->test", runtime1Layers)
+                              .dependsOn(runtime1, lib % "test->test", 
runtime1 % "test->test", runtime1Layers, slf4jLogger % "test")
                               .settings(commonSettings)
 
 lazy val runtime1Layers = Project("daffodil-runtime1-layers", 
file("daffodil-runtime1-layers")).configs(IntegrationTest)
-                              .dependsOn(runtime1, lib % "test->test")
+                              .dependsOn(runtime1, lib % "test->test", 
slf4jLogger % "test")
                               .settings(commonSettings)
 
 val runtime2CFiles        = Library("libruntime2.a")
 lazy val runtime2         = Project("daffodil-runtime2", 
file("daffodil-runtime2")).configs(IntegrationTest)
                               .enablePlugins(CcPlugin)
-                              .dependsOn(core, core % "test->test")
+                              .dependsOn(core, core % "test->test", 
slf4jLogger % "test")
                               .settings(commonSettings)
                               .settings(
                                 Compile / cCompiler := sys.env.getOrElse("CC", 
"cc"),
@@ -90,42 +116,43 @@ lazy val runtime2         = Project("daffodil-runtime2", 
file("daffodil-runtime2
                               )
 
 lazy val core             = Project("daffodil-core", 
file("daffodil-core")).configs(IntegrationTest)
-                              .dependsOn(runtime1Unparser, udf, lib % 
"test->test", runtime1 % "test->test", io % "test->test")
+                              .dependsOn(runtime1Unparser, udf, lib % 
"test->test", runtime1 % "test->test", io % "test->test", slf4jLogger % "test")
                               .settings(commonSettings)
 
 lazy val japi             = Project("daffodil-japi", 
file("daffodil-japi")).configs(IntegrationTest)
-                              .dependsOn(core)
+                              .dependsOn(core, slf4jLogger % "test")
                               .settings(commonSettings)
 
 lazy val sapi             = Project("daffodil-sapi", 
file("daffodil-sapi")).configs(IntegrationTest)
-                              .dependsOn(core)
+                              .dependsOn(core, slf4jLogger % "test")
                               .settings(commonSettings)
 
-lazy val tdmlLib             = Project("daffodil-tdml-lib", 
file("daffodil-tdml-lib")).configs(IntegrationTest)
-                              .dependsOn(macroLib % "compile-internal", lib, 
io, io % "test->test")
+lazy val tdmlLib          = Project("daffodil-tdml-lib", 
file("daffodil-tdml-lib")).configs(IntegrationTest)
+                              .dependsOn(macroLib % "compile-internal", lib, 
io, io % "test->test", slf4jLogger % "test")
                               .settings(commonSettings)
 
 lazy val tdmlProc         = Project("daffodil-tdml-processor", 
file("daffodil-tdml-processor")).configs(IntegrationTest)
-                              .dependsOn(tdmlLib, runtime2, core)
+                              .dependsOn(tdmlLib, runtime2, core, slf4jLogger)
                               .settings(commonSettings)
 
 lazy val cli              = Project("daffodil-cli", 
file("daffodil-cli")).configs(IntegrationTest)
-                              .dependsOn(tdmlProc, runtime2, sapi, japi, 
schematron % Runtime, udf % "it->test") // causes runtime2/sapi/japi to be 
pulled into the helper zip/tar
+                              .dependsOn(tdmlProc, runtime2, sapi, japi, 
schematron % Runtime, udf % "it->test", slf4jLogger) // causes 
runtime2/sapi/japi to be pulled into the helper zip/tar
                               .settings(commonSettings, nopublish)
                               .settings(libraryDependencies ++= 
Dependencies.cli)
                               .settings(libraryDependencies ++= 
Dependencies.exi)
 
 lazy val udf              = Project("daffodil-udf", 
file("daffodil-udf")).configs(IntegrationTest)
+                              .dependsOn(slf4jLogger % "test")
                               .settings(commonSettings)
 
 lazy val schematron       = Project("daffodil-schematron", 
file("daffodil-schematron"))
-                              .dependsOn(lib, sapi % Test)
+                              .dependsOn(lib, sapi % Test, slf4jLogger % 
"test")
                               .settings(commonSettings)
                               .settings(libraryDependencies ++= 
Dependencies.schematron)
                               .configs(IntegrationTest)
 
 lazy val test             = Project("daffodil-test", 
file("daffodil-test")).configs(IntegrationTest)
-                              .dependsOn(tdmlProc, runtime2 % "test->test", 
udf % "test->test")
+                              .dependsOn(tdmlProc % "test", runtime2 % 
"test->test", udf % "test->test")
                               .settings(commonSettings, nopublish)
                               //
                               // Uncomment the following line to run these 
tests 
@@ -134,7 +161,7 @@ lazy val test             = Project("daffodil-test", 
file("daffodil-test")).conf
                               //.settings(IBMDFDLCrossTesterPlugin.settings)
 
 lazy val testIBM1         = Project("daffodil-test-ibm1", 
file("daffodil-test-ibm1")).configs(IntegrationTest)
-                              .dependsOn(tdmlProc)
+                              .dependsOn(tdmlProc % "test")
                               .settings(commonSettings, nopublish)
                               //
                               // Uncomment the following line to run these 
tests 
@@ -143,11 +170,11 @@ lazy val testIBM1         = Project("daffodil-test-ibm1", 
file("daffodil-test-ib
                               //.settings(IBMDFDLCrossTesterPlugin.settings)
 
 lazy val tutorials        = Project("daffodil-tutorials", 
file("tutorials")).configs(IntegrationTest)
-                              .dependsOn(tdmlProc)
+                              .dependsOn(tdmlProc % "test")
                               .settings(commonSettings, nopublish)
 
 lazy val testStdLayout    = Project("daffodil-test-stdLayout", 
file("test-stdLayout")).configs(IntegrationTest)
-                              .dependsOn(tdmlProc)
+                              .dependsOn(tdmlProc % "test")
                               .settings(commonSettings, nopublish)
 
 
diff --git a/daffodil-cli/bin.LICENSE b/daffodil-cli/bin.LICENSE
index e9ac9701a..c54800ff1 100644
--- a/daffodil-cli/bin.LICENSE
+++ b/daffodil-cli/bin.LICENSE
@@ -1782,7 +1782,7 @@ is subject to the terms and conditions of the following 
licenses.
   This product bundles 'SLF4J API' from the above files.
   These files are available under the MIT license:
 
-    Copyright (c) 2004-2017 QOS.ch
+    Copyright (c) 2004-2022 QOS.ch Sarl (Switzerland)
     All rights reserved.
 
     Permission is hereby granted, free  of charge, to any person obtaining
diff --git a/daffodil-cli/bin.NOTICE b/daffodil-cli/bin.NOTICE
index 755b6a29d..82363d87d 100644
--- a/daffodil-cli/bin.NOTICE
+++ b/daffodil-cli/bin.NOTICE
@@ -30,6 +30,9 @@ The following NOTICE information applies to binary components 
distributed with t
   in some artifacts (usually source distributions); but is always available
   from the source code management (SCM) system project uses.
 
+- com.typesafe.scala-logging.scala-logging_<VERSION>.jar
+  Copyright 2014-2021 Lightbend, Inc.
+
 - commons-codec.commons-codec-<VERSION>.jar
   Apache Commons Codec
   Copyright 2002-2020 The Apache Software Foundation
@@ -87,25 +90,6 @@ The following NOTICE information applies to binary 
components distributed with t
   Apache HttpComponents Core HTTP/2
   Copyright 2005-2020 The Apache Software Foundation
 
-- org.apache.logging.log4j.log4j-api-<VERSION>.jar
-  Apache Log4j API
-  Copyright 1999-2022 The Apache Software Foundation
-
-- org.apache.logging.log4j.log4j-api-scala_<VERSION>.jar
-  Apache Log4j Scala API
-  Copyright 2016-2022 Apache Software Foundation
-
-  AsciidocPlugin.scala
-  Copyright (c) 2008, 2009, 2010, 2011 Josh Suereth, Steven Blundy, Josh Cough,
-  Mark Harrah, Stuart Roebuck, Tony Sloane, Vesa Vilhonen, Jason Zaugg
-
-- org.apache.logging.log4j.log4j-core-<VERSION>.jar
-  Apache Log4j Core
-  Copyright 1999-2012 Apache Software Foundation
-
-  ResolverUtil.java
-  Copyright 2005-2006 Tim Fennell
-
 - org.jdom.jdom2-<VERSION>.jar
   Copyright (C) 2000-2012 Jason Hunter & Brett McLaughlin.
   All rights reserved.
diff --git a/daffodil-cli/build.sbt b/daffodil-cli/build.sbt
index 6d07885cb..559af2ff4 100644
--- a/daffodil-cli/build.sbt
+++ b/daffodil-cli/build.sbt
@@ -23,10 +23,8 @@ enablePlugins(JavaAppPackaging)
 enablePlugins(RpmPlugin)
 enablePlugins(WindowsPlugin)
 
-// CLI tests are not thread safe
-// due to use of temporary buffer used to capture stdout/stderr.
-// So we cannot test in parallel
-Test / parallelExecution := false
+// integration tests require forking, which require a lot of extra memory, so
+// these should only be run sequentially
 IntegrationTest / parallelExecution := false
 
 // need 'sbt stage' to build the CLI for cli integration tests
@@ -49,7 +47,6 @@ Universal / mappings ++= Seq(
   baseDirectory.value / "bin.LICENSE" -> "LICENSE",
   baseDirectory.value / "bin.NOTICE" -> "NOTICE",
   baseDirectory.value / "README.md" -> "README.md",
-  sourceDirectory.value / "conf" / "log4j2.xml" -> "conf/log4j2.xml",
 )
 
 maintainer := "Apache Daffodil <[email protected]>"
diff --git a/daffodil-cli/src/conf/.keep b/daffodil-cli/src/conf/.keep
new file mode 100644
index 000000000..ae1e83eeb
--- /dev/null
+++ b/daffodil-cli/src/conf/.keep
@@ -0,0 +1,14 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/daffodil-cli/src/conf/log4j2.xml b/daffodil-cli/src/conf/log4j2.xml
deleted file mode 100644
index b267bd2df..000000000
--- a/daffodil-cli/src/conf/log4j2.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-  Licensed to the Apache Software Foundation (ASF) under one or more
-  contributor license agreements.  See the NOTICE file distributed with
-  this work for additional information regarding copyright ownership.
-  The ASF licenses this file to You under the Apache License, Version 2.0
-  (the "License"); you may not use this file except in compliance with
-  the License.  You may obtain a copy of the License at
-
-      http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
--->
-
-<Configuration>
-  <Appenders>
-    <Console name="STDERR" target="SYSTEM_ERR">
-      <PatternLayout pattern="[%p{lowerCase=true}] %m%n"/>
-    </Console>
-  </Appenders>
-  <Loggers>
-    <Root level="WARN">
-      <AppenderRef ref="STDERR"/>
-    </Root>
-  </Loggers>
-</Configuration>
diff --git a/daffodil-cli/src/main/scala/org/apache/daffodil/cli/Main.scala 
b/daffodil-cli/src/main/scala/org/apache/daffodil/cli/Main.scala
index 3ea8e5a95..1cf74b029 100644
--- a/daffodil-cli/src/main/scala/org/apache/daffodil/cli/Main.scala
+++ b/daffodil-cli/src/main/scala/org/apache/daffodil/cli/Main.scala
@@ -42,8 +42,7 @@ import com.typesafe.config.ConfigFactory
 
 import org.apache.commons.io.output.NullOutputStream
 
-import org.apache.logging.log4j.Level
-import org.apache.logging.log4j.core.config.Configurator
+import org.slf4j.event.Level;
 
 import org.rogach.scallop
 import org.rogach.scallop.ArgType
@@ -91,6 +90,7 @@ import 
org.apache.daffodil.lib.schema.annotation.props.gen.BitOrder
 import org.apache.daffodil.tdml.Runner
 import org.apache.daffodil.tdml.TDMLException
 import org.apache.daffodil.tdml.TDMLTestNotCompatibleException
+import org.apache.daffodil.slf4j.DaffodilLogger
 import org.apache.daffodil.runtime1.udf.UserDefinedFunctionFatalErrorException
 import org.apache.daffodil.lib.util.Logger
 import org.apache.daffodil.lib.util.Misc
@@ -530,21 +530,50 @@ object ValidatorPatterns {
 
 object Main {
 
-  var STDIN: InputStream = System.in
-  var STDOUT: PrintStream = System.out
-  var STDERR: PrintStream = System.err
+  def main(arguments: Array[String]): Unit = {
+    val main = new Main()
+    val exitCode = main.run(arguments)
+    System.exit(exitCode.id)
+  }
 
-  /**
-   * Allows changing where the input/output of the CLI goes to. This defaults
-   * to the normal System.in/out/err, but can be changed to support easier
-   * testing. This is not thread safe, but this Main object is not really
-   * thread safe anyways if it is configured to read/write from stdin/stout.
-   */
-  def setInputOutput(in: InputStream, out: PrintStream, err: PrintStream): 
Unit = {
-    STDIN = in
-    STDOUT = out
-    STDERR = err
+  // write blobs to $PWD/daffodil-blobs/*.bin
+  val blobDir = Paths.get(System.getProperty("user.dir"), "daffodil-blobs")
+  val blobSuffix = ".bin"
+
+  object ExitCode extends Enumeration {
+
+    val Success = Value(0)
+    val Failure = Value(1)
+
+    val FileNotFound = Value(2)
+    val OutOfMemory = Value(3)
+    val BugFound = Value(4)
+    val NotYetImplemented = Value(5)
+
+    val ParseError = Value(20)
+    val UnparseError = Value(21)
+    val GenerateCodeError = Value(23)
+    val TestError = Value(24)
+    val PerformanceTestError = Value(25)
+    val ConfigError = Value(26)
+
+    val LeftOverData = Value(31)
+    val InvalidParserException = Value(32)
+    val BadExternalVariable = Value(33)
+    val UserDefinedFunctionError = Value(34)
+    val UnableToCreateProcessor = Value(35)
+    val LayerExecutionError = Value(36)
+
+    val Usage = Value(64)
   }
+}
+
+class Main(
+  STDIN: InputStream = System.in,
+  STDOUT: PrintStream = System.out,
+  STDERR: PrintStream = System.err,
+) {
+  import Main._
 
   val traceCommands = Seq(
     "display info parser",
@@ -709,11 +738,10 @@ object Main {
     cg
   }
 
-  // write blobs to $PWD/daffodil-blobs/*.bin
-  val blobDir = Paths.get(System.getProperty("user.dir"), "daffodil-blobs")
-  val blobSuffix = ".bin"
-
+  // assumes that the DaffodilLogger is the instance used by the CLI
+  lazy val daffodilLogger = Logger.log.underlying.asInstanceOf[DaffodilLogger]
 
+  // Set a log level and stream for this Thread only
   def setLogLevel(verbose: Int): Unit = {
     val verboseLevel = verbose match {
       case 0 => Level.WARN
@@ -721,12 +749,13 @@ object Main {
       case 2 => Level.DEBUG
       case _ => Level.TRACE
     }
-    Configurator.setLevel("org.apache.daffodil", verboseLevel)
+    daffodilLogger.setThreadLoggerConfig(verboseLevel, STDERR)
   }
 
   def runIgnoreExceptions(arguments: Array[String]): ExitCode.Value = {
     val conf = new CLIConf(arguments)
 
+    // Set the log level to whatever was parsed by the options
     setLogLevel(conf.verbose())
 
     val ret: ExitCode.Value = conf.subcommand match {
@@ -1411,38 +1440,11 @@ object Main {
     1
   }
 
-  object ExitCode extends Enumeration {
-
-    val Success = Value(0)
-    val Failure = Value(1)
-
-    val FileNotFound = Value(2)
-    val OutOfMemory = Value(3)
-    val BugFound = Value(4)
-    val NotYetImplemented = Value(5)
-
-
-    val ParseError = Value(20)
-    val UnparseError = Value(21)
-    val GenerateCodeError = Value(23)
-    val TestError = Value(24)
-    val PerformanceTestError = Value(25)
-    val ConfigError = Value(26)
-
-    val LeftOverData = Value(31)
-    val InvalidParserException = Value(32)
-    val BadExternalVariable = Value(33)
-    val UserDefinedFunctionError = Value(34)
-    val UnableToCreateProcessor = Value(35)
-    val LayerExecutionError = Value(36)
-
-    val Usage = Value(64)
-
-  }
-
-
   def run(arguments: Array[String]): ExitCode.Value = {
     val ret = try {
+      // Initialize the log level to Level.WARN in case we log anything before
+      // succesfully parsing command line arguments and setting the log level
+      setLogLevel(0)
       runIgnoreExceptions(arguments)
     } catch {
       case s: scala.util.control.ControlThrowable => throw s
@@ -1496,14 +1498,14 @@ object Main {
         bugFound(e)
         ExitCode.BugFound
       }
+    } finally {
+      // now that we are done we can remove the ThreadLocal that was set for
+      // CLI thread specific logging
+      daffodilLogger.removeThreadLoggerConfig()
     }
     ret
   }
 
-  def main(arguments: Array[String]): Unit = {
-    val exitCode = run(arguments)
-    System.exit(exitCode.id)
-  }
 }
 
 class EXIErrorHandler extends org.xml.sax.ErrorHandler with 
javax.xml.transform.ErrorListener {
diff --git a/daffodil-cli/src/templates/bash-template 
b/daffodil-cli/src/templates/bash-template
index fbee60c13..ad06ddf45 100755
--- a/daffodil-cli/src/templates/bash-template
+++ b/daffodil-cli/src/templates/bash-template
@@ -77,7 +77,6 @@ DEFAULT_JOPTS=(
   "-Xms1024m"
   "-Xmx1024m"
   "-XX:ReservedCodeCacheSize=128m"
-  "-Dlog4j.configurationFile=$CONFDIR/log4j2.xml"
 )
 
 exec java "${DEFAULT_JOPTS[@]}" $JOPTS -cp "$CLASSPATH" $MAINCLASS "$@"
diff --git a/daffodil-cli/src/templates/bat-template 
b/daffodil-cli/src/templates/bat-template
index 05f121510..56fe0ac17 100755
--- a/daffodil-cli/src/templates/bat-template
+++ b/daffodil-cli/src/templates/bat-template
@@ -59,6 +59,5 @@ set DEFAULT_JOPTS=
 set DEFAULT_JOPTS=%DEFAULT_JOPTS% -Xms1024m
 set DEFAULT_JOPTS=%DEFAULT_JOPTS% -Xmx1024m
 set DEFAULT_JOPTS=%DEFAULT_JOPTS% -XX:ReservedCodeCacheSize=128m
-set DEFAULT_JOPTS=%DEFAULT_JOPTS% 
"-Dlog4j.configurationFile=%CONFDIR%\log4j2.xml"
 
 java %DEFAULT_JOPTS% %JOPTS% -cp "%CLASSPATH%" %MAINCLASS% %*
diff --git 
a/daffodil-cli/src/test/scala/org/apache/daffodil/cli/cliTest/Util.scala 
b/daffodil-cli/src/test/scala/org/apache/daffodil/cli/cliTest/Util.scala
index cb0ef8963..4286132c4 100644
--- a/daffodil-cli/src/test/scala/org/apache/daffodil/cli/cliTest/Util.scala
+++ b/daffodil-cli/src/test/scala/org/apache/daffodil/cli/cliTest/Util.scala
@@ -45,13 +45,6 @@ import net.sf.expectit.matcher.Matchers.contains
 
 import org.apache.commons.io.FileUtils
 
-import org.apache.logging.log4j.Level
-import org.apache.logging.log4j.core.appender.OutputStreamAppender
-import org.apache.logging.log4j.core.config.AbstractConfiguration
-import org.apache.logging.log4j.core.config.ConfigurationSource
-import org.apache.logging.log4j.core.config.Configurator
-import org.apache.logging.log4j.core.layout.PatternLayout
-
 import org.junit.Assert.assertEquals
 
 import org.apache.daffodil.cli.Main
@@ -373,13 +366,12 @@ object Util {
       val psOut = new PrintStream(out)
       val psErr = new PrintStream(err)
 
-      // configure the CLI and log4j to use our custom streams, nothing should
-      // actually use stdin/stdout/stderr
-      Main.setInputOutput(in, psOut, psErr)
-      configureLog4j(psErr)
-
       try {
-        exitCode = Main.run(args)
+        // Run a thread-safe CLI instance that uses our custom streams that
+        // expect will read/write. Nothing in the CLI should use the real
+        // stdin/stdout/stderr, or expect won't be able to see it
+        val main = new Main(in, psOut, psErr)
+        exitCode = main.run(args)
       } catch {
         case t: Throwable => {
           // Main.run should never throw an exception so if it did it means 
the CLI
@@ -390,42 +382,6 @@ object Util {
         }
       }
     }
-
-    /**
-     * By default log4j outputs to stderr. This changes that so it writes to a
-     * provided PrintStream which is connected to the CLITester, allowing tests
-     * to expect content written by log4j. This also defines the same pattern
-     * used by the CLI from the daffodil-cli/src/conf/log4j2.xml config
-     * file--we duplicate it here because the normal log4j config file targets
-     * stderr while we need to target the provided PrintStream (which can only
-     * be defined programatically). It was also found to be too difficult to
-     * load the config file and mutate the log4j configuration to write to this
-     * PrintStream instead of stderr.
-     */
-    private def configureLog4j(ps: PrintStream): Unit = {
-      val config = new AbstractConfiguration(null, 
ConfigurationSource.NULL_SOURCE) {
-        override def doConfigure(): Unit = {
-          val appenderName = "DaffodilCli"
-
-          val layout = PatternLayout.newBuilder()
-            .withPattern("[%p{lowerCase=true}] %m%n")
-            .withConfiguration(this)
-            .build()
-
-          val appenderBuilder: OutputStreamAppender.Builder[_] = 
OutputStreamAppender.newBuilder()
-          appenderBuilder.setName(appenderName)
-          appenderBuilder.setLayout(layout)
-          appenderBuilder.setTarget(ps)
-          appenderBuilder.setConfiguration(this)
-          val appender = appenderBuilder.build()
-
-          val rootLogger = getRootLogger()
-          rootLogger.setLevel(Level.WARN);
-          rootLogger.addAppender(appender, null, null)
-        }
-      }
-      Configurator.reconfigure(config)
-    }
   }
 
   /**
diff --git 
a/daffodil-lib/src/main/scala/org/apache/daffodil/lib/util/Logger.scala 
b/daffodil-lib/src/main/scala/org/apache/daffodil/lib/util/Logger.scala
index 58de97e43..1819dea97 100644
--- a/daffodil-lib/src/main/scala/org/apache/daffodil/lib/util/Logger.scala
+++ b/daffodil-lib/src/main/scala/org/apache/daffodil/lib/util/Logger.scala
@@ -17,30 +17,18 @@
 
 package org.apache.daffodil.lib.util
 
-import org.apache.logging.log4j.scala.Logging
+import com.typesafe.scalalogging.{ Logger => ScalaLogger }
 
 /**
- * The log4j.scala.Logging trait adds a log4j.scala.Logger member val called
- * 'logger' to whatever class mixes it in. Classes that mixin this trait can
- * then just call logger.warn/info/debug/etc to log a message, which are macros
- * to minimize overhead.
- *
- * However, the Logger class is not serializable, and so all classes that might
- * need to be serialized, (e.g. runtime objects), cannot take this approach.
- *
- * Instead, we have a single Logger object that mixes in this trait. This
- * object is never serialized and so can be used anywhere, regardless of
- * serializablility. For simplicity, consistency, and to avoid potential
- * serialization issues, all logging should be done via the logger in this
- * object, rather than mixing in the Logging trait, for example:
+ * At this time, we do not need loggers specific to each class. Instead we do
+ * the lookup once to get the and "org.apache.daffodil" logger via the
+ * scala-logging/SLF4J API and store that logger in this object. For simplicity
+ * and consistency, all logging should be done via the logger in this object,
+ * for example:
  *
  *   Logger.log.info("Message to log")
  *
- * The downside to this is that it breaks the ability to use Log4j's feature to
- * configure different log levels for different namespaces or classes or have
- * useful line numbers, because all logging comes from and is associated with
- * this single Logger in the 'org.apache.daffodil.lib.util.Logger' class.
  */
-object Logger extends Logging {
-  def log = this.logger  
+object Logger {
+  val log = ScalaLogger("org.apache.daffodil")
 }
diff --git 
a/daffodil-lib/src/main/scala/org/apache/daffodil/lib/util/Timer.scala 
b/daffodil-lib/src/main/scala/org/apache/daffodil/lib/util/Timer.scala
index a8e19fdf7..980388f8a 100644
--- a/daffodil-lib/src/main/scala/org/apache/daffodil/lib/util/Timer.scala
+++ b/daffodil-lib/src/main/scala/org/apache/daffodil/lib/util/Timer.scala
@@ -216,7 +216,7 @@ object TimeTracker {
   /**
    * Output the results of the tracked sections in sorted columnar format.
    */
-  def logTimes(logLevel: org.apache.logging.log4j.Level): Unit = {
+  def logTimes(): Unit = {
     val stats = sectionTimes.asScala.toSeq.map { case (name, 
SectionTime(timeNS, count)) => {
       val average = timeNS / count
       (name, timeNS / 1000000000.0, average, count)
@@ -247,11 +247,11 @@ object TimeTracker {
       "%"  + averageLen + "s  " +
       "%"  + countLen + "s"
 
-    Logger.log(logLevel, formatString.format("Name", "Time", "Pct", "Average", 
"Count"))
+    Logger.log.info(formatString.format("Name", "Time", "Pct", "Average", 
"Count"))
     stringStats.foreach { stats =>
-      Logger.log(logLevel, formatString.format(stats.productIterator.toList: 
_*))
+      Logger.log.info(formatString.format(stats.productIterator.toList: _*))
     }
-    Logger.log(logLevel, f"Total Time: $totalTime%.3f")
+    Logger.log.info(f"Total Time: $totalTime%.3f")
   }
 
   def clear(): Unit = {
diff --git a/daffodil-slf4j-logger/src/main/resources/META-INF/LICENSE 
b/daffodil-slf4j-logger/src/main/resources/META-INF/LICENSE
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/daffodil-slf4j-logger/src/main/resources/META-INF/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/daffodil-slf4j-logger/src/main/resources/META-INF/NOTICE 
b/daffodil-slf4j-logger/src/main/resources/META-INF/NOTICE
new file mode 100644
index 000000000..8972bbb01
--- /dev/null
+++ b/daffodil-slf4j-logger/src/main/resources/META-INF/NOTICE
@@ -0,0 +1,10 @@
+Apache Daffodil
+Copyright 2022 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+Based on source code originally developed by
+- The Univerisity of Illinois National Center for Supercomputing Applications 
(http://www.ncsa.illinois.edu/)
+- Tresys Technology (http://www.tresys.com/)
+- International Business Machines Corporation (http://www.ibm.com)
diff --git 
a/daffodil-slf4j-logger/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider
 
b/daffodil-slf4j-logger/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider
new file mode 100644
index 000000000..584a796e5
--- /dev/null
+++ 
b/daffodil-slf4j-logger/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider
@@ -0,0 +1,16 @@
+#  Licensed to the Apache Software Foundation (ASF) under one or more
+#  contributor license agreements.  See the NOTICE file distributed with
+#  this work for additional information regarding copyright ownership.
+#  The ASF licenses this file to You under the Apache License, Version 2.0
+#  (the "License"); you may not use this file except in compliance with
+#  the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+org.apache.daffodil.slf4j.DaffodilSLF4JServiceProvider
diff --git 
a/daffodil-slf4j-logger/src/main/scala/org/apahe/daffodil/slf4j/DaffodilSLF4JLogger.scala
 
b/daffodil-slf4j-logger/src/main/scala/org/apahe/daffodil/slf4j/DaffodilSLF4JLogger.scala
new file mode 100644
index 000000000..c716a513d
--- /dev/null
+++ 
b/daffodil-slf4j-logger/src/main/scala/org/apahe/daffodil/slf4j/DaffodilSLF4JLogger.scala
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.daffodil.slf4j
+
+import java.io.PrintStream
+import java.util.concurrent.ConcurrentHashMap
+
+import org.slf4j.ILoggerFactory
+import org.slf4j.IMarkerFactory
+import org.slf4j.Logger
+import org.slf4j.Marker
+import org.slf4j.event.Level
+import org.slf4j.helpers.BasicMarkerFactory
+import org.slf4j.helpers.AbstractLogger
+import org.slf4j.helpers.MessageFormatter
+import org.slf4j.helpers.NOPMDCAdapter
+import org.slf4j.spi.MDCAdapter
+import org.slf4j.spi.SLF4JServiceProvider
+
+class DaffodilSLF4JServiceProvider extends SLF4JServiceProvider {
+  override lazy val getLoggerFactory: ILoggerFactory = new 
DaffodilLoggerFactory()
+  override lazy val getMarkerFactory: IMarkerFactory = new BasicMarkerFactory()
+  override lazy val getMDCAdapter: MDCAdapter = new NOPMDCAdapter()
+  override lazy val getRequestedApiVersion: String = "2.0.99"
+  override def initialize(): Unit = {}
+}
+
+class DaffodilLoggerFactory extends ILoggerFactory {
+
+  private val loggerMap = new ConcurrentHashMap[String, DaffodilLogger]()
+
+  override def getLogger(name: String): Logger = {
+    val daffodilLogger = loggerMap.get(name)
+    if (daffodilLogger != null) {
+      daffodilLogger
+    } else {
+      val newInstance = new DaffodilLogger(name)
+      val oldInstance = loggerMap.putIfAbsent(name, newInstance)
+      if (oldInstance == null) {
+        newInstance
+      } else {
+        oldInstance
+      }
+    }
+  }
+}
+
+/**
+ * A logger that should only be used by specifically by Daffodil (e.g. CLI, 
TDML
+ * Runner) to support thread specific log levels and log streams. This is 
mostly
+ * useful for testing where we want to allow each test thread to log at a
+ * different level and stream. To set the level and stream, one should call:
+ *
+ *   daffodilLogger.setThreadLoggerConfig(level, stream)
+ *
+ * When done, the config should be removed by calling:
+ *
+ *   daffodilLogger.removeThreadLoggerConfig()
+ */
+class DaffodilLogger(name: String) extends AbstractLogger {
+
+  case class LoggerConfig(level: Level, stream: PrintStream)
+
+  private val perThreadLoggerConfig = new ThreadLocal[LoggerConfig] {
+    // defaults to null which disables log writing. If logs need to be written
+    // to a stream (e.g. output for CLI, capture for testing), then the thread
+    // requiring it should call the setThreadLoggerConfig and
+    // removeThreadLoggerConfig funcctions.
+    override def initialValue() = null
+  }
+
+  /**
+   * Set the log level and stream to use for the calling thread
+   */
+  def setThreadLoggerConfig(level: Level, stream: PrintStream): Unit = {
+    val loggerConfig = LoggerConfig(level, stream)
+    perThreadLoggerConfig.set(loggerConfig)
+  }
+
+  /**
+   * Clean up logger config state associated with this thread
+   */
+  def removeThreadLoggerConfig(): Unit = {
+    perThreadLoggerConfig.remove()
+  }
+
+  override protected def getFullyQualifiedCallerName(): String = null
+
+  override protected def handleNormalizedLoggingCall(
+    level: Level,
+    marker: Marker,
+    msg: String,
+    arguments: Array[Object],
+    throwable: Throwable): Unit = {
+
+    val loggerConfig = perThreadLoggerConfig.get
+    if (loggerConfig != null) {
+      val buf = new StringBuilder()
+
+      val levelStr = level.toString.toLowerCase
+      buf.append('[')
+      buf.append(levelStr)
+      buf.append("] ")
+
+      // a weird quirk of the scala-logging library is that if s-interpolation 
is used and
+      // the last interpolated variable is a Throwable, then it thinks we're 
calling the
+      // Throwable variant of a log function, and so the Throwable isn't in 
the arguments
+      // array but is in the throwable variable. We don't use the Throwable 
variant in
+      // Daffodil, so if there is a throwable, just append it to the arguments 
array and
+      // the MessageFormatter will convert it to a string as intended
+      val fixedArgs =
+        if (throwable != null) {
+          if (arguments == null) {
+            Array[Object](throwable)
+          } else {
+            arguments :+ throwable
+          }
+        } else {
+          arguments
+        }
+
+      val formattedMessage = MessageFormatter.basicArrayFormat(msg, fixedArgs)
+      buf.append(formattedMessage)
+
+      loggerConfig.stream.println(buf.toString)
+      loggerConfig.stream.flush()
+    }
+  }
+
+  override def isErrorEnabled(): Boolean = isLevelEnabled(Level.ERROR)
+
+  override def isErrorEnabled(marker: Marker): Boolean = isErrorEnabled()
+
+  override def isWarnEnabled(): Boolean = isLevelEnabled(Level.WARN)
+
+  override def isWarnEnabled(marker: Marker): Boolean = isWarnEnabled()
+
+  override def isInfoEnabled(): Boolean = isLevelEnabled(Level.INFO)
+
+  override def isInfoEnabled(marker: Marker): Boolean = isInfoEnabled()
+
+  override def isDebugEnabled(): Boolean = isLevelEnabled(Level.DEBUG)
+
+  override def isDebugEnabled(marker: Marker): Boolean = isDebugEnabled()
+
+  override def isTraceEnabled(): Boolean = isLevelEnabled(Level.TRACE)
+
+  override def isTraceEnabled(marker: Marker): Boolean = isTraceEnabled()
+
+  private def isLevelEnabled(level: Level): Boolean = {
+    val loggerConfig = perThreadLoggerConfig.get
+    loggerConfig != null && loggerConfig.level.toInt <= level.toInt
+  }
+
+}
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index 6fcc8d0a1..bc3dd1876 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -30,9 +30,11 @@ object Dependencies {
     "xml-resolver" % "xml-resolver" % "1.2",
     "commons-io" % "commons-io" % "2.11.0",
     "com.typesafe" % "config" % "1.4.2",
-    "org.apache.logging.log4j" %% "log4j-api-scala" % "12.0",
-    "org.apache.logging.log4j" % "log4j-api" % "2.19.0",
-    "org.apache.logging.log4j" % "log4j-core" % "2.19.0" % "it,test",
+    "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4",
+  )
+
+  lazy val slf4jAPI = Seq(
+    "org.slf4j" % "slf4j-api" % "2.0.6",
   )
 
   lazy val infoset = Seq(
@@ -46,7 +48,6 @@ object Dependencies {
     "org.jline" % "jline" % "3.22.0",
     "org.rogach" %% "scallop" % "4.1.0",
     "net.sf.expectit" % "expectit-core" % "0.9.0" % "it,test",
-    "org.apache.logging.log4j" % "log4j-core" % "2.19.0",
   )
 
   lazy val test = Seq(

Reply via email to