Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Feature: logging capability #421

Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ val catsEffectV = "2.4.1"
val slf4jV = "1.7.30"
val munitCatsEffectV = "1.0.1"
val logbackClassicV = "1.2.3"
val enclosureV = "0.1.0"

Global / onChangedBuildSource := ReloadOnSourceChanges

Expand Down Expand Up @@ -111,11 +112,17 @@ lazy val core = crossProject(JSPlatform, JVMPlatform)
.settings(
name := "log4cats-core",
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-core" % catsV
"org.typelevel" %%% "cats-core" % catsV,
"com.lorandszakacs" %%% "enclosure" % enclosureV,
)
)
lazy val coreJVM = core.jvm
lazy val coreJS = core.js
lazy val coreJS = core
.settings(dottyJsSettings(ThisBuild / crossScalaVersions))
.jsSettings(
scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule))
)
.js

lazy val testing = crossProject(JSPlatform, JVMPlatform)
.settings(commonSettings, releaseSettings)
Expand Down Expand Up @@ -159,6 +166,20 @@ lazy val commonSettings = Seq(
"org.typelevel" %%% "munit-cats-effect-2" % munitCatsEffectV % Test,
),
testFrameworks += new TestFramework("munit.Framework"),
// major scala version folders do not work properly for shared sources between JS/JVM
// see: https://github.com/sbt/sbt/issues/5895#issuecomment-739510744
Compile / unmanagedSourceDirectories ++= {
val major = if (isDotty.value) "-3" else "-2"
List(CrossType.Pure, CrossType.Full).flatMap(
_.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + major))
)
},
Test / unmanagedSourceDirectories ++= {
val major = if (isDotty.value) "-3" else "-2"
List(CrossType.Pure, CrossType.Full).flatMap(
_.sharedSrcDir(baseDirectory.value, "test").toList.map(f => file(f.getPath + major))
)
}
)

lazy val releaseSettings = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2018 Christopher Davenport
*
* 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.
*/

package org.typelevel.log4cats

import com.lorandszakacs.enclosure.Enclosure

/**
* Logging capability trait, or put crudeley "logger factory".
*
* The recommended way of creating loggers is through this capability trait.
* You instantiate it once in your application (dependent on the specific
* logging backend you use), and pass this around in your application.
*
* This has several advantages:
* - you no longer pass around _very powerful_ `F[_]: Sync` constraints that can do
* almost anything, when you just need logging
* - you are in control of how loggers are created, and you can even add in whatever
* custom functionality you need for your own applications here. e.g. create loggers
* that also send logs to some external providers by giving an implementation to this
* trait.
*
* @tparam G[_]
* the effect type in which the loggers are constructed.
* e.g. make cats.Id if logger creation can be done as a pure computation
*/
trait GenLogging[G[_], LoggerType] {
def fromName(name: String): G[LoggerType]

def create(implicit enc: Enclosure): G[LoggerType] = fromName(enc.fullModuleName)

def fromClass(clazz: Class[_]): G[LoggerType] =
fromName(clazz.getName) //N.B. .getCanonicalName does not exist on scala JS.
}

object GenLogging {
def apply[G[_], LoggerType](implicit l: GenLogging[G, LoggerType]): GenLogging[G, LoggerType] =
l
}

object Logging {
def apply[F[_]](implicit l: Logging[F]): Logging[F] = l
}

object LoggingF {
def apply[F[_]](implicit l: LoggingF[F]): LoggingF[F] = l
}
123 changes: 123 additions & 0 deletions core/shared/src/main/scala/org/typelevel/log4cats/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright 2018 Christopher Davenport
*
* 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.
*/

package org.typelevel

