Skip to content

Commit a49f0f3

Browse files
authored
Merge pull request #86 from jakubjanecek/StringInterpolation
String Interpolation
2 parents 1e51d75 + 44244a4 commit a49f0f3

File tree

4 files changed

+136
-31
lines changed

4 files changed

+136
-31
lines changed

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ if (logger.isDebugEnabled) logger.debug(s"Some $expensive message!")
2323
A compatible logging backend is [Logback](http://logback.qos.ch), add it to your sbt build definition:
2424

2525
```scala
26-
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.1.7"
26+
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3"
2727
```
2828

2929
If you are looking for a version compatible with Scala 2.10, check out Scala Logging 2.x.
@@ -94,6 +94,28 @@ class MyClass extends LazyLogging {
9494
- SLF4J loggers and our Logger now survive serialization. By survive serialization, we mean that the
9595
deserialized logger instances are fully functional.
9696

97+
## String Interpolation
98+
It is idiomatic to use Scala's string interpolation `logger.error(s"log $value")` instead of SLF4J string interpolation `logger.error("log {}", value)`.
99+
However there are some tools (such as [Sentry](https://sentry.io)) that use the log message format as grouping key. Therefore they do not work well with
100+
Scala's string interpolation.
101+
102+
Scala Logging replaces simple string interpolations with their SLF4J counterparts like this:
103+
104+
```scala
105+
logger.error(s"my log message: $arg1 $arg2 $arg3")
106+
```
107+
108+
```scala
109+
logger.error("my log message: {} {} {}", arg1, arg2, arg3)
110+
```
111+
112+
This has no effect on behavior and performace should be comparable (depends on the underlying logging library).
113+
114+
### Limitations
115+
- Works only when string interpolation is directly used inside the logging statement. That is when the log message is static (available at compile time).
116+
- Works only for the `logger.<level>(message)` and `logger.<level>(marker, message)` logging methods. It does not work if you want to log an exception and
117+
use string interpolation too (this is a limitation of the SLF4J API).
118+
97119
## Line numbers in log message?
98120

99121
Using the [sourcecode](https://github.com/lihaoyi/sourcecode#logging) library, it's possible to add line number

src/main/scala/com/typesafe/scalalogging/LoggerMacro.scala

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ import scala.reflect.macros.blackbox.Context
2121

2222
private object LoggerMacro {
2323

24+
private final val ArgumentMarker = "{}"
25+
2426
type LoggerContext = Context { type PrefixType = Logger }
2527

2628
// Error
2729

2830
def errorMessage(c: LoggerContext)(message: c.Expr[String]) = {
29-
import c.universe._
30-
val underlying = q"${c.prefix}.underlying"
31-
q"if ($underlying.isErrorEnabled) $underlying.error($message)"
31+
val (messageFormat, args) = deconstructInterpolatedMessage(c)(message)
32+
errorMessageArgs(c)(messageFormat, args: _*)
3233
}
3334

3435
def errorMessageCause(c: LoggerContext)(message: c.Expr[String], cause: c.Expr[Throwable]) = {
@@ -47,9 +48,8 @@ private object LoggerMacro {
4748
}
4849

4950
def errorMessageMarker(c: LoggerContext)(marker: c.Expr[Marker], message: c.Expr[String]) = {
50-
import c.universe._
51-
val underlying = q"${c.prefix}.underlying"
52-
q"if ($underlying.isErrorEnabled) $underlying.error($marker, $message)"
51+
val (messageFormat, args) = deconstructInterpolatedMessage(c)(message)
52+
errorMessageArgsMarker(c)(marker, messageFormat, args: _*)
5353
}
5454

5555
def errorMessageCauseMarker(c: LoggerContext)(marker: c.Expr[Marker], message: c.Expr[String], cause: c.Expr[Throwable]) = {
@@ -70,9 +70,8 @@ private object LoggerMacro {
7070
// Warn
7171

7272
def warnMessage(c: LoggerContext)(message: c.Expr[String]) = {
73-
import c.universe._
74-
val underlying = q"${c.prefix}.underlying"
75-
q"if ($underlying.isWarnEnabled) $underlying.warn($message)"
73+
val (messageFormat, args) = deconstructInterpolatedMessage(c)(message)
74+
warnMessageArgs(c)(messageFormat, args: _*)
7675
}
7776

7877
def warnMessageCause(c: LoggerContext)(message: c.Expr[String], cause: c.Expr[Throwable]) = {
@@ -91,9 +90,8 @@ private object LoggerMacro {
9190
}
9291

9392
def warnMessageMarker(c: LoggerContext)(marker: c.Expr[Marker], message: c.Expr[String]) = {
94-
import c.universe._
95-
val underlying = q"${c.prefix}.underlying"
96-
q"if ($underlying.isWarnEnabled) $underlying.warn($marker, $message)"
93+
val (messageFormat, args) = deconstructInterpolatedMessage(c)(message)
94+
warnMessageArgsMarker(c)(marker, messageFormat, args: _*)
9795
}
9896

9997
def warnMessageCauseMarker(c: LoggerContext)(marker: c.Expr[Marker], message: c.Expr[String], cause: c.Expr[Throwable]) = {
@@ -114,9 +112,8 @@ private object LoggerMacro {
114112
// Info
115113

116114
def infoMessage(c: LoggerContext)(message: c.Expr[String]) = {
117-
import c.universe._
118-
val underlying = q"${c.prefix}.underlying"
119-
q"if ($underlying.isInfoEnabled) $underlying.info($message)"
115+
val (messageFormat, args) = deconstructInterpolatedMessage(c)(message)
116+
infoMessageArgs(c)(messageFormat, args: _*)
120117
}
121118

122119
def infoMessageCause(c: LoggerContext)(message: c.Expr[String], cause: c.Expr[Throwable]) = {
@@ -135,9 +132,8 @@ private object LoggerMacro {
135132
}
136133

137134
def infoMessageMarker(c: LoggerContext)(marker: c.Expr[Marker], message: c.Expr[String]) = {
138-
import c.universe._
139-
val underlying = q"${c.prefix}.underlying"
140-
q"if ($underlying.isInfoEnabled) $underlying.info($marker, $message)"
135+
val (messageFormat, args) = deconstructInterpolatedMessage(c)(message)
136+
infoMessageArgsMarker(c)(marker, messageFormat, args: _*)
141137
}
142138

143139
def infoMessageCauseMarker(c: LoggerContext)(marker: c.Expr[Marker], message: c.Expr[String], cause: c.Expr[Throwable]) = {
@@ -158,9 +154,8 @@ private object LoggerMacro {
158154
// Debug
159155

160156
def debugMessage(c: LoggerContext)(message: c.Expr[String]) = {
161-
import c.universe._
162-
val underlying = q"${c.prefix}.underlying"
163-
q"if ($underlying.isDebugEnabled) $underlying.debug($message)"
157+
val (messageFormat, args) = deconstructInterpolatedMessage(c)(message)
158+
debugMessageArgs(c)(messageFormat, args: _*)
164159
}
165160

166161
def debugMessageCause(c: LoggerContext)(message: c.Expr[String], cause: c.Expr[Throwable]) = {
@@ -179,9 +174,8 @@ private object LoggerMacro {
179174
}
180175

181176
def debugMessageMarker(c: LoggerContext)(marker: c.Expr[Marker], message: c.Expr[String]) = {
182-
import c.universe._
183-
val underlying = q"${c.prefix}.underlying"
184-
q"if ($underlying.isDebugEnabled) $underlying.debug($marker, $message)"
177+
val (messageFormat, args) = deconstructInterpolatedMessage(c)(message)
178+
debugMessageArgsMarker(c)(marker, messageFormat, args: _*)
185179
}
186180

187181
def debugMessageCauseMarker(c: LoggerContext)(marker: c.Expr[Marker], message: c.Expr[String], cause: c.Expr[Throwable]) = {
@@ -202,9 +196,8 @@ private object LoggerMacro {
202196
// Trace
203197

204198
def traceMessage(c: LoggerContext)(message: c.Expr[String]) = {
205-
import c.universe._
206-
val underlying = q"${c.prefix}.underlying"
207-
q"if ($underlying.isTraceEnabled) $underlying.trace($message)"
199+
val (messageFormat, args) = deconstructInterpolatedMessage(c)(message)
200+
traceMessageArgs(c)(messageFormat, args: _*)
208201
}
209202

210203
def traceMessageCause(c: LoggerContext)(message: c.Expr[String], cause: c.Expr[Throwable]) = {
@@ -223,9 +216,8 @@ private object LoggerMacro {
223216
}
224217

225218
def traceMessageMarker(c: LoggerContext)(marker: c.Expr[Marker], message: c.Expr[String]) = {
226-
import c.universe._
227-
val underlying = q"${c.prefix}.underlying"
228-
q"if ($underlying.isTraceEnabled) $underlying.trace($marker, $message)"
219+
val (messageFormat, args) = deconstructInterpolatedMessage(c)(message)
220+
traceMessageArgsMarker(c)(marker, messageFormat, args: _*)
229221
}
230222

231223
def traceMessageCauseMarker(c: LoggerContext)(marker: c.Expr[Marker], message: c.Expr[String], cause: c.Expr[Throwable]) = {
@@ -243,4 +235,18 @@ private object LoggerMacro {
243235
q"if ($underlying.isTraceEnabled) $underlying.trace($marker, $message, ..$args)"
244236
}
245237

238+
/** Checks whether `messsage` is an interpolated string and transforms it into SLF4J string interpolation. */
239+
private def deconstructInterpolatedMessage(c: LoggerContext)(message: c.Expr[String]) = {
240+
import c.universe._
241+
242+
message.tree match {
243+
case q"scala.StringContext.apply(..$parts).s(..$args)" =>
244+
val strings = parts.collect { case Literal(Constant(s: String)) => s }
245+
val messageFormat = strings.mkString(ArgumentMarker)
246+
(c.Expr(q"$messageFormat"), args.map(arg => q"$arg").map(c.Expr[Any](_)))
247+
248+
case _ => (message, Seq.empty)
249+
}
250+
}
251+
246252
}

src/test/scala/com/typesafe/scalalogging/LoggerSpec.scala

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ class LoggerSpec extends WordSpec with Matchers with MockitoSugar {
4343
logger.error(msg)
4444
verify(underlying, never).error(anyString)
4545
}
46+
47+
"call the underlying logger's error method with arguments if the error level is enabled and string is interpolated" in {
48+
val f = fixture(_.isErrorEnabled, true)
49+
import f._
50+
logger.error(s"msg $arg1 $arg2 $arg3")
51+
verify(underlying).error("msg {} {} {}", arg1, arg2, arg3)
52+
}
53+
54+
"call the underlying logger's error method with two arguments if the error level is enabled and string is interpolated" in {
55+
val f = fixture(_.isErrorEnabled, true)
56+
import f._
57+
logger.error(s"msg $arg1 $arg2")
58+
verify(underlying).error("msg {} {}", List(arg1, arg2): _*)
59+
}
4660
}
4761

4862
"Calling error with a message and cause" should {
@@ -104,6 +118,13 @@ class LoggerSpec extends WordSpec with Matchers with MockitoSugar {
104118
logger.warn(msg)
105119
verify(underlying, never).warn(anyString)
106120
}
121+
122+
"call the underlying logger's warn method if the warn level is enabled and string is interpolated" in {
123+
val f = fixture(_.isWarnEnabled, true)
124+
import f._
125+
logger.warn(s"msg $arg1 $arg2 $arg3")
126+
verify(underlying).warn("msg {} {} {}", arg1, arg2, arg3)
127+
}
107128
}
108129

109130
"Calling warn with a message and cause" should {
@@ -165,6 +186,13 @@ class LoggerSpec extends WordSpec with Matchers with MockitoSugar {
165186
logger.info(msg)
166187
verify(underlying, never).info(anyString)
167188
}
189+
190+
"call the underlying logger's info method if the info level is enabled and string is interpolated" in {
191+
val f = fixture(_.isInfoEnabled, true)
192+
import f._
193+
logger.info(s"msg $arg1 $arg2 $arg3")
194+
verify(underlying).info("msg {} {} {}", arg1, arg2, arg3)
195+
}
168196
}
169197

170198
"Calling info with a message and cause" should {
@@ -226,6 +254,13 @@ class LoggerSpec extends WordSpec with Matchers with MockitoSugar {
226254
logger.debug(msg)
227255
verify(underlying, never).debug(anyString)
228256
}
257+
258+
"call the underlying logger's debug method if the debug level is enabled and string is interpolated" in {
259+
val f = fixture(_.isDebugEnabled, true)
260+
import f._
261+
logger.debug(s"msg $arg1 $arg2 $arg3")
262+
verify(underlying).debug("msg {} {} {}", arg1, arg2, arg3)
263+
}
229264
}
230265

231266
"Calling debug with a message and cause" should {
@@ -287,6 +322,13 @@ class LoggerSpec extends WordSpec with Matchers with MockitoSugar {
287322
logger.trace(msg)
288323
verify(underlying, never).trace(anyString)
289324
}
325+
326+
"call the underlying logger's trace method if the trace level is enabled and string is interpolated" in {
327+
val f = fixture(_.isTraceEnabled, true)
328+
import f._
329+
logger.trace(s"msg $arg1 $arg2 $arg3")
330+
verify(underlying).trace("msg {} {} {}", arg1, arg2, arg3)
331+
}
290332
}
291333

292334
"Calling trace with a message and cause" should {

src/test/scala/com/typesafe/scalalogging/LoggerWithMarkerSpec.scala

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ class LoggerWithMarkerSpec extends WordSpec with Matchers with MockitoSugar {
5858
logger.error(marker, msg)
5959
verify(underlying, never).error(refEq(DummyMarker), anyString)
6060
}
61+
62+
"call the underlying logger's error method if the error level is enabled and string is interpolated" in {
63+
val f = fixture(_.isErrorEnabled, true)
64+
import f._
65+
logger.error(marker, s"msg $arg1 $arg2 $arg3")
66+
verify(underlying).error(marker, "msg {} {} {}", arg1, arg2, arg3)
67+
}
6168
}
6269

6370
"Calling error with a marker and a message and cause" should {
@@ -119,6 +126,13 @@ class LoggerWithMarkerSpec extends WordSpec with Matchers with MockitoSugar {
119126
logger.warn(marker, msg)
120127
verify(underlying, never).warn(refEq(DummyMarker), anyString)
121128
}
129+
130+
"call the underlying logger's warn method if the warn level is enabled and string is interpolated" in {
131+
val f = fixture(_.isWarnEnabled, true)
132+
import f._
133+
logger.warn(marker, s"msg $arg1 $arg2 $arg3")
134+
verify(underlying).warn(marker, "msg {} {} {}", arg1, arg2, arg3)
135+
}
122136
}
123137

124138
"Calling warn with a marker and a message and cause" should {
@@ -180,6 +194,13 @@ class LoggerWithMarkerSpec extends WordSpec with Matchers with MockitoSugar {
180194
logger.info(marker, msg)
181195
verify(underlying, never).info(refEq(DummyMarker), anyString)
182196
}
197+
198+
"call the underlying logger's info method if the info level is enabled and string is interpolated" in {
199+
val f = fixture(_.isInfoEnabled, true)
200+
import f._
201+
logger.info(marker, s"msg $arg1 $arg2 $arg3")
202+
verify(underlying).info(marker, "msg {} {} {}", arg1, arg2, arg3)
203+
}
183204
}
184205

185206
"Calling info with a marker and a message and cause" should {
@@ -241,6 +262,13 @@ class LoggerWithMarkerSpec extends WordSpec with Matchers with MockitoSugar {
241262
logger.debug(marker, msg)
242263
verify(underlying, never).debug(refEq(DummyMarker), anyString)
243264
}
265+
266+
"call the underlying logger's debug method if the debug level is enabled and string is interpolated" in {
267+
val f = fixture(_.isDebugEnabled, true)
268+
import f._
269+
logger.debug(marker, s"msg $arg1 $arg2 $arg3")
270+
verify(underlying).debug(marker, "msg {} {} {}", arg1, arg2, arg3)
271+
}
244272
}
245273

246274
"Calling debug with a marker and a message and cause" should {
@@ -302,6 +330,13 @@ class LoggerWithMarkerSpec extends WordSpec with Matchers with MockitoSugar {
302330
logger.trace(marker, msg)
303331
verify(underlying, never).trace(refEq(DummyMarker), anyString)
304332
}
333+
334+
"call the underlying logger's trace method if the trace level is enabled and string is interpolated" in {
335+
val f = fixture(_.isTraceEnabled, true)
336+
import f._
337+
logger.trace(marker, s"msg $arg1 $arg2 $arg3")
338+
verify(underlying).trace(marker, "msg {} {} {}", arg1, arg2, arg3)
339+
}
305340
}
306341

307342
"Calling trace with a marker and a message and cause" should {

0 commit comments

Comments
 (0)