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 all 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
}
159 changes: 159 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,159 @@
/*
* 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.

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.

Quickest fix might be to import the implicits:

```
// assumes dependency on log4cats-slf4j module
import org.typelevel.log4cats._
import org.typelevel.log4cats.slf4j.implicits._
val logger: SelfAwareStructuredLogger[IO] = Logging[IO].create
// or:
def anyFSyncLogger[F[_]: Sync]: SelfAwareStructuredLogger[F] = Logging[F].create
```

Alternatively, a mutually exclusive solution, is to explicitely create your own Logging[F] instances
and pass them around implicitely:

```
import cats.effect.IO
import org.typelevel.log4cats._
import org.typelevel.log4cats.slf4j._

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

def useLogging[F[_]: Logging] = Logging[F].create.info("yay! effect polymorphic code")

useLogging[IO]
```

If neither of those things work, keep in mind that:
a) for the type Logging, ${F} is _only_ the effect in which logging is done.
```
Logging[${F}].create.info(message) : ${F}[Unit]
```

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

c) 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 `import org.typelevel.log4cats.slf4j.implicits._` AND created your own instance. Do one or the either.
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}].
"""
)
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.

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

Quickest fix might be to import the implicits:

```
// assumes dependency on log4cats-slf4j module
import org.typelevel.log4cats._
import org.typelevel.log4cats.slf4j.implicits._
import cats.effect.{Sync, IO}

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

def anyFSyncLogger[F[_]: Sync]: F[SelfAwareStructuredLogger[F]] = LoggingF[F].create
```

Alternatively, a mutually exclusive solution, is to explicitely create your own LoggingF[F] instances
Copy link
Contributor

@nigredo-tori nigredo-tori Apr 16, 2021

Choose a reason for hiding this comment

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

Just passing by here... Is this wall of text really necessary? I imagine that in most cases this warning would be caused by missing imports. I feel like the rest of this should be moved to documentation, with the warning itself stripped to something like this:

Implicit not found for LoggingF[${F}]. This is often caused by missing imports:

import org.typelevel.log4cats._
import org.typelevel.log4cats.slf4j.implicits._

Additional information can be found here: https://log4cats.github.io/logging-capability.html

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.

That's probably a better approach 👍 will move the message once I put docs updates in the PR.

and pass them around implicitely:

```
// assumes dependency on log4cats-slf4j module
import org.typelevel.log4cats._
import org.typelevel.log4cats.slf4j._
import cats.effect.{Sync, 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]

def useLogging[F[_]: LoggingF] = LoggingF[F].create.flatMap{logger => logger.info("yay! effect polymorphic code")}

useLogging[IO]
```

If neither of those things work, keep in mind that:
a) 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]
```

b) 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 `import org.typelevel.log4cats.slf4j.implicits._` AND created your own instance. Do one or the either.
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 😅

"""
)
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(
"Logging[cats.effect.IO] is defined as creating _only_ loggers of type SelfAwareStructuredLogger[cats.effect.IO]"
),
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(
"val loggerF: cats.effect.IO[SelfAwareStructuredLogger] = Logging[cats.effect.IO].create"
),
clue = s"""|Actual compiler error was:
|
|$errors
|
|
|""".stripMargin
)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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 org.typelevel.log4cats.noop.internal.NoOpLoggingInstances

object implicits extends NoOpLoggingInstances
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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.internal

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

trait NoOpLoggingInstances {
implicit def log4catsSummonNoOpGenLogging[G[_]: Applicative, F[_]: Applicative]: NoOpLoggingGen[
G,
F
] = NoOpLoggingGen[G, F]
}
Loading