/
logger.cr
178 lines (157 loc) · 5.46 KB
/
logger.cr
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
# The `Logger` class provides a simple but sophisticated logging utility that you can use to output messages.
#
# The messages have associated levels, such as `INFO` or `ERROR` that indicate their importance.
# You can then give the `Logger` a level, and only messages at that level of higher will be printed.
#
# For instance, in a production system, you may have your `Logger` set to `INFO` or even `WARN`.
# When you are developing the system, however, you probably want to know about the program’s internal state,
# and would set the `Logger` to `DEBUG`.
#
# If logging to multiple locations is required, an `IO::MultiWriter` can be
# used.
#
# ### Example
#
# ```
# require "logger"
#
# log = Logger.new(STDOUT)
# log.level = Logger::WARN
#
# # or
# log = Logger.new(STDOUT, level: Logger::WARN)
#
# log.debug("Created logger")
# log.info("Program started")
# log.warn("Nothing to do!")
#
# begin
# File.each_line("/foo/bar.log") do |line|
# unless line =~ /^(\w+) = (.*)$/
# log.error("Line in wrong format: #{line}")
# end
# end
# rescue err
# log.fatal("Caught exception; exiting")
# log.fatal(err)
# end
# ```
class Logger
property level : Severity
property progname : String
# Customizable `Proc` (with a reasonable default)
# which the `Logger` uses to format and print its entries.
#
# Use this setter to provide a custom formatter.
# The `Logger` will invoke it with the following arguments:
# - severity: a `Logger::Severity`
# - datetime: `Time`, the entry's timestamp
# - progname: `String`, the program name, if set when the logger was built
# - message: `String`, the body of a message
# - io: `IO`, the Logger's stream, to which you must write the final output
#
# Example:
#
# ```
# require "logger"
#
# logger = Logger.new(STDOUT)
# logger.progname = "YodaBot"
#
# logger.formatter = Logger::Formatter.new do |severity, datetime, progname, message, io|
# label = severity.unknown? ? "ANY" : severity.to_s
# io << label[0] << ", [" << datetime << " #" << Process.pid << "] "
# io << label.rjust(5) << " -- " << progname << ": " << message
# end
#
# logger.warn("Fear leads to anger. Anger leads to hate. Hate leads to suffering.")
#
# # Prints to the console:
# # "W, [2017-05-06 18:00:41 -0300 #11927] WARN --
# # YodaBot: Fear leads to anger. Anger leads to hate. Hate leads to suffering."
# ```
property formatter
# A logger severity level.
enum Severity
# Low-level information for developers
DEBUG
# Generic (useful) information about system operation
INFO
# A warning
WARN
# A handleable error condition
ERROR
# An unhandleable error that results in a program crash
FATAL
UNKNOWN
end
alias Formatter = Severity, Time, String, String, IO ->
private DEFAULT_FORMATTER = Formatter.new do |severity, datetime, progname, message, io|
label = severity.unknown? ? "ANY" : severity.to_s
io << label[0] << ", [" << datetime << " #" << Process.pid << "] "
io << label.rjust(5) << " -- " << progname << ": " << message
end
# :nodoc:
record Message,
severity : Severity,
datetime : Time,
progname : String,
message : String
# Creates a new logger that will log to the given *io*.
# If *io* is `nil` then all log calls will be silently ignored.
def initialize(@io : IO?, @level = Severity::INFO, @formatter = DEFAULT_FORMATTER, @progname = "")
@closed = false
@mutex = Mutex.new
end
# Calls the *close* method on the object passed to `initialize`.
def close
return if @closed
return unless io = @io
@closed = true
@mutex.synchronize do
io.close
end
end
{% for name in Severity.constants %}
{{name.id}} = Severity::{{name.id}}
# Returns `true` if the logger's current severity is lower or equal to `{{name.id}}`.
def {{name.id.downcase}}?
level <= Severity::{{name.id}}
end
# Logs *message* if the logger's current severity is lower or equal to `{{name.id}}`.
# *progname* overrides a default progname set in this logger.
def {{name.id.downcase}}(message, progname = nil)
log(Severity::{{name.id}}, message, progname)
end
# Logs the message as returned from the given block if the logger's current severity
# is lower or equal to `{{name.id}}`. The block is not run if the severity is higher.
# *progname* overrides a default progname set in this logger.
def {{name.id.downcase}}(progname = nil)
log(Severity::{{name.id}}, progname) { yield }
end
{% end %}
# Logs *message* if *severity* is higher or equal with the logger's current
# severity. *progname* overrides a default progname set in this logger.
def log(severity, message, progname = nil)
return if severity < level || !@io
write(severity, Time.local, progname || @progname, message)
end
# Logs the message as returned from the given block if *severity*
# is higher or equal with the loggers current severity. The block is not run
# if *severity* is lower. *progname* overrides a default progname set in this logger.
def log(severity, progname = nil)
return if severity < level || !@io
write(severity, Time.local, progname || @progname, yield)
end
private def write(severity, datetime, progname, message)
io = @io
return unless io
progname_to_s = progname.to_s
message_to_s = message.to_s
@mutex.synchronize do
formatter.call(severity, datetime, progname_to_s, message_to_s, io)
io.puts
io.flush
end
end
end