-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
format.cr
202 lines (180 loc) · 5.65 KB
/
format.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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
class Log
# The program name used for log entries
#
# Defaults to the executable name
class_property progname = File.basename(PROGRAM_NAME)
# The current process PID
protected class_getter pid : String = Process.pid.to_s
# Base interface to convert log entries and write them to an `IO`
module Formatter
# Writes a `Log::Entry` through an `IO`
abstract def format(entry : Log::Entry, io : IO)
# Creates an instance of a `Log::Formatter` that calls
# the specified `Proc` for every entry
def self.new(&proc : (Log::Entry, IO) ->)
ProcFormatter.new proc
end
end
# :nodoc:
private struct ProcFormatter
include Formatter
def initialize(@proc : (Log::Entry, IO) ->)
end
def format(entry : Log::Entry, io : IO) : Nil
@proc.call(entry, io)
end
end
# Base implementation of `Log::Formatter` to convert
# log entries into text representation
#
# This can be used to create efficient formatters:
# ```
# require "log"
#
# struct MyFormat < Log::StaticFormatter
# def run
# string "- "
# severity
# string ": "
# message
# end
# end
#
# Log.setup(:info, Log::IOBackend.new(formatter: MyFormat))
# Log.info { "Hello" } # => - INFO: Hello
# Log.error { "Oh, no!" } # => - ERROR: Oh, no!
# ```
#
# There is also a helper macro to generate these formatters. Here's
# an example that generates the same result:
# ```
# Log.define_formatter MyFormat, "- #{severity}: #{message}"
# ```
abstract struct StaticFormatter
extend Formatter
def initialize(@entry : Log::Entry, @io : IO)
end
# Write the entry timestamp in RFC3339 format
def timestamp : Nil
@entry.timestamp.to_rfc3339(@io, fraction_digits: 6)
end
# Write a fixed string
def string(str) : Nil
@io << str
end
# Write the message of the entry
def message : Nil
@io << @entry.message
end
# Write the severity
#
# This writes the severity in uppercase and left padded
# with enough space so all the severities fit
def severity : Nil
@entry.severity.label.rjust(@io, 6)
end
# Write the source for non-root entries
#
# It doesn't write any output for entries generated from the root logger.
# Parameters `before` and `after` can be provided to be written around
# the value.
# ```
# Log.define_formatter TestFormatter, "#{source(before: '[', after: "] ")}#{message}"
# Log.setup(:info, Log::IOBackend.new(formatter: TestFormatter))
# Log.for("foo.bar").info { "Hello" } # => - [foo.bar] Hello
# ```
def source(*, before = nil, after = nil)
if @entry.source.size > 0
@io << before << @entry.source << after
end
end
# Write all the values from the entry data
#
# It doesn't write any output if the entry data is empty.
# Parameters `before` and `after` can be provided to be written around
# the value.
def data(*, before = nil, after = nil) : Nil
unless @entry.data.empty?
@io << before << @entry.data << after
end
end
# Write all the values from the context
#
# It doesn't write any output if the context is empty.
# Parameters `before` and `after` can be provided to be written around
# the value.
def context(*, before = nil, after = nil)
unless @entry.context.empty?
@io << before << @entry.context << after
end
end
# Write the exception, including backtrace
#
# It doesn't write any output unless there is an exception in the entry.
# Parameters `before` and `after` can be provided to be written around
# the value. `before` defaults to `'\n'` so the exception is written
# on a separate line
def exception(*, before = '\n', after = nil) : Nil
if ex = @entry.exception
@io << before
ex.inspect_with_backtrace(@io)
@io << after
end
end
# Write the program name. See `Log.progname`.
def progname : Nil
@io << Log.progname
end
# Write the current process identifier
def pid(*, before = '#', after = nil)
@io << before << Log.pid << after
end
# Write the `Log::Entry` to the `IO` using this pattern
def self.format(entry, io) : Nil
new(entry, io).run
end
# Subclasses must implement this method to define the output pattern
abstract def run
end
# Generate subclasses of `Log::StaticFormatter` from a string with interpolations
#
# Example:
# ```
# Log.define_formatter MyFormat, "- #{severity}: #{message}"
# ```
# See `Log::StaticFormatter` for the available methods that can
# be called within the interpolations.
macro define_formatter(name, pattern)
struct {{name}} < ::Log::StaticFormatter
def run
{% for part in pattern.expressions %}
{% if part.is_a?(StringLiteral) %}
string {{ part }}
{% else %}
{{ part }}
{% end %}
{% end %}
end
end
end
end
# Default short format
#
# It writes log entries with the following format:
# ```
# 2020-05-07T17:40:07.994508000Z INFO - my.source: Initializing everything
# ```
#
# When the entries have context data it's also written to the output:
# ```
# 2020-05-07T17:40:07.994508000Z INFO - my.source: Initializing everything -- {"data" => 123}
# ```
#
# Exceptions are written in a separate line:
# ```
# 2020-05-07T17:40:07.994508000Z ERROR - my.source: Something failed
# Oh, no (Exception)
# from ...
# ```
Log.define_formatter Log::ShortFormat, "#{timestamp} #{severity} - #{source(after: ": ")}#{message}" \
"#{data(before: " -- ")}#{context(before: " -- ")}#{exception}"