Skip to content

Commit 1c43278

Browse files
Merge pull request #629 from hamnis/logger-factory
Add LoggerFactory typeclass
2 parents d920281 + 688e205 commit 1c43278

File tree

18 files changed

+519
-177
lines changed

18 files changed

+519
-177
lines changed

build.sbt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import com.typesafe.tools.mima.core._
2+
13
val Scala213 = "2.13.8"
24
val Scala212 = "2.12.15"
35
val Scala3 = "3.0.2"
@@ -45,7 +47,11 @@ lazy val core = crossProject(JSPlatform, JVMPlatform)
4547
name := "log4cats-core",
4648
libraryDependencies ++= Seq(
4749
"org.typelevel" %%% "cats-core" % catsV
48-
)
50+
),
51+
libraryDependencies ++= {
52+
if (tlIsScala3.value) Seq.empty
53+
else Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided)
54+
}
4955
)
5056

5157
lazy val testing = crossProject(JSPlatform, JVMPlatform)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2018 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.log4cats
18+
19+
import org.typelevel.log4cats.internal.LoggerNameMacro
20+
21+
trait LoggerNameCompat {
22+
implicit def name: LoggerName = macro LoggerNameMacro.getLoggerName
23+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright 2018 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.log4cats.internal
18+
19+
import scala.annotation.tailrec
20+
import scala.reflect.macros.blackbox
21+
22+
private[log4cats] class LoggerNameMacro(val c: blackbox.Context) {
23+
final def getLoggerName = SharedLoggerNameMacro.getLoggerNameImpl(c)
24+
}
25+
26+
private[log4cats] object SharedLoggerNameMacro {
27+
28+
/** Get a logger by reflecting the enclosing class name. */
29+
private[log4cats] def getLoggerNameImpl(c: blackbox.Context) = {
30+
import c.universe._
31+
32+
@tailrec def findEnclosingClass(sym: c.universe.Symbol): c.universe.Symbol = {
33+
sym match {
34+
case NoSymbol =>
35+
c.abort(c.enclosingPosition, s"Couldn't find an enclosing class or module for the logger")
36+
case s if s.isModule || s.isClass =>
37+
s
38+
case other =>
39+
/* We're not in a module or a class, so we're probably inside a member definition. Recurse upward. */
40+
findEnclosingClass(other.owner)
41+
}
42+
}
43+
44+
val cls = findEnclosingClass(c.internal.enclosingOwner)
45+
46+
assert(cls.isModule || cls.isClass, "Enclosing class is always either a module or a class")
47+
48+
def nameBySymbol(s: Symbol) = {
49+
def fullName(s: Symbol): String = {
50+
@inline def isPackageObject = (
51+
(s.isModule || s.isModuleClass)
52+
&& s.owner.isPackage
53+
&& s.name.decodedName.toString == termNames.PACKAGE.decodedName.toString
54+
)
55+
56+
if (s.isModule || s.isClass) {
57+
if (isPackageObject) {
58+
s.owner.fullName
59+
} else if (s.owner.isStatic) {
60+
s.fullName
61+
} else {
62+
fullName(s.owner) + "." + s.name.encodedName.toString
63+
}
64+
} else {
65+
fullName(s.owner)
66+
}
67+
}
68+
69+
q"new _root_.org.typelevel.log4cats.LoggerName(${fullName(s)})"
70+
}
71+
72+
def nameByType(s: Symbol) = {
73+
val typeSymbol: ClassSymbol = (if (s.isModule) s.asModule.moduleClass else s).asClass
74+
val typeParams = typeSymbol.typeParams
75+
76+
if (typeParams.isEmpty) {
77+
q"new _root_.org.typelevel.log4cats.LoggerName(_root_.scala.Predef.classOf[$typeSymbol].getName)"
78+
} else {
79+
if (typeParams.exists(_.asType.typeParams.nonEmpty)) {
80+
/* We have at least one higher-kinded type: fall back to by-name logger construction, as
81+
* there's no simple way to declare a higher-kinded type with an "any" parameter. */
82+
nameBySymbol(s)
83+
} else {
84+
val typeArgs = List.fill(typeParams.length)(WildcardType)
85+
val typeConstructor = tq"$typeSymbol[..${typeArgs}]"
86+
q"new _root_.org.typelevel.log4cats.LoggerName(_root_.scala.Predef.classOf[$typeConstructor].getName)"
87+
}
88+
}
89+
}
90+
91+
@inline def isInnerClass(s: Symbol) =
92+
s.isClass && !(s.owner.isPackage)
93+
94+
val instanceByName =
95+
(true && cls.isModule || cls.isModuleClass) || (cls.isClass && isInnerClass(
96+
cls
97+
))
98+
99+
if (instanceByName) {
100+
nameBySymbol(cls)
101+
} else {
102+
nameByType(cls)
103+
}
104+
}
105+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2018 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.log4cats
18+
19+
import org.typelevel.log4cats.internal.LoggerNameMacro
20+
21+
import scala.quoted.*
22+
23+
trait LoggerNameCompat {
24+
implicit inline def name: LoggerName =
25+
${ LoggerNameMacro.getLoggerName }
26+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2018 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.log4cats
18+
package internal
19+
20+
import scala.annotation.tailrec
21+
import scala.quoted.*
22+
23+
private[log4cats] object LoggerNameMacro {
24+
def getLoggerName(using qctx: Quotes): Expr[LoggerName] = {
25+
val name = getLoggerNameImpl
26+
'{ new LoggerName($name) }
27+
}
28+
29+
def getLoggerNameImpl(using qctx: Quotes): Expr[String] = {
30+
import qctx.reflect._
31+
32+
@tailrec def findEnclosingClass(sym: Symbol): Symbol = {
33+
sym match {
34+
case s if s.isNoSymbol =>
35+
report.throwError("Couldn't find an enclosing class or module for the logger")
36+
case s if s.isClassDef =>
37+
s
38+
case other =>
39+
/* We're not in a module or a class, so we're probably inside a member definition. Recurse upward. */
40+
findEnclosingClass(other.owner)
41+
}
42+
}
43+
44+
def symbolName(s: Symbol): Expr[String] = {
45+
def fullName(s: Symbol): String = {
46+
val flags = s.flags
47+
if (flags.is(Flags.Package)) {
48+
s.fullName
49+
} else if (s.isClassDef) {
50+
if (flags.is(Flags.Module)) {
51+
if (s.name == "package$") {
52+
fullName(s.owner)
53+
} else {
54+
val chomped = s.name.stripSuffix("$")
55+
fullName(s.owner) + "." + chomped
56+
}
57+
} else {
58+
fullName(s.owner) + "." + s.name
59+
}
60+
} else {
61+
fullName(s.owner)
62+
}
63+
}
64+
65+
Expr(fullName(s).stripSuffix("$"))
66+
}
67+
68+
val cls = findEnclosingClass(Symbol.spliceOwner)
69+
symbolName(cls)
70+
}
71+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2018 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.log4cats
18+
19+
import scala.annotation.implicitNotFound
20+
21+
@implicitNotFound("""
22+
Implicit not found for LoggerFactory[${F}].
23+
Information can be found here: https://log4cats.github.io/logging-capability.html
24+
""")
25+
trait LoggerFactory[F[_]] extends LoggerFactoryGen[F] {
26+
type LoggerType = SelfAwareStructuredLogger[F]
27+
}
28+
29+
object LoggerFactory extends LoggerFactoryGenCompanion {
30+
def apply[F[_]: LoggerFactory]: LoggerFactory[F] = implicitly
31+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2018 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.log4cats
18+
19+
trait LoggerFactoryGen[F[_]] {
20+
type LoggerType <: Logger[F]
21+
def getLogger(implicit name: LoggerName): LoggerType = getLoggerFromName(name.value)
22+
def getLoggerFromClass(clazz: Class[_]): LoggerType = getLoggerFromName(clazz.getName)
23+
def create(implicit name: LoggerName): F[LoggerType] = fromName(name.value)
24+
def fromClass(clazz: Class[_]): F[LoggerType] = fromName(clazz.getName)
25+
def getLoggerFromName(name: String): LoggerType
26+
def fromName(name: String): F[LoggerType]
27+
}
28+
29+
private[log4cats] trait LoggerFactoryGenCompanion {
30+
def getLogger[F[_]](implicit lf: LoggerFactoryGen[F], name: LoggerName): lf.LoggerType =
31+
lf.getLogger
32+
def getLoggerFromName[F[_]](name: String)(implicit lf: LoggerFactoryGen[F]): lf.LoggerType =
33+
lf.getLoggerFromName(name)
34+
def getLoggerFromClass[F[_]](clazz: Class[_])(implicit lf: LoggerFactoryGen[F]): lf.LoggerType =
35+
lf.getLoggerFromClass(clazz)
36+
def create[F[_]](implicit lf: LoggerFactoryGen[F], name: LoggerName): F[lf.LoggerType] =
37+
lf.create
38+
def fromName[F[_]](name: String)(implicit lf: LoggerFactoryGen[F]): F[lf.LoggerType] =
39+
lf.fromName(name)
40+
def fromClass[F[_]](clazz: Class[_])(implicit lf: LoggerFactoryGen[F]): F[lf.LoggerType] =
41+
lf.fromClass(clazz)
42+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2018 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.log4cats
18+
19+
object LoggerName extends LoggerNameCompat
20+
21+
final case class LoggerName(value: String) extends AnyVal
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2018 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.log4cats
18+
19+
import munit.FunSuite
20+
21+
class LoggerNameTest extends FunSuite {
22+
class FooA {
23+
val name = LoggerName.name
24+
}
25+
26+
object FooB {
27+
val name = LoggerName.name
28+
}
29+
30+
test("names") {
31+
val name1 = new FooA().name
32+
val name2 = FooB.name
33+
34+
assertEquals(name1.value, "org.typelevel.log4cats.LoggerNameTest.FooA")
35+
assertEquals(name2.value, "org.typelevel.log4cats.LoggerNameTest.FooB")
36+
}
37+
}

0 commit comments

Comments
 (0)