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
|
// SPDX-License-Identifier: Apache-2.0
package logger
import java.io.{ByteArrayOutputStream, File, FileOutputStream, PrintStream}
import firrtl.AnnotationSeq
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.
* 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 {
// None indicates "not set"
val Error, Warn, Info, Debug, Trace, None = Value
def default = Warn
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)
def getLogger: Logger = logger
}
/**
* Mutable state of the logging system. Multiple LoggerStates may be present
* when used in multi-threaded environments
*/
private class LoggerState {
var globalLevel = LogLevel.None
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()
}
}
/** 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 = Seq.empty)(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 (getGlobalLevel >= level) {
logIt()
}
}
}
def getGlobalLevel: LogLevel.Value = state.globalLevel match {
// None means "not set" so use default in that case
case LogLevel.None => LogLevel.default
case other => other
}
/**
* 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
}
/** 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
}
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)
|