package object log4cats {

/**
* Convenience type for describing Logging where creation of the loggers itself is a pure operation
*
* @tparam F[_]
* the effect type in which logging is done.
*/
@scala.annotation.implicitNotFound(
"""
Implicit not found for Logging[${F}]. Which is a convenience type alias.

Keep in mind that:

a) Logging[${F}] has to be created _explicitely_ once (at least) in your code

b) for the type Logging, ${F} is _only_ the effect in which logging is done.
```
Logging[${F}].create.info(message) : ${F}[Unit]
```

c) The Logging[${F}] type itself described logging creation as being pure, i.e. in cats.Id

d) Logging[${F}] is defined as creating _only_ loggers of type SelfAwareStructuredLogger[${F}].

The full definition of Logging is:
```
type Logging[F[_]] = GenLogging[cats.Id, SelfAwareStructuredLogger[F]]
```

Therefore, your problem might be:
1) you didn't create a Logging[${F}] to begin with, or didn't mark it as implicit
2) you only have a GenLogger[G[_], SelfAwareStructuredLogger[${F}]] for some G[_] type other than cats.Id
3) you do actually have a GenLogger[Id, L], but where L is not SelfAwareStructuredLogger[${F}].

If you are unsure how to create a Logging[${F}], then you can to look at the `log4cats-slf4j`,
or `log4cats-noop` modules for concrete implementations.

Example for slf4j:
```
import cats.effect.IO

// we create our Logging[IO]
implicit val logging: Logging[IO] = Slf4jLogging.forSync[IO]

//we summon our instance, and create our logger
val logger: SelfAwareStructuredLogger[IO] = Logging[IO].create
logger.info("logging in IO!"): IO[Unit]
```
"""
)
type Logging[F[_]] = GenLogging[cats.Id, SelfAwareStructuredLogger[F]]

/**
* Convenience type
*
* @tparam F[_]
* the effect type of both logger creation, and logging.
*/
@scala.annotation.implicitNotFound(
"""
Implicit not found for LoggingF[${F}]. Which is a convenience type alias.

Keep in mind that:

a) LoggingF[${F}] has to be created _explicitely_ once (at least) in your code

b) for the type LoggingF, ${F} is _both_ the effect in which logger creation is done
_and_ the effect in which logging is done
```
val loggerF: ${F}[SelfAwareStructuredLogger] = Logging[${F}].create
loggerF.flatMap{logger => logger.info("the result of the .info is in F"): ${F}[Unit] }: ${F}[Unit]
```

c) LoggingF[${F}] is defined as creating _only_ loggers of type SelfAwareStructuredLogger[${F}].

The full definition of Logging is:
```
type LoggingF[F[_]] = GenLogging[F, SelfAwareStructuredLogger[F]]
```
Therefore, your problem might be:
1) you didn't create a LoggingF[${F}] to begin with, or didn't mark it as implicit
2) you only have a GenLoggerF[G[_], SelfAwareStructuredLogger[${F}]] for some G[_] type other than type ${F}
3) you do actually have a GenLogger[${F}, L], but where L is not SelfAwareStructuredLogger[${F}].
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can/should we support users doing import org.typelevel.log4s.Slf4jLogging.implicits._?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would prefer not to 😅

It would carry something like implicit def genericSlf4jLogging[F[_]: Sync]: Slf4jLogging[F, F]? And others, provided they don't wind up conflicting with each other under the same import.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, why not? Shouldn't conflict as long as types are different, so it could be both Logging and LoggingF under one object. I've no objections to not having it as an experienced Scala-FP-CE-TF developer but not everybody's like that, and a page's worth of text for implicitNotFound to describe how to refactor all your code is probably going to alienate more than help.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good points. But I am still in favor of a page worth of implicitNotFound because adopters of Logging as capability are probably experienced Scala-FP-CE-TF devs, since it's an entirely new feature.

I can also move the practical examples first, with the alternative of a-la-carte implicits import as well, and then move the general points about what might be wrong at the bottom.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that sounds better. Even "experienced Scala-FP-CE-TF devs" prefer a simple entry point (cue: CE2 Timer/ContextShift/Concurrent vs CE3 import unsafe), and for people just trying log4cats out (there's always a lot of people swayed by TF but not yet with enough experience to handle all the issues) that would provide a straightforward experience.

Copy link
Member Author

@lorandszakacs lorandszakacs Apr 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the PR, rephrased the implicitNotFound message so it doesn't seem like we're giving refactoring advice. But now it's somehow even longer 😅


If you are unsure how to create a LoggingF[${F}], then you can to look at the
`log4cats-slf4j` module for concrete implementations.

Example for slf4j:
```
import cats.effect.IO

// we use IO for both logger creation effect, and logging effect. Otherwise it wouldn't be a LoggingF
implicit val loggingF: LoggingF[IO] = Slf4jGenLogging.forSync[IO, IO]

//we summon our instance, and create our logger
val loggerIO: IO[SelfAwareStructuredLogger[IO]] = LoggingF[IO].create

loggerIO.flatMap{logger => logger.info("logging in IO"): IO[Unit]}: IO[Unit]
```
"""
)
type LoggingF[F[_]] = GenLogging[F, SelfAwareStructuredLogger[F]]

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2018 Christopher Davenport
*
* 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.
*/

package org.typelevel.log4cats

// TODO: figure out why the implicitNotFound annotation does not work on Scala 3.
class LoggingImplicitNotFoundTests extends munit.FunSuite {

test("receive proper implicitNotFound error message on Logging") {
val errors = compileErrors(
"import cats.effect.IO;import org.typelevel.log4cats.Logging;Logging[IO]"
)
assert(
errors.contains(
"you didn't create a Logging[cats.effect.IO] to begin with, or didn't mark it as implicit"
),
clue = s"""|Actual compiler error was:
|
|$errors
|
|
|""".stripMargin
)
}

test("receive proper implicitNotFound error message on LoggingF") {
val errors = compileErrors(
"import cats.effect.IO;import org.typelevel.log4cats.LoggingF;LoggingF[IO]"
)
assert(
errors.contains(
"you didn't create a LoggingF[cats.effect.IO] to begin with, or didn't mark it as implicit"
),
clue = s"""|Actual compiler error was:
|
|$errors
|
|
|""".stripMargin
)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2018 Christopher Davenport
*
* 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.
*/

package org.typelevel.log4cats.noop

import cats.Applicative
import org.typelevel.log4cats._

trait NoOpLogging[F[_]] extends Logging[F] {
override def fromName(name: String): SelfAwareStructuredLogger[F]
}

object NoOpLogging {
def apply[F[_]](implicit logging: NoOpLogging[F]): NoOpLogging[F] = logging
def create[F[_]: Applicative] = new NoOpLogging[F] {
override def fromName(name: String): SelfAwareStructuredLogger[F] = NoOpLogger[F]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2018 Christopher Davenport
*
* 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.
*/

package org.typelevel.log4cats.slf4j

import org.slf4j.{Logger => JLogger}
import cats.effect.Sync
import org.typelevel.log4cats._
import cats.{Applicative, Defer, Id}

// format: off
trait Slf4jGenLogging[G[_], F[_]] extends GenLogging[G, SelfAwareStructuredLogger[F]] {
protected implicit def syncF: Sync[F]
protected def lift(f: => SelfAwareStructuredLogger[F]): G[SelfAwareStructuredLogger[F]]

override def fromName(name: String): G[SelfAwareStructuredLogger[F]] = lift(Slf4jLogger.getLoggerFromName[F](name))
def fromSlf4j(logger: JLogger): G[SelfAwareStructuredLogger[F]] = lift(Slf4jLogger.getLoggerFromSlf4j[F](logger))
}
// format: off

object Slf4jGenLogging {
def apply[G[_], F[_]](implicit logging: Slf4jGenLogging[G, F]): Slf4jGenLogging[G, F] = logging

// format: off
def forSync[G[_], F[_]](implicit F: Sync[F], G: Applicative[G], GD: Defer[G]): Slf4jGenLogging[G, F] =
new Slf4jGenLogging[G, F] {
override protected val syncF: Sync[F] = F
override protected def lift(l: => SelfAwareStructuredLogger[F]): G[SelfAwareStructuredLogger[F]] = GD.defer(G.pure(l))
}
// format: on
}

trait Slf4jLogging[F[_]] extends Slf4jGenLogging[Id, F]

object Slf4jLogging {
def apply[F[_]](implicit logging: Slf4jLogging[F]): Slf4jLogging[F] = logging

// format: off
def forSync[F[_]](implicit F: Sync[F]): Slf4jLogging[F] = new Slf4jLogging[F] {
override protected def lift(f: => SelfAwareStructuredLogger[F]): Id[SelfAwareStructuredLogger[F]] = f
override protected def syncF: Sync[F] = F
}
// format: on
}
Loading