├── README.md ├── myProgram.nim ├── someModule ├── loggertypes.nim └── someModule.nim └── superlog.nim /README.md: -------------------------------------------------------------------------------- 1 | # Superlog concept 2 | Based on an idea I had while replying to a [forum topic](https://forum.nim-lang.org/t/8880#58038). 3 | The general idea is that this would be a zero-cost pluggable logger. Each 4 | module that wants to support logging simply imports the superlog module, 5 | defines its loggable types, and then call `log` with a severity and one of its 6 | loggable types as the arguments. Like this: 7 | 8 | ```nim 9 | log info, LogInfo(msg: "Hello world") 10 | ``` 11 | 12 | Note that in this example `LogInfo` just contains a string, but the objects can 13 | contain anything the library wants to log. 14 | 15 | Then a module which imports this module can first import superlog and the 16 | loggable types from the library. Then it can register a logging procedure for 17 | that module like so: 18 | 19 | ```nim 20 | var currentSeverity = debug 21 | 22 | proc myLogger*(sev: Severity, message: LogInfo) = 23 | if sev > currentSeverity: 24 | echo now().format("yyyy-MM-dd HH:mm:sszzz"), ": ", message.msg 25 | 26 | registerLogger(LogInfo, myLogger) 27 | ``` 28 | 29 | When this is done it imports the module proper and all the `log` statements in 30 | the module will now be replaced with a call to the registered log procedure for 31 | that log type. Types which doesn't have a registered procedure will simply 32 | expand to an empty statement and that call simply doesn't create any code on 33 | runtime. 34 | 35 | ## Enrichment 36 | For fun I also implemented the ability to inspect the signature of the 37 | registered procedure. This allows the logging procedure to grab other 38 | information from the loggers scope. Maybe most useful is `instantiationInfo`: 39 | 40 | ```nim 41 | var currentSeverity = debug 42 | 43 | proc myLogger*(sev: Severity, instInfo: InstantiationInfo, message: LogInfo) = 44 | if sev > currentSeverity: 45 | var instStr = instInfo.filename & ":" & $instInfo.line & ":" & $instinfo.column 46 | echo now().format("yyyy-MM-dd HH:mm:sszzz"), ": ", message.msg, " {", instStr, "}" 47 | 48 | registerLogger(LogInfo, myLogger) 49 | ``` 50 | 51 | Currently all the arguments are optional, you can use `Severity`, 52 | `InstantiationInfo`, and `Locals[T]` (which returns the tuple from [locals()](https://nim-lang.org/docs/system.html#locals). 53 | Any unknown argument is replaced with the message, which of course can be a 54 | type class so you can use the same logger proc for different types: 55 | 56 | ```nim 57 | var currentSeverity = debug 58 | 59 | proc myLogger*(sev: Severity, instInfo: InstantiationInfo, message: LogInfo or ExceptionInfo) = 60 | if sev > currentSeverity: 61 | stdout.write now().format("yyyy-MM-dd HH:mm:sszzz") & ": " 62 | when message is LogInfo: 63 | stdout.write message.msg 64 | else: 65 | stdout.write "Exception ", message.ex.msg 66 | stdout.writeLine " [" & instInfo.filename & ":" & $instInfo.line & ":" & $instinfo.column & "]" 67 | 68 | registerLogger(LogInfo, myLogger) 69 | registerLogger(ExceptionInfo, myLogger) 70 | ``` 71 | 72 | ## Benefits 73 | The benefits of implementing a logger this way is that library owners don't 74 | have to worry about the cost of logging, users can simply not define a logger 75 | for a type. And users are free to do whatever they want with the logged data. 76 | Some good applications (courtesy of Araq): 77 | 78 | - You can use the library in a GUI setting. 79 | - You can i18n the error messages. 80 | - You can store the results in a database. 81 | - You can aggregate results. 82 | - You can filter and ignore results. 83 | 84 | And of course close to my own heart you could also import libraries with 85 | logging in a microcontroller or other super-restricted context and don't worry 86 | about the overhead. You could of course even write the messages out over 87 | serial, or just blink a coloured LED based on severity. 88 | 89 | ## Improvements 90 | To improve this proof of concept it should move away from creating type paths 91 | manually and instead use something like `macros.signatureHash`. All the body 92 | generation logic should also be moved into the registration macro, this makes 93 | it possible to write other registration macros which does other things than 94 | adding a procedure call. 95 | 96 | Another improvement would be to implement generics and define a 97 | `LogMessage[module: static[string]]` type and a companying 98 | `log(severity: Severity, message: string)` template. The template would grab 99 | the current module and create a message of the correct type and log with that. 100 | The module could then get away without having a separate loggingtypes file if 101 | it only wants to log strings. And the user would then be able to register a 102 | callback for every `LogMessage` regardless of module, or it could register them 103 | for a specific module. In essence this would make the simple case of just 104 | logging strings much easier. 105 | -------------------------------------------------------------------------------- /myProgram.nim: -------------------------------------------------------------------------------- 1 | import superlog 2 | import times 3 | 4 | var currentSeverity = warning 5 | 6 | import someModule/loggertypes 7 | 8 | # This procedure can take several arguments in any order, the superlogger will 9 | # automatically add the supported types in the call. This way we can get 10 | # information from the application being logged that it might not even 11 | # explicitly expose to us (Hint: Locals[T] is supported and will return a tuple 12 | # of all the locals in the logging scope, use with care). 13 | 14 | proc myLogger*(sev: Severity, instInfo: InstantiationInfo, message: LogInfo or ExceptionInfo) = 15 | if sev > currentSeverity: 16 | var instStr = instInfo.filename & ":" & $instInfo.line & ":" & $instinfo.column 17 | when message is LogInfo: 18 | echo now().format("yyyy-MM-dd HH:mm:sszzz"), ": ", message.msg, " [", instStr, "]" 19 | else: 20 | echo now().format("yyyy-MM-dd HH:mm:sszzz"), ": Exception ", message.ex.msg, " [", instStr, "]" 21 | 22 | # We also don't have to define paths, can simply pass the types in directly 23 | registerLogger(LogInfo, myLogger) 24 | registerLogger(ExceptionInfo, myLogger) 25 | 26 | # All of the above could of course be wrapped into some kind of block statement 27 | # if that was defined in superlogger. Currently I've just added severity as 28 | # a special field as it is very commonly used, but more information like module 29 | # name and instantiation information could be added as well (as long as care is 30 | # taken to not leak it into the code when it isn't used): 31 | # registerLogger(LogInfo, ExceptionInfo): 32 | # if sev > currentSeverity: 33 | # when message is LogInfo: 34 | # echo now().format("yyyy-MM-dd HH:mm:sszzz"), ": ", message.msg 35 | # else: 36 | # echo now().format("yyyy-MM-dd HH:mm:sszzz"), ": Exception ", message.ex.msg 37 | 38 | import someModule/someModule 39 | 40 | someProc("greet") 41 | currentSeverity = debug 42 | someProc("greet") 43 | -------------------------------------------------------------------------------- /someModule/loggertypes.nim: -------------------------------------------------------------------------------- 1 | type 2 | LogInfo* = object 3 | msg*: string 4 | ExceptionInfo* = object 5 | ex*: ValueError 6 | UnloggedInfo* = object 7 | unseenMsg*: string 8 | 9 | import "../superlog" 10 | template log*(x: string) = log(info, LogInfo(msg: x)) 11 | template logUnseen*(x: string) = log(info, UnloggedInfo(unseenMsg: x)) 12 | template log*(x: ref ValueError) = log(error, ExceptionInfo(ex: x[])) 13 | -------------------------------------------------------------------------------- /someModule/someModule.nim: -------------------------------------------------------------------------------- 1 | import "../superlog" 2 | import loggertypes 3 | import strutils 4 | 5 | proc someProc*(task: string) = 6 | echo task 7 | log "Task \"" & task & "\" failed succesfully" 8 | logUnseen "This message is never seen anywhere, not even in the binary" 9 | try: 10 | var x = parseInt("not an int") 11 | except ValueError as e: 12 | log e 13 | echo "Done" 14 | -------------------------------------------------------------------------------- /superlog.nim: -------------------------------------------------------------------------------- 1 | import tables, macros 2 | 3 | var activeLoggers {.compileTime.}: Table[string, NimNode] 4 | 5 | type 6 | Severity* = enum 7 | debug, 8 | info, 9 | notice, 10 | warning, 11 | error, 12 | critical, 13 | alert, 14 | emergency 15 | ModuleName* = string 16 | InstantiationInfo* = typeof(instantiationInfo()) 17 | Locals*[T] = T 18 | 19 | let 20 | # Ability to add extra information from the loggers scope to the message 21 | extraTypes {.compileTime.} = { 22 | "Severity": newIdentNode("severity"), 23 | "InstantiationInfo": nnkCall.newTree(newIdentNode("instantiationInfo")), 24 | "Locals": nnkCall.newTree(newIdentNode("locals")) 25 | }.toTable 26 | 27 | macro log*(sev: Severity, x: typed): untyped = 28 | var typeName = if x.getTypeInst.kind in {nnkRefTy}: 29 | x.getTypeInst[0] 30 | else: 31 | x.getTypeInst 32 | var 33 | moduleName = typeName.owner.strVal 34 | typePath = moduleName & "." & typeName.strVal 35 | echo "Trying to fetch logger for ", typePath 36 | if activeLoggers.hasKey(typePath): 37 | var 38 | log = activeLoggers[typePath] 39 | infoIdent = newIdentNode("message") 40 | severityIdent = extraTypes["Severity"] 41 | result = quote do: 42 | block: 43 | let 44 | `severityIdent` = `sev` 45 | `infoIdent` = `x` 46 | `log` 47 | else: 48 | result = newStmtList() 49 | echo result.repr 50 | 51 | macro registerLogger*(x: typedesc, y: proc): untyped = 52 | var typePath = x.owner.strVal & "." & x.strVal 53 | echo "Registering logger for ", typePath 54 | var call = nnkCall.newTree(y) 55 | let procType = y.getTypeImpl 56 | assert procType.kind == nnkProcTy, "Logger must be a proc" 57 | assert procType[0][0].kind == nnkEmpty, "Logger can't return anything" 58 | for field in procType[0][1..^1]: 59 | case field[1].kind: 60 | of nnkSym: 61 | call.add if extraTypes.hasKey(field[1].strVal): 62 | extraTypes[field[1].strVal] 63 | else: 64 | newIdentNode("message") 65 | of nnkBracketExpr: 66 | if extraTypes.hasKey(field[1][0].strVal): 67 | call.add extraTypes[field[1][0].strVal] 68 | else: discard 69 | 70 | activeLoggers[typePath] = call 71 | 72 | --------------------------------------------------------------------------------