{"id":20798832,"url":"https://github.com/tersesystems/echopraxia-plusscala","last_synced_at":"2025-05-06T20:11:22.486Z","repository":{"id":38237824,"uuid":"487658916","full_name":"tersesystems/echopraxia-plusscala","owner":"tersesystems","description":"Scala API for Echopraxia","archived":false,"fork":false,"pushed_at":"2024-04-07T16:31:20.000Z","size":395,"stargazers_count":3,"open_issues_count":4,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-04-07T19:29:38.042Z","etag":null,"topics":["debugging","log4j2","logback","logging","observability","scala","structured-logging"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tersesystems.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2022-05-01T22:47:00.000Z","updated_at":"2024-04-14T16:55:33.917Z","dependencies_parsed_at":"2023-11-13T06:28:05.342Z","dependency_job_id":"d084b4bc-cf6e-4a3b-acc8-bba4664c1015","html_url":"https://github.com/tersesystems/echopraxia-plusscala","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tersesystems%2Fechopraxia-plusscala","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tersesystems%2Fechopraxia-plusscala/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tersesystems%2Fechopraxia-plusscala/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tersesystems%2Fechopraxia-plusscala/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tersesystems","download_url":"https://codeload.github.com/tersesystems/echopraxia-plusscala/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252761228,"owners_count":21800125,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["debugging","log4j2","logback","logging","observability","scala","structured-logging"],"created_at":"2024-11-17T17:05:50.459Z","updated_at":"2025-05-06T20:11:22.467Z","avatar_url":"https://github.com/tersesystems.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Scala API for Echopraxia\n\n[Echopraxia](https://github.com/tersesystems/echopraxia) is a structured logging framework with implementations for Logback and Log4J.\n\nThe Scala API for [Echopraxia](https://github.com/tersesystems/echopraxia) is a layer over the Java API that leverages Scala features to provide type classes and source code information.  Echopraxia is compiled for Scala 2.13, and 3.\n\nIn practical terms, you define some type classes that set what the name and value of a class should be:\n\n```scala\nimport echopraxia.plusscala.logging.api._\n\ntrait Logging extends EchopraxiaBase {\n  implicit val uuidToField: ToField[UUID] = ToField(_ =\u003e \"uuid\", uuid =\u003e ToValue(uuid.toString))\n}\n```\n\nand then logging will provide both structured logging in JSON and line oriented format in logfmt:\n\n```scala\nimport echopraxia.plusscala.logger._\n\nclass Processor extends Logging {\n  private val logger = LoggerFactory.getLogger(getClass)\n\n  def process(): Unit = {\n    logger.info(UUID.randomUUID) // uses implicit type class for UUID\n  }\n}\n```\n\nThis will print out the uuid in [logfmt](https://www.brandur.org/logfmt) when used with a pattern format appender:\n\n```\n16:45:32.905 INFO  [main]: uuid=9e6805df-a211-4129-b96d-882e0d9eb609\n```\n\nand will print out JSON when using a structured logging appender like [logstash-logback-appender](https://github.com/logfellow/logstash-logback-encoder) or [Log4J2 template layout format](https://logging.apache.org/log4j/2.x/manual/json-template-layout.html):\n\n```json\n{\n  \"@timestamp\": \"...\",\n  \"level\": \"INFO\",\n  \"uuid\": \"9e6805df-a211-4129-b96d-882e0d9eb609\"\n}\n```\n\n## Examples\n\nFor the fastest possible way to try out Echopraxia, download and run the [Scala CLI script](https://github.com/tersesystems/smallest-dynamic-logging-example/blob/main/scala-cli/script.sc).\n\nExamples are available at [tersesystems/echopraxia-scala-example](https://github.com/tersesystems/echopraxia-scala-example) and [tersesystems/echopraxia-examples](https://github.com/tersesystems/echopraxia-examples).\n\n## What Does It Mean?\n\nQ: What does this mean for developers?\n\nA: You can write code faster and with fewer bugs by using Echopraxia for debugging.  Echopraxia is oriented for \"printf debugging\", so all the `println` and `toString` methods that go into your code at development can be entered as logging statements.  You can [snapshot](https://www.scalactic.org/user_guide/Snapshots) and dump your internal state easily.  You can also easily disable or filter debug logging using conditions.\n\nQ: What does this mean for operations?\n\nA: Echopraxia makes your application more observable through structured logging and automatic \"call-by-name\" methods that can capture request and trace contexts.  Echopraxia also targets managing logging on a budget -- determining \"when to log\" on an already deployed application.  All logging statements in Echopraxia are based around fields and values, and can incorporate complex conditional logic that can alter [logging at runtime](https://github.com/tersesystems/dynamic-debug-logging), down to individual statements.\n\nQ: Is this a replacement for SLF4J / Log4J / Logback?\n\nA: You can use Echopraxia and your usual logging framework side by side in the same application.  Echopraxia sends correctly formatted structured logging messages to the underlying framework and is well-behaved, tracking the implementations enabled logging levels and filters.\n\n## Logger\n\nAdd the following to your `build.sbt` file:\n\n```scala\n// check github releases or mvnrepository.com for latest\nlibraryDependencies += \"com.tersesystems.echopraxia.plusscala\" %% \"logger\" % echopraxiaPlusScalaVersion\n```\n\nand one of the underlying core logger providers and frameworks, i.e. for `logstash-logback-encoder`:\n\n```scala\nlibraryDependencies += \"com.tersesystems.echopraxia\" % \"logstash\" % \"4.0.0\" // provides core logger\nlibraryDependencies += \"ch.qos.logback\" % \"logback-classic\" % \"1.5.15\"\nlibraryDependencies += \"net.logstash.logback\" % \"logstash-logback-encoder\" % \"8.0\"\n```\n\nor for log4j2:\n\n```scala\nlibraryDependencies += \"com.tersesystems.echopraxia\" % \"log4j\" % \"4.0.0\"\nlibraryDependencies += \"org.apache.logging.log4j\" % \"log4j-core\" % \"2.20.0\"\nlibraryDependencies += \"org.apache.logging.log4j\" % \"log4j-layout-template-json\" % \"2.20.0\"\n```\n\nTo import the Scala API, add the following:\n\n```scala\nimport echopraxia.api._\nimport echopraxia.plusscala.api._\nimport echopraxia.plusscala.logger._\n\ntrait Logging extends EchopraxiaBase {\n  // add implicits for your classes here\n}\n\nobject CustomFieldBuilder extends FieldBuilder with Logging\n\nclass Example extends Logging {\n  val logger = LoggerFactory.getLogger(CustomFieldBuilder)\n\n  def doStuff: Unit = {\n    logger.info(\"do some stuff\")\n  }\n}\n```\n\nThe logger takes `Field*` as arguments, and `EchopraxiaBase` contains the implicit conversions to turn tuples into fields:\n\n```scala\n// val field: echopraxia.api.Field = \"name\" -\u003e \"will\" under the hood\nlogger.info(\"hi {}\", \"name\" -\u003e \"will\") // prints hi name=will\n```\n\nYou can either provide an explicit message, or just provide the fields themselves for shortcut.\n\n```scala\nlogger.info(\"name\" -\u003e \"will\", \"admin\" -\u003e true)\n// same as logger.info(\"{} {}\", \"name\" -\u003e \"will\", \"admin\" -\u003e true)\n```\n\nYou can also handle lists:\n\n```scala\nval seq = Seq(\"first\" -\u003e 1, \"second\" -\u003e 2)\nlogger.info(\"seq = {}\", \"listOfTuples\" -\u003e seq)\n```\n\nThere is a slight problem when you render lists which do not have a common base type.  In this case you have to explicitly add `HeterogeneousFieldSupport` in, which will map iterables as arrays automatically, and then cast to `Seq[Field]`:\n\n```scala\ntrait Logging extends EchopraxiaBase with HeterogeneousFieldSupport\n\nlogger.info(\"list\" -\u003e Seq[Field](\"name\" -\u003e \"will\", \"admin\" -\u003e true))\n```\n\nYou can, of course, log exceptions automatically, either with or without a message.\n\n```scala\nlogger.error(\"something went wrong: {}\", e)\n// this also works\nlogger.error(e)\n```\n\n## Field Builders\n\nThere are some values that are awkward to represent using implicit conversion, such as when you want to render a `null` explicitly.  The logger comes with a field builder function that can be used for fine-grained control of arguments.  In this case, we can use `nullField` to render the field.\n\n```scala\nlogger.debug(\"this will render foo=null -- {}\", _.nullField(\"foo\"))\n```\n\nField builders can take custom methods.  This is particularly when you want to add complex field conversion logic inside a logging statement that you don't want available to the enclosing class, i.e.\n\n```scala\nobject MyFieldBuilder extends FieldBuilder with Logging {\n  def state1(foo: Foo): Field = keyValue(foo, foo).withDisplayName(\"foo in state one\")\n}\n\nval logger = LoggerFactory.getLogger(getClass, MyFieldBuilder)\n\nlogger.info(\"User {} can do complex method {}\", fb.list(\n  fb.keyValue(\"name\" -\u003e \"will\"),\n  fb.state1(foo)\n))\n```\n\nYou can append fields together using `++` from `LowPriorityImplicits` if you prefer to not use `fb.list`:\n\n```scala\nimport echopraxia.api._\n\nlogger.info(\"User {} can do complex method {}\", fb =\u003e fb.keyValue(\"name\" -\u003e \"will\") ++ fb.myComplexMethod(foo))\n```\n\nField builder functions have the advantage of being lazily evaluated, which makes them useful in a debugging context.  For example, if you are rendering a field for debugging, you may want to wrap it in a conditional to avoid unnecessary object creation:\n\n```scala\nif (logger.isLoggingDebug()) {\n  val field: Field = \"foo\" -\u003e foo\n  logger.debug(\"use isLoggingDebug to avoid creation of field {}\", field)\n}\n```\n\nThis is not needed with field builder functions, as they are only executed when the log level is met.\n\n```scala\nlogger.debug(\"This only creates field if logger.level \u003e= DEBUG {}\", _.keyValue(\"foo\" -\u003e foo\"))\n```\n\n### Options, Either, and Future\n\nCommonly used types like `Option`, `Either`, and `Future` are handled automatically by `EchopraxiaBase`:\n\n```scala\nval optInt: Option[Int] = None\nlogger.info(\"optInt\" -\u003e optInt)  // None will render as \"optInt\": null in JSON.\n```\n\nor for Either:\n\n```scala\nval eitherInt: Either[Int, String] = Left(1)\nlogger.info(\"eitherInt\" -\u003e eitherInt) // \"eitherInt\": 1\n```\n\nor for futures, you get an object back that has the completed status and if completed, either the success or failure value.\n\n```scala\nval future = Future.successful(\"yay\")\nlogger.info(\"future\" -\u003e future) // future={completed=true, success=yay}\n```\n\n## Extending Logging\n\nExtending logging is done by extending the `ToValue`, `ToName` type classes that are packaged in `EchopraxiaBase`.\n\n### ToValue\n\nThe `ToValue` type class looks like this:\n\n```scala\npackage echopraxia.plusscala.api\nimport echopraxia.api.Value\n\ntrait ValueTypeClasses {\n  trait ToValue[-T] {\n    def toValue(t: T): Value[_]\n  }\n}\n```\n\nLet's start off by adding a `ToValue` for `java.time.Instant`, by converting it to a string.  There are already `ToValue` mappings for all the built-ins like string, boolean, and numbers, so calling `ToValue(instant.string)` will resolve the implicit and return a `Value[String]`.\n\n```scala\nimport echopraxia.plusscala.api._\nimport java.time.Instant\n\ntrait Logging extends EchopraxiaBase {\n  implicit val instantToValue: ToValue[Instant] = instant =\u003e ToValue(instant.toString)\n}\n```\n\nNow you can log `Instant` in the same way:\n\n```scala\nlogger.info(\"now\" -\u003e Instant.now)\n```\n\nIf you want to render an object, you must use `ToObjectValue`, which takes a `Seq[Field]`:\n\n```scala\ncase class Person(name: String, age: Int)\n\nimplicit val personToValue: ToObjectValue[Person] = { person =\u003e\n  ToObjectValue(\n    \"name\" -\u003e person.name,\n    \"age\" -\u003e person.age\n  )\n}\n```\n\nYou can also specify a common format for tuples and maps:\n\n```scala\ntrait Logging extends EchopraxiaBase {\n  implicit def tupleToValue[TVK: ToValue, TVV: ToValue]: ToValue[Tuple2[TVK, TVV]] = { case (k, v) =\u003e\n    ToObjectValue(\"key\" -\u003e k, \"value\" -\u003e v)\n  }\n}\n```\n\nallows for rendering of a map as a series of tuples:\n\n```scala\n// people=[{key=person1, value={name=Person1, age=12}}, {key=person2, value={name=Person2, age=15}}]\nlogger.info(\"people\" -\u003e Map(\"person1\" -\u003e person1, \"person2\" -\u003e person2))\n```\n\nYou can also include more complex logic in a `ToValue`, for example dealing with sensitive values can be handled by adding an implicit flag:\n\n```scala\ntrait Logging extends EchopraxiaBase {\n  implicit def creditCardToValue(implicit cap: Sensitive = Censored): ToValue[CreditCard] = cc =\u003e {\n    ToObjectValue(\n      sensitiveKeyValue(\"cc_number\", cc.number),\n      \"expiration_date\" -\u003e cc.expirationDate\n    )\n  }\n\n  def sensitiveKeyValue(name: String, value: String)(implicit cap: Sensitive = Censored): Field = {\n    cap match {\n      case Censored =\u003e\n        name -\u003e \"[CENSORED]\"\n      case Explicit =\u003e\n        name -\u003e value\n    }\n  }\n\n  sealed trait Sensitive\n  case object Censored extends Sensitive\n  case object Explicit extends Sensitive\n}\n```\n\n### ToName\n\nRather than using a tuple, you can specify a default name for a field using the `ToName` value class.\n\nThe `ToName` type class looks like this:\n\n```scala\ntrait NameTypeClass {\n  trait ToName[-T] {\n    def toName(t: Option[T]): String\n  }\n}\n```\n\nand is typically defined as an implicit like this:\n\n```scala\ntrait Logging extends EchopraxiaBase {\n  implicit val instantToName: ToName[Instant] = _ =\u003e \"instant\"\n}\n```\n\nThere are traits that provide implicits that can resolve common effects, such as `OptionToNameImplicits`, `TryToNameImplicits`, and `EitherToNameImplicits`.\n\nIf a type has both `ToName` and `ToValue` specified on it, then you can pass in the object and have a field rendered automatically.\n\n```scala\nval epoch = Instant.EPOCH\nlogger.info(epoch) // instant=1970-01-01T00:00:00Z\n```\n\nThis comes in very handy when using `Future`, for example:\n\n```scala\ntrait Logging extends EchopraxiaBase {\n  implicit def futureToName[T: ClassTag]: ToName[Future[T]] = _ =\u003e s\"future[${classTag[T].runtimeClass.getName}]\"\n}\n```\n\nyields the future's type:\n\n```scala\nlogger.info(Future.successful(true)) // future[boolean]={completed=true, success=true}\n```\n\n### ToField\n\nBecause `ToName` and `ToValue` are commonly specified together, you can set mappings for both at the same time using `ToField`.\n\nFor example, rather than defining\n\n```scala\ncase class Title(raw: String)    extends AnyVal\n\ntrait Logging extends EchopraxiaBase {\n  implicit val titleToName: ToName[Title] = _ =\u003e \"title\"\n  implicit val titleToValue: ToValue[Title] = t =\u003e ToValue(t.raw)\n}\n```\n\nYou could define both with `ToField(toNameFunction, toFieldFunction)`:\n\n```scala\ntrait Logging extends EchopraxiaBase {\n  implicit val titleToField: ToField[Title] = ToField(_ =\u003e \"title\", t =\u003e ToValue(t.raw))\n}\n```\n\nThis gets especially useful when you are building up complex state objects where the case class fields all line up:\n\n```scala\ntrait Logging extends EchopraxiaBase {\n  implicit val titleToField: ToField[Title] = ToField(_ =\u003e \"title\", t =\u003e ToValue(t.raw))\n\n  implicit val authorToField: ToField[Author] = ToField(_ =\u003e \"author\", a =\u003e ToValue(a.raw))\n\n  implicit val categoryToField: ToField[Category] = ToField(_ =\u003e \"category\", c =\u003e ToValue(c.raw))\n\n  implicit val bookToField: ToField[Book] = ToField(_ =\u003e \"book\", book =\u003e ToObjectValue(book.title, book.category, book.author, book.price))\n}\n```\n\nyields\n\n```scala\nval book1 = Book(\n  Category(\"reference\"),\n  Author(\"Nigel Rees\"),\n  Title(\"Sayings of the Century\")\n)\nlogger.info(book) // book={title=Sayings of the Century, category=reference, author=Nigel Rees}\n```\n\n## Field Presentation\n\nThere are times when the default field presentation is awkward, and you'd like to cut down on the amount of information displayed in the message. You can do this by adding presentation hints to the field.\n\n### AsValueOnly\n\nThe `asValueOnly` method has the effect of turning a \"key=value\" field into a \"value\" field in text format, just like the value method:\n\n```scala\nval field: Field = \"name\" -\u003e \"value\"\nfield.asValueOnly.toString must be(\"value\")\n```\n\n### Display Name\n\nThe `withDisplayName` method shows a human-readable string in text format bracketed in quotes:\n\n```scala\nval field: Field = \"name\" -\u003e 1\nfield.withDisplayName(\"human readable name\").toString must be(\"\\\"human readable name\\\"=1\")\n```\n\n### Elided\n\nThe `asElided` method will elide the field so that it is passed over and does not show in text format:\n\n```scala\nval field: Field = \"name\" -\u003e 1\nfield.asElided.toString must be(\"\")\n```\n\n## Value Presentation\n\nValue presentation changes how values are rendered in a line oriented format, so that they are more human readable. Value presentation is different from field presentation in that the field name cannot be changed in value presentation.\n\n### AsCardinal\n\nThe asCardinal method, when used on an array value or on a string value, displays the number of elements in the array bracketed by \"|\" characters in text format:\n\n```scala\nval cardinalField: Field = \"elements\" -\u003e ToValue(1,2,3).asArray.asCardinal();\ncardinalField.toString(); // renders elements=|3|\n```\n\nor for a string value:\n\n```scala\nval cardinalField: Field = \"elements\" -\u003e ToValue(\"123\").asString.asCardinal();\ncardinalField.toString(); // renders elements=|3|\n```\n\n### AbbreviateAfter\n\nThe `abbreviateAfter` method will truncate an array or string that is very long and replace the rest with ellipsis:\n\n```scala\nvar abbrField = keyValue(\"abbreviatedField\", ToValue(veryLongString).asString.abbreviateAfter(5));\nabbrField.toString(); // renders abbreviatedField=12345...\n```\n\n### ToStringValue\n\nThe `withToStringValue` uses a custom string for the value, providing something more human-readable. This is particularly useful in arrays and complex nested objects, where you may want a summary of the object rather than the full JSON rendering.\n\n```scala\nval formatter = DateTimeFormatter.ofPattern(\"dd/MM/yyyy\").withZone(ZoneId.systemDefault());\nval instantField: Field = \"instant\" -\u003e ToValue(instant.toString()).withToStringValue(formatter.format(instant));\ninstantField.toString(); // renders ISO8601 in JSON, but 01/01/1970 with toString()\n```\n\n## Context\n\nYou can compose loggers with [context](https://github.com/tersesystems/echopraxia#context) using `withFields` and the context fields will render in JSON:\n\n```scala\nval loggerWithField = logger.withFields(\"correlationId\" -\u003e correlationId)\nloggerWithField.info(\"renders with correlationId in JSON\")\n```\n\nThis is \"call by name\" i.e. the function defined is evaluated on every logging statement and may change between logging statements.\n\n## API\n\nYou can convert levels, conditions, and logging contexts to Java using the `.asJava` suffix.\n\nConversion of Java levels, conditions, and logging contexts are handled through type enrichment adding `.asScala` methods to the classes.\n\nTo enable type enrichment, import the `api` package,\n\n```scala\nimport echopraxia.plusscala.api._\n```\n\nor\n\n```scala\nimport echopraxia.plusscala.api.Implicits._\n```\n\nexplicitly if you only want the implicits.\n\nThis is useful when using the [condition scripts](https://github.com/tersesystems/echopraxia#dynamic-conditions-with-scripts) module of Echopraxia, for example.\n\n## Conditions\n\nConditions in the Scala API use Scala idioms and classes.  \n\nThe `JsonPathCondition` provides some useful JSONPath methods, but is only available by default for the `logstash` module.  You can add the `jsonpath` module to JUL or Log4J2, but it does have an SLF4J dependency.\n\nThe `find` methods in the logging context are converted to Scala, so `java.math.BigInteger` is converted to `BigInt`, for example:\n\n```scala\nimport echopraxia.plusscala.logging.api.JsonPathCondition\n\nval bigIntCondition = JsonPathCondition(_.findNumber(\"$.bigInt\").contains(BigInt(\"52\")))\nval bigIntLogger = logger.withCondition(bigIntCondition)\nbigIntLogger.info(\"only logs if bigInt is 52\", _.number(\"bigInt\", BigInt(\"52\")))\n```\n\nLikewise, if you look up `findList` to find an object, it will return the object as a `Map[String, Any]` which you can then match on.\n\n```scala\nval isWill = JsonPathCondition { context =\u003e\n  val list = context.findList(\"$.person[?(@.name == 'will')]\")\n  val map = list.head.asInstanceOf[Map[String, Any]]\n  map(\"name\") == \"will\"\n}\n```\n\nAlso, `ctx.fields` returns a `Seq[Field]` which allows you to match fields using the Scala collections API.  You can use this to match on fields and values without using a JSON path, which can be useful when you want to match on an entire object rather than a single path.\n\n```scala\n// matching a field is easier than multiple inline predicates\nprivate val willField: Field = (\"person\" -\u003e Person(\"will\", 1))\nprivate val condition: Condition = Condition(_.fields.contains(willField))\n\ndef conditionUsingFields() = {\n  val thisPerson = Person(\"will\", 1)\n  logger.info(condition, \"person matches! {}\", \"person\" -\u003e thisPerson)\n}\n```\n\nLevels in conditions have in-fix comparison operators:\n\n```scala\nval infoOrHigherCondition: Condition = Condition { (level, ctx) =\u003e\n  level \u003e= Level.INFO // same as greaterThanOrEqual\n}\n```\n\nConditions can be composed using the logical operators `and`, `or`, and `xor`:\n\n```scala\nval andCondition: Condition = conditionOne and conditionTwo\nval orCondition: Condition = conditionOne or conditionTwo\nval xorCondition: Condition = conditionOne xor conditionTwo\n```\n\nThere are two special conditions, `Condition.always` and `Condition.never`.  Using one of these conditions will short-circuit other conditions under the right circumstances, and can enable logging optimizations.\n\nFor example, using `logger.withCondition(Condition.always)` will return the same logger, while using `Condition.never` will result in a no-op logger being returned:\n\n```scala\nval neverLogger = logger.withCondition(Condition.never)\nneverLogger.error(\"I will never log\") // no-op\n```\n\nBecause the JVM is very good at optimizing out no-op methods, using `Condition.never` is only ~1ns overhead over a straight call.\n\nYou can change these constants to have different names by overriding the resource bundle.  You can also override the `sourceInfoFields` using a custom logger to change or suppress source code fields entirely.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftersesystems%2Fechopraxia-plusscala","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftersesystems%2Fechopraxia-plusscala","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftersesystems%2Fechopraxia-plusscala/lists"}