1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
|
// SPDX-License-Identifier: Apache-2.0
package logger
import java.io.{ByteArrayOutputStream, File, FileOutputStream, PrintStream}
import firrtl.{AnnotationSeq, ExecutionOptionsManager}
import firrtl.options.Viewer.view
import logger.phases.{AddDefaults, Checks}
import scala.util.DynamicVariable
/**
* This provides a facility for a log4scala* type logging system. Why did we write our own? Because
* the canned ones are just too darned hard to turn on, particularly when embedded in a distribution.
* This one can be turned on programmatically or with the options exposed in the [[firrtl.CommonOptions]]
* and [[ExecutionOptionsManager]] APIs in firrtl.
* There are 4 main options:
* * A simple global option to turn on all in scope (and across threads, might want to fix this)
* * Turn on specific levels for specific fully qualified class names
* * Set a file to write things to, default is just to use stdout
* * Include the class names and level in the output. This is useful for figuring out what
* the class names that extend LazyLogger are.
*
* This is not overly optimized but does pass the string as () => String to avoid string interpolation
* occurring if the the logging level is not sufficiently high. This could be further optimized by playing
* with methods.
*/
/**
* The supported log levels, what do they mean? Whatever you want them to.
*/
object LogLevel extends Enumeration {
val Error, Warn, Info, Debug, Trace, None = Value
def apply(s: String): LogLevel.Value = s.toLowerCase match {
case "error" => LogLevel.Error
case "warn" => LogLevel.Warn
case "info" => LogLevel.Info
case "debug" => LogLevel.Debug
case "trace" => LogLevel.Trace
case level => throw new Exception(s"Unknown LogLevel '$level'")
}
}
/**
* extend this trait to enable logging in a class you are implementing
*/
trait LazyLogging {
protected val logger = new Logger(this.getClass.getName)
}
/**
* Mutable state of the logging system. Multiple LoggerStates may be present
* when used in multi-threaded environments
*/
private class LoggerState {
var globalLevel = LogLevel.Warn
val classLevels = new scala.collection.mutable.HashMap[String, LogLevel.Value]
val classToLevelCache = new scala.collection.mutable.HashMap[String, LogLevel.Value]
var logClassNames = false
var stream: PrintStream = Console.out
var fromInvoke: Boolean = false // this is used to not have invokes re-create run-state
var stringBufferOption: Option[Logger.OutputCaptor] = None
override def toString: String = {
s"gl $globalLevel classLevels ${classLevels.mkString("\n")}"
}
/**
* create a new state object copying the basic values of this state
* @return new state object
*/
def copy: LoggerState = {
val newState = new LoggerState
newState.globalLevel = this.globalLevel
newState.classLevels ++= this.classLevels
newState.stream = this.stream
newState.logClassNames = this.logClassNames
newState
}
}
/**
* Singleton in control of what is supposed to get logged, how it's to be logged and where it is to be logged
* We uses a dynamic variable in case multiple threads are used as can be in scalatests
*/
object Logger {
private val updatableLoggerState = new DynamicVariable[Option[LoggerState]](Some(new LoggerState))
private def state: LoggerState = {
updatableLoggerState.value.get
}
/**
* a class for managing capturing logging output in a string buffer
*/
class OutputCaptor {
val byteArrayOutputStream = new ByteArrayOutputStream()
val printStream = new PrintStream(byteArrayOutputStream)
/**
* Get logged messages to this captor as a string
* @return
*/
def getOutputAsString: String = {
byteArrayOutputStream.toString
}
/**
* Clear the string buffer
*/
def clear(): Unit = {
byteArrayOutputStream.reset()
}
}
/**
* This creates a block of code that will have access to the
* thread specific logger. The state will be set according to the
* logging options set in the common options of the manager
* @param manager source of logger settings
* @param codeBlock code to be run with these logger settings
* @tparam A The return type of codeBlock
* @return Whatever block returns
*/
@deprecated("Use makeScope(opts: FirrtlOptions)", "FIRRTL 1.2")
def makeScope[A](manager: ExecutionOptionsManager)(codeBlock: => A): A =
makeScope(manager.commonOptions.toAnnotations)(codeBlock)
/**
* See makeScope using manager. This creates a manager from a command line arguments style
* list of strings
* @param args List of strings
* @param codeBlock the block to call
* @tparam A return type of codeBlock
* @return
*/
@deprecated("Use makescope(opts: FirrtlOptions)", "FIRRTL 1.2")
def makeScope[A](args: Array[String] = Array.empty)(codeBlock: => A): A = {
val executionOptionsManager = new ExecutionOptionsManager("logger")
if (executionOptionsManager.parse(args)) {
makeScope(executionOptionsManager)(codeBlock)
} else {
throw new Exception(s"logger invoke failed to parse args ${args.mkString(", ")}")
}
}
/** Set a scope for this logger based on available annotations
* @param options a sequence annotations
* @param codeBlock some Scala code over which to define this scope
* @tparam A return type of the code block
* @return the original return of the code block
*/
def makeScope[A](options: AnnotationSeq)(codeBlock: => A): A = {
val runState: LoggerState = {
val newRunState = updatableLoggerState.value.getOrElse(new LoggerState)
if (newRunState.fromInvoke) {
newRunState
} else {
val forcedNewRunState = new LoggerState
forcedNewRunState.fromInvoke = true
forcedNewRunState
}
}
updatableLoggerState.withValue(Some(runState)) {
setOptions(options)
codeBlock
}
}
/**
* Used to test whether a given log statement should generate some logging output.
* It breaks up a class name into a list of packages. From this list generate progressively
* broader names (lopping off from right side) checking for a match
* @param className class name that the logging statement came from
* @param level the level of the log statement being evaluated
* @return
*/
private def testPackageNameMatch(className: String, level: LogLevel.Value): Option[Boolean] = {
val classLevels = state.classLevels
if (classLevels.isEmpty) return None
// If this class name in cache just use that value
val levelForThisClassName = state.classToLevelCache.getOrElse(
className, {
// otherwise break up the class name in to full package path as list and find most specific entry you can
val packageNameList = className.split("""\.""").toList
/*
* start with full class path, lopping off from the tail until nothing left
*/
def matchPathToFindLevel(packageList: List[String]): LogLevel.Value = {
if (packageList.isEmpty) {
LogLevel.None
} else {
val partialName = packageList.mkString(".")
val level = classLevels.getOrElse(
partialName, {
matchPathToFindLevel(packageList.reverse.tail.reverse)
}
)
level
}
}
val levelSpecified = matchPathToFindLevel(packageNameList)
if (levelSpecified != LogLevel.None) {
state.classToLevelCache(className) = levelSpecified
}
levelSpecified
}
)
if (levelForThisClassName != LogLevel.None) {
Some(levelForThisClassName >= level)
} else {
None
}
}
/**
* Used as the common log routine, for warn, debug etc. Only calls message if log should be generated
* Allows lazy evaluation of any string interpolation or function that generates the message itself
* @note package level supercedes global, which allows one to turn on debug everywhere except for specific classes
* @param level level of the called statement
* @param className class name of statement
* @param message a function returning a string with the message
*/
private def showMessage(level: LogLevel.Value, className: String, message: => String): Unit = {
def logIt(): Unit = {
if (state.logClassNames) {
state.stream.println(s"[$level:$className] $message")
} else {
state.stream.println(message)
}
}
testPackageNameMatch(className, level) match {
case Some(true) => logIt()
case Some(false) =>
case None =>
if (
(state.globalLevel == LogLevel.None && level == LogLevel.Error) ||
(state.globalLevel != LogLevel.None && state.globalLevel >= level)
) {
logIt()
}
}
}
def getGlobalLevel: LogLevel.Value = {
state.globalLevel
}
/**
* This resets everything in the current Logger environment, including the destination
* use this with caution. Unexpected things can happen
*/
def reset(): Unit = {
state.classLevels.clear()
clearCache()
state.logClassNames = false
state.globalLevel = LogLevel.Error
state.stream = System.out
}
/**
* clears the cache of class names top class specific log levels
*/
private def clearCache(): Unit = {
state.classToLevelCache.clear()
}
/**
* This sets the global logging level
* @param level The desired global logging level
*/
def setLevel(level: LogLevel.Value): Unit = {
state.globalLevel = level
}
/**
* This sets the logging level for a particular class or package
* The package name must be general to specific. I.e.
* package1.package2.class
* package1.package2
* package1
* Will work.
* package2.class will not work if package2 is within package1
* @param classOrPackageName The string based class name or
* @param level The desired global logging level
*/
def setLevel(classOrPackageName: String, level: LogLevel.Value): Unit = {
clearCache()
state.classLevels(classOrPackageName) = level
}
/**
* Set the log level based on a class type
* @example {{{ setLevel(classOf[SomeClass], LogLevel.Debug) }}}
* @param classType Kind of class
* @param level log level to set
*/
def setLevel(classType: Class[_ <: LazyLogging], level: LogLevel.Value): Unit = {
clearCache()
val name = classType.getCanonicalName
state.classLevels(name) = level
}
/**
* Clears the logging data in the string capture buffer if it exists
* @return The logging data if it exists
*/
def clearStringBuffer(): Unit = {
state.stringBufferOption match {
case Some(x) => x.byteArrayOutputStream.reset()
case None =>
}
}
/**
* Set the logging destination to a file name
* @param fileName destination name
*/
def setOutput(fileName: String): Unit = {
state.stream = new PrintStream(new FileOutputStream(new File(fileName)))
}
/**
* Set the logging destination to a print stream
* @param stream destination stream
*/
def setOutput(stream: PrintStream): Unit = {
state.stream = stream
}
/**
* Sets the logging destination to Console.out
*/
def setConsole(): Unit = {
state.stream = Console.out
}
/**
* Adds a list of of className, loglevel tuples to the global (dynamicVar)
* See testPackageNameMatch for a description of how class name matching works
* @param namesToLevel a list of tuples (class name, log level)
*/
def setClassLogLevels(namesToLevel: Map[String, LogLevel.Value]): Unit = {
clearCache()
state.classLevels ++= namesToLevel
}
/**
* This is used to set the options that have been set in a optionsManager or are coming
* from the command line via an options manager
* @param optionsManager manager
*/
@deprecated("Use setOptions(annotations: AnnotationSeq)", "FIRRTL 1.2")
def setOptions(optionsManager: ExecutionOptionsManager): Unit =
setOptions(optionsManager.commonOptions.toAnnotations)
/** Set logger options based on the content of an [[firrtl.AnnotationSeq AnnotationSeq]]
* @param inputAnnotations annotation sequence containing logger options
*/
def setOptions(inputAnnotations: AnnotationSeq): Unit = {
val annotations =
Seq(new AddDefaults, Checks)
.foldLeft(inputAnnotations)((a, p) => p.transform(a))
val lopts = view[LoggerOptions](annotations)
state.globalLevel = (state.globalLevel, lopts.globalLogLevel) match {
case (LogLevel.None, LogLevel.None) => LogLevel.None
case (x, LogLevel.None) => x
case (LogLevel.None, x) => x
case (_, x) => x
case _ => LogLevel.Error
}
setClassLogLevels(lopts.classLogLevels)
if (lopts.logFileName.nonEmpty) {
setOutput(lopts.logFileName.get)
}
state.logClassNames = lopts.logClassNames
}
}
/**
* Classes implementing [[LazyLogging]] will have logger of this type
* @param containerClass passed in from the LazyLogging trait in order to provide class level logging granularity
*/
class Logger(containerClass: String) {
/**
* Log message at Error level
* @param message message generator to be invoked if level is right
*/
def error(message: => String): Unit = {
Logger.showMessage(LogLevel.Error, containerClass, message)
}
/**
* Log message at Warn level
* @param message message generator to be invoked if level is right
*/
def warn(message: => String): Unit = {
Logger.showMessage(LogLevel.Warn, containerClass, message)
}
/**
* Log message at Inof level
* @param message message generator to be invoked if level is right
*/
def info(message: => String): Unit = {
Logger.showMessage(LogLevel.Info, containerClass, message)
}
/**
* Log message at Debug level
* @param message message generator to be invoked if level is right
*/
def debug(message: => String): Unit = {
Logger.showMessage(LogLevel.Debug, containerClass, message)
}
/**
* Log message at Trace level
* @param message message generator to be invoked if level is right
*/
def trace(message: => String): Unit = {
Logger.showMessage(LogLevel.Trace, containerClass, message)
}
}
/** An exception originating from the Logger
* @param str an exception message
* @param cause a reason for the exception
*/
class LoggerException(val str: String, cause: Throwable = null) extends RuntimeException(str, cause)
|