lego / woof Goto Github PK
View Code? Open in Web Editor NEWA pure Scala 3 logging library with no reflection
License: Other
A pure Scala 3 logging library with no reflection
License: Other
Since we moved to the "applicative" style Filter DSL, it's no longer easy to inspect the functionality of the different filter nodes. We should add scaladocs to explain each node in detail.
Example: What exactly does ClassRegex(regex: Regex)
do?
Going forward, it would be nice to have a slf4j-2
module to support the new binding mechanism. After a while, we can drop the current slf4j
module completely.
The core module could be used in Scala.js
so cross building that module and adding it to the release pipeline would be nice.
Now that 2.0.x
is out, we need to either fully adopt it or split the slf4j module in 2 versions.
As I read the table above, users DO need to update their SLF4J
API-version if we fully adopt version 2.0.x. This might not be straightforward to do for the users if the API is provided by their framework, or if the API is consumed by downstream dependencies (e.g. http4s
).
Ultimately, this means splitting is the only sensible option if we mean to support the new version, as users will be forced to override dependencies of downstream libraries otherwise.
org.legogroup.woof.slf4j.WoofLogger
has a hard dependency on slf4j-api
, however, since 2.0.x
is backwards compatible, we should be able to re-use that code for both 1.x.x
and 2.x.x
lines leaving only a re-implementation of org.slf4j.impl.StaticLoggerBinder
which is small.
If we choose to go ahead with this, the 2.x.x
line needs to implement a ServiceLoader instead of a static binder while keeping WoofLogger
completely as-is.
This is a bit trickier because we support scala.js. We could support this for node environments, but not really in the browser. There is also the option to skip scala.js completely for this feature, but I think having node support would be quite nice.
File rotation could also be nice.
Hi,
I was wondering if you would consider a custom (probably optional) argument for makeIOLogger
so that the value of enclosingClass
is not computed via macro?
My use case is that I already have a Logger
interface all my services use, so I want to abstract over any implementation detail (woof could be just one of many interpreters). At the moment, the "enclosing class" is the following one, which is not pretty:
trading.lib.Logger$._$$anon
Ideally, though, this should represent the package of each service instead of this common package name.
For reference, here's how I instantiate it in my Logger
:
val make: IO[Logger[IO]] =
given Filter = Filter.everything
given Printer = ColorPrinter()
WoofLogger.makeIoLogger(Output.fromConsole).map(from[IO])
Or maybe there's a way to make my logger constructor aware of the right enclosing class via macro?
Thanks!
Currently, creating a logger is effectful because we create an IOLocal
context.
object DefaultLogger:
def makeIo(output: Output[IO], outputs: Output[IO]*)(using Clock[IO], Printer, Filter): IO[DefaultLogger[IO]] =
for given StringLocal[IO] <- ioStringLocal
yield new DefaultLogger[IO](output, outputs*)
end DefaultLogger
The actual interface requires a local instance to allow adding context to effects:
trait Logger[F[_]]:
val stringLocal: StringLocal[F]
def doLog(level: LogLevel, message: String)(using LogInfo): F[Unit]
...
end Logger
This in turn allows us to scope context to an effect value whenever we have a logger in scope:
def program(using Logger[IO]): IO[Unit] =
for
_ <- Logger[IO].debug("This is some debug")
_ <- Logger[IO].info("HEY!")
_ <- Logger[IO].warn("I'm warning you")
_ <- Logger[IO].error("I give up")
yield ()
import Logger.*
val mainWithContext: IO[Unit] =
for
given Logger[IO] <- DefaultLogger.makeIo(consoleOutput)
_ <- program.withLogContext("trace-id", "4d334544-6462-43fa-b0b1-12846f871573")
_ <- Logger[IO].info("Now the context is gone")
yield ()
This is very convenient and easy to use. There are 2 main drawbacks:
Profunctor
for this interfaceIf the Logger
no longer has a reference to the local context, we place the burden on the user to pass around this context. It's not clear if there's a good solution to avoid this (maybe Odin has solved this?).
Simplify the interface to something akin to:
case class LogLine(level: LogLevel, message: String, context: Map[String,String], info: LogInfo)
trait Logger[F[_]]:
def doLog(line: LogLine): F[Unit]
end Logger
There has been progress in scala-native world recently. It got runtime for cats-effect. As this library is built on cats-effect, would it be possible to add support for scala-native?
There is a Contributing.md, but there is no useful content in there. There should be one.
Currently, log filters are defined as:
type Filter = LogLine => Boolean
which is the simplest and most flexible way to filter logs. It works quite well and supports any non-effectful filter you could come up with.
In the end, we often use a composition of the pre-defined filters with atLeastLevel
, regexFilter
, or everything
.
These filters can all be defined in a "applicative style", allowing for easier testing and introspection at the cost of some minor flexibility.
Having an applicative filter would allow performance optimizations not currently possible, e.g. using the nothing
filter could eliminate the overhead of materializing the log line in the first place.
Currently, you'd have to fold over a list of key value pairs and add them to the log context โ the proposal is to add an overload or modify withLogContext
to take varargs instead.
In the end, printers are just styling. It would be nice to have a few more options (different colors, different timestamp layout, reordering of information etc).
This is more for discussion. Sometimes, it may be interesting to understand which fiber/thread you are logging from. I believe this may incur some runtime cost.
Having a class with a deep nesting e.g. com.mydomain.services.implementations.MyClassImpl
adds a lot of noise to logs. It could be shortened to c.m.s.i.MyClassImpl
or similar.
However, for shorter paths it might make sense to keep everything, e.g. services.MigrationService
.
It would be nice to have a Printer
that, depending on the length of the string, cuts down the class path based on some heuristic.
Another consideration would be to expand the LogInfo
class https://github.com/LEGO/woof/blob/main/modules/core/src/main/scala/org/legogroup/woof/LogInfo.scala such that enclosingClass
gets a more precise type than the current String
.
This would make the Printer
implementation more straight forward.
Currently, there's not much documentation regarding e.g. configuration of log levels, printers, outputs etc. It would make sense to create and maintain a module with a bunch of self-contained main functions, each showing some aspect of the library.
Currently, the org.legogroup.Output
expects String
as input which would stop the application from efficiently doing any kind of structured logging.
Combining the Printer
and Output
concept would easily solve this with a method signature similar to:
def output(
epochMillis: EpochMillis,
level: LogLevel,
info: LogInfo,
message: String,
context: List[(String, String)],
): F[Unit]
however, this will lead to a less composable architecture. We could change the arguments to a case class:
case class LogLine(
epochMillis: EpochMillis,
level: LogLevel,
info: LogInfo,
message: String,
context: List[(String, String)],
)
which would make it much easier to pass around. I still think there's some merit to separating the stateless transformations from the effectful outputs, but it's not easy to pick a suitable input type for the Output
class methods.
Consider the following:
Here, it would be nice to allow pairing up printers and outputs which would basically solve this and many more cases.
On a final note, I do realize that we could simply change it to be 1 concept of LogLine => F[Unit]
and let the library users do their own function composition, but I feel like that may be a bit too unclear and not very intention-revealing. This solution will not force people to use String
as an intermediate format, though!
Thoughts are very welcome!
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.