0
# Logging System
1
2
Thread-safe logging interface supporting multiple log levels and ANSI color codes for user-facing messages during test execution.
3
4
## Capabilities
5
6
### Logger Interface
7
8
Thread-safe logging interface for providing feedback to users during test runs.
9
10
```scala { .api }
11
/**
12
* A logger through which to provide feedback to the user about a run.
13
* The difference between event handler and logger:
14
* - EventHandler: for events consumed by client software
15
* - Logger: for messages consumed by client user (humans)
16
*
17
* Implementations of this interface must be thread-safe.
18
*/
19
trait Logger {
20
/**
21
* True if ANSI color codes are understood by this instance.
22
* @return whether ANSI color codes are supported
23
*/
24
def ansiCodesSupported(): Boolean
25
26
/**
27
* Provide an error message.
28
* @param msg the error message
29
*/
30
def error(msg: String): Unit
31
32
/**
33
* Provide a warning message.
34
* @param msg the warning message
35
*/
36
def warn(msg: String): Unit
37
38
/**
39
* Provide an info message.
40
* @param msg the info message
41
*/
42
def info(msg: String): Unit
43
44
/**
45
* Provide a debug message.
46
* @param msg the debug message
47
*/
48
def debug(msg: String): Unit
49
50
/**
51
* Provide a stack trace.
52
* @param t the Throwable containing the stack trace being logged
53
*/
54
def trace(t: Throwable): Unit
55
}
56
```
57
58
**Usage Examples:**
59
60
```scala
61
// Basic logger implementation
62
class ConsoleLogger(colorSupport: Boolean = true) extends Logger {
63
def ansiCodesSupported(): Boolean = colorSupport
64
65
def error(msg: String): Unit = {
66
val colored = if (ansiCodesSupported()) s"\u001B[31m[ERROR]\u001B[0m $msg" else s"[ERROR] $msg"
67
System.err.println(colored)
68
}
69
70
def warn(msg: String): Unit = {
71
val colored = if (ansiCodesSupported()) s"\u001B[33m[WARN]\u001B[0m $msg" else s"[WARN] $msg"
72
System.err.println(colored)
73
}
74
75
def info(msg: String): Unit = {
76
val colored = if (ansiCodesSupported()) s"\u001B[32m[INFO]\u001B[0m $msg" else s"[INFO] $msg"
77
println(colored)
78
}
79
80
def debug(msg: String): Unit = {
81
val colored = if (ansiCodesSupported()) s"\u001B[36m[DEBUG]\u001B[0m $msg" else s"[DEBUG] $msg"
82
println(colored)
83
}
84
85
def trace(t: Throwable): Unit = {
86
if (ansiCodesSupported()) {
87
System.err.println(s"\u001B[31m${t.getClass.getSimpleName}: ${t.getMessage}\u001B[0m")
88
} else {
89
System.err.println(s"${t.getClass.getSimpleName}: ${t.getMessage}")
90
}
91
t.printStackTrace()
92
}
93
}
94
95
// Usage in test tasks
96
class MyTask(taskDef: TaskDef) extends Task {
97
def execute(eventHandler: EventHandler, loggers: Array[Logger]): Array[Task] = {
98
val logger = loggers.headOption.getOrElse(new NoOpLogger())
99
100
logger.info(s"Starting test suite: ${taskDef.fullyQualifiedName()}")
101
102
try {
103
val testClass = loadTestClass(taskDef.fullyQualifiedName())
104
logger.debug(s"Loaded test class: ${testClass.getName}")
105
106
val results = runTests(testClass)
107
logger.info(s"Completed ${results.size} tests")
108
109
results.foreach { result =>
110
result match {
111
case Success(testName) =>
112
logger.info(s"✓ $testName")
113
case Failure(testName, cause) =>
114
logger.error(s"✗ $testName: ${cause.getMessage}")
115
case Error(testName, cause) =>
116
logger.error(s"⚠ $testName - ERROR")
117
logger.trace(cause)
118
}
119
}
120
121
Array.empty
122
123
} catch {
124
case t: Throwable =>
125
logger.error(s"Failed to execute test suite: ${t.getMessage}")
126
logger.trace(t)
127
Array.empty
128
}
129
}
130
}
131
```
132
133
### Logger Implementations
134
135
**No-Op Logger:**
136
```scala
137
class NoOpLogger extends Logger {
138
def ansiCodesSupported(): Boolean = false
139
def error(msg: String): Unit = {}
140
def warn(msg: String): Unit = {}
141
def info(msg: String): Unit = {}
142
def debug(msg: String): Unit = {}
143
def trace(t: Throwable): Unit = {}
144
}
145
```
146
147
**File Logger:**
148
```scala
149
import java.io.{FileWriter, PrintWriter}
150
import java.time.LocalDateTime
151
152
class FileLogger(filePath: String) extends Logger with AutoCloseable {
153
private val writer = new PrintWriter(new FileWriter(filePath, true))
154
155
def ansiCodesSupported(): Boolean = false // Files don't support ANSI
156
157
private def writeWithTimestamp(level: String, msg: String): Unit = {
158
val timestamp = LocalDateTime.now().toString
159
writer.println(s"$timestamp [$level] $msg")
160
writer.flush()
161
}
162
163
def error(msg: String): Unit = writeWithTimestamp("ERROR", msg)
164
def warn(msg: String): Unit = writeWithTimestamp("WARN", msg)
165
def info(msg: String): Unit = writeWithTimestamp("INFO", msg)
166
def debug(msg: String): Unit = writeWithTimestamp("DEBUG", msg)
167
168
def trace(t: Throwable): Unit = {
169
writeWithTimestamp("ERROR", s"${t.getClass.getSimpleName}: ${t.getMessage}")
170
t.printStackTrace(writer)
171
writer.flush()
172
}
173
174
def close(): Unit = writer.close()
175
}
176
```
177
178
**Buffered Logger:**
179
```scala
180
import scala.collection.mutable
181
182
class BufferedLogger extends Logger {
183
private val buffer = mutable.ListBuffer[LogEntry]()
184
185
sealed trait LogLevel
186
case object ErrorLevel extends LogLevel
187
case object WarnLevel extends LogLevel
188
case object InfoLevel extends LogLevel
189
case object DebugLevel extends LogLevel
190
case object TraceLevel extends LogLevel
191
192
case class LogEntry(level: LogLevel, message: String, timestamp: Long = System.currentTimeMillis())
193
194
def ansiCodesSupported(): Boolean = false
195
196
def error(msg: String): Unit = synchronized {
197
buffer += LogEntry(ErrorLevel, msg)
198
}
199
200
def warn(msg: String): Unit = synchronized {
201
buffer += LogEntry(WarnLevel, msg)
202
}
203
204
def info(msg: String): Unit = synchronized {
205
buffer += LogEntry(InfoLevel, msg)
206
}
207
208
def debug(msg: String): Unit = synchronized {
209
buffer += LogEntry(DebugLevel, msg)
210
}
211
212
def trace(t: Throwable): Unit = synchronized {
213
buffer += LogEntry(TraceLevel, s"${t.getClass.getSimpleName}: ${t.getMessage}\n${t.getStackTrace.mkString("\n")}")
214
}
215
216
def getEntries(): List[LogEntry] = synchronized {
217
buffer.toList
218
}
219
220
def clear(): Unit = synchronized {
221
buffer.clear()
222
}
223
224
def flushTo(target: Logger): Unit = synchronized {
225
buffer.foreach { entry =>
226
entry.level match {
227
case ErrorLevel => target.error(entry.message)
228
case WarnLevel => target.warn(entry.message)
229
case InfoLevel => target.info(entry.message)
230
case DebugLevel => target.debug(entry.message)
231
case TraceLevel =>
232
// Parse back the throwable info for trace
233
val lines = entry.message.split("\n")
234
target.error(lines.head)
235
// Can't recreate full throwable, just log the stack trace
236
lines.tail.foreach(line => target.error(s" $line"))
237
}
238
}
239
}
240
}
241
```
242
243
### Thread Safety
244
245
Since Logger implementations must be thread-safe, here are patterns for concurrent access:
246
247
```scala
248
class ThreadSafeConsoleLogger extends Logger {
249
private val lock = new Object()
250
251
def ansiCodesSupported(): Boolean = true
252
253
def error(msg: String): Unit = lock.synchronized {
254
System.err.println(s"\u001B[31m[ERROR]\u001B[0m $msg")
255
}
256
257
def warn(msg: String): Unit = lock.synchronized {
258
System.err.println(s"\u001B[33m[WARN]\u001B[0m $msg")
259
}
260
261
def info(msg: String): Unit = lock.synchronized {
262
println(s"\u001B[32m[INFO]\u001B[0m $msg")
263
}
264
265
def debug(msg: String): Unit = lock.synchronized {
266
println(s"\u001B[36m[DEBUG]\u001B[0m $msg")
267
}
268
269
def trace(t: Throwable): Unit = lock.synchronized {
270
System.err.println(s"\u001B[31m${t.getClass.getSimpleName}: ${t.getMessage}\u001B[0m")
271
t.printStackTrace()
272
}
273
}
274
```
275
276
### Logger Composition
277
278
Combine multiple loggers for different outputs:
279
280
```scala
281
class CompositeLogger(loggers: Logger*) extends Logger {
282
def ansiCodesSupported(): Boolean = loggers.exists(_.ansiCodesSupported())
283
284
def error(msg: String): Unit = loggers.foreach(_.error(msg))
285
def warn(msg: String): Unit = loggers.foreach(_.warn(msg))
286
def info(msg: String): Unit = loggers.foreach(_.info(msg))
287
def debug(msg: String): Unit = loggers.foreach(_.debug(msg))
288
def trace(t: Throwable): Unit = loggers.foreach(_.trace(t))
289
}
290
291
// Usage: log to both console and file
292
val compositeLogger = new CompositeLogger(
293
new ConsoleLogger(),
294
new FileLogger("test-results.log")
295
)
296
```
297
298
### Logger Filtering
299
300
Filter logs by level:
301
302
```scala
303
class FilteringLogger(underlying: Logger, minLevel: LogLevel) extends Logger {
304
sealed trait LogLevel { def priority: Int }
305
case object Debug extends LogLevel { val priority = 0 }
306
case object Info extends LogLevel { val priority = 1 }
307
case object Warn extends LogLevel { val priority = 2 }
308
case object Error extends LogLevel { val priority = 3 }
309
310
def ansiCodesSupported(): Boolean = underlying.ansiCodesSupported()
311
312
private def shouldLog(level: LogLevel): Boolean = level.priority >= minLevel.priority
313
314
def error(msg: String): Unit = if (shouldLog(Error)) underlying.error(msg)
315
def warn(msg: String): Unit = if (shouldLog(Warn)) underlying.warn(msg)
316
def info(msg: String): Unit = if (shouldLog(Info)) underlying.info(msg)
317
def debug(msg: String): Unit = if (shouldLog(Debug)) underlying.debug(msg)
318
def trace(t: Throwable): Unit = if (shouldLog(Error)) underlying.trace(t)
319
}
320
321
// Usage: only log warnings and errors
322
val filteredLogger = new FilteringLogger(new ConsoleLogger(), Warn)
323
```
324
325
## ANSI Color Codes
326
327
When `ansiCodesSupported()` returns true, use ANSI escape codes for colored output:
328
329
```scala
330
object AnsiColors {
331
val Reset = "\u001B[0m"
332
val Red = "\u001B[31m" // Error
333
val Yellow = "\u001B[33m" // Warning
334
val Green = "\u001B[32m" // Info/Success
335
val Cyan = "\u001B[36m" // Debug
336
val Blue = "\u001B[34m" // Info alternative
337
val Magenta = "\u001B[35m" // Special cases
338
339
val Bold = "\u001B[1m"
340
val Underline = "\u001B[4m"
341
}
342
343
class ColorfulLogger extends Logger {
344
def ansiCodesSupported(): Boolean = true
345
346
def error(msg: String): Unit = {
347
println(s"${AnsiColors.Red}${AnsiColors.Bold}[ERROR]${AnsiColors.Reset} ${AnsiColors.Red}$msg${AnsiColors.Reset}")
348
}
349
350
def warn(msg: String): Unit = {
351
println(s"${AnsiColors.Yellow}[WARN]${AnsiColors.Reset} $msg")
352
}
353
354
def info(msg: String): Unit = {
355
println(s"${AnsiColors.Green}[INFO]${AnsiColors.Reset} $msg")
356
}
357
358
def debug(msg: String): Unit = {
359
println(s"${AnsiColors.Cyan}[DEBUG]${AnsiColors.Reset} $msg")
360
}
361
362
def trace(t: Throwable): Unit = {
363
println(s"${AnsiColors.Red}${AnsiColors.Underline}${t.getClass.getSimpleName}${AnsiColors.Reset}${AnsiColors.Red}: ${t.getMessage}${AnsiColors.Reset}")
364
t.printStackTrace()
365
}
366
}
367
```
368
369
## Usage Patterns
370
371
### Test Framework Integration
372
373
Frameworks receive logger arrays and should use them for user communication:
374
375
```scala
376
class MyTestFramework extends Framework {
377
def runner(args: Array[String], remoteArgs: Array[String],
378
testClassLoader: ClassLoader): Runner = {
379
new MyRunner(args, remoteArgs, testClassLoader)
380
}
381
}
382
383
class MyRunner extends Runner {
384
def tasks(taskDefs: Array[TaskDef]): Array[Task] = {
385
taskDefs.map(taskDef => new MyTask(taskDef))
386
}
387
}
388
389
class MyTask(taskDef: TaskDef) extends Task {
390
def execute(eventHandler: EventHandler, loggers: Array[Logger]): Array[Task] = {
391
// Use first available logger, fallback to no-op
392
val logger = loggers.headOption.getOrElse(new NoOpLogger())
393
394
logger.info(s"Executing: ${taskDef.fullyQualifiedName()}")
395
396
// Execute test logic with logging
397
executeWithLogging(logger, eventHandler)
398
}
399
}
400
```
401
402
### Best Practices
403
404
**Performance:**
405
- Check log level before expensive string operations
406
- Use lazy evaluation for debug messages
407
- Batch multiple related log messages when possible
408
409
**User Experience:**
410
- Provide meaningful, actionable messages
411
- Use appropriate log levels (error for failures, info for progress)
412
- Include relevant context (test names, timing, counts)
413
- Format stack traces clearly
414
415
**Thread Safety:**
416
- Always implement thread-safe logging
417
- Use proper synchronization or thread-safe data structures
418
- Consider performance impact of synchronization
419
420
```scala
421
// Good: lazy evaluation and appropriate levels
422
def executeTest(testName: String, logger: Logger): Unit = {
423
logger.info(s"Starting test: $testName")
424
425
val startTime = System.currentTimeMillis()
426
try {
427
// Test execution
428
val result = runTest(testName)
429
val duration = System.currentTimeMillis() - startTime
430
431
logger.debug(s"Test $testName completed in ${duration}ms")
432
logger.info(s"✓ $testName")
433
434
} catch {
435
case t: Throwable =>
436
logger.error(s"✗ $testName failed: ${t.getMessage}")
437
logger.trace(t)
438
}
439
}
440
```