or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

discovery.mdevents.mdexecution.mdframework.mdindex.mdlogging.md

logging.mddocs/

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

```