/
builder.cr
162 lines (143 loc) · 4.85 KB
/
builder.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
require "weak_ref"
# Used in `Log.setup` methods to configure the binding to be used.
module Log::Configuration
# Binds a *source* pattern to a *backend* for all logs that are of severity equal or higher to *level*.
abstract def bind(source : String, level : Severity, backend : Backend)
end
# A `Log::Builder` creates `Log` instances for a given source.
# It allows you to bind sources and patterns to a given backend.
# Already created `Log` will be reconfigured as needed.
class Log::Builder
include Configuration
@mutex = Mutex.new(:unchecked)
@logs = Hash(String, WeakRef(Log)).new
private record Binding, source : String, level : Severity, backend : Backend
@bindings = Array(Binding).new
# Binds a *source* pattern to a *backend* for all logs that are of severity equal or higher to *level*.
def bind(source : String, level : Severity, backend : Backend) : Nil
# TODO validate source is a valid path
@mutex.synchronize do
binding = Binding.new(source: source, level: level, backend: backend)
@bindings << binding
each_log do |log|
if Builder.matches(log.source, binding.source)
append_backend(log, binding.level, binding.backend)
end
end
end
end
# :nodoc:
# Removes an existing bind. It assumes there was a single bind with that backend.
def unbind(source : String, level : Severity, backend : Backend) : Nil
@mutex.synchronize do
binding = Binding.new(source: source, level: level, backend: backend)
@bindings.delete(binding) || raise ArgumentError.new("Non-existing binding #{source}, #{level}, #{backend}")
each_log do |log|
if Builder.matches(log.source, binding.source)
remove_backend(log, binding.backend)
end
end
end
end
# Removes all existing bindings.
def clear : Nil
@mutex.synchronize do
@bindings.clear
each_log do |log|
log.backend = nil
log.initial_level = :none
end
end
end
# Returns a `Log` for the given *source* with a severity level and
# backend according to the bindings in `self`.
# If new bindings are applied, the existing `Log` instances will be
# reconfigured.
# Calling this method multiple times with the same value will return
# the same object.
def for(source : String) : Log
@mutex.synchronize do
log = @logs[source]?.try &.value
if log.nil?
log = Log.new(source, nil, :none)
@bindings.each do |binding|
next unless Builder.matches(log.source, binding.source)
append_backend(log, binding.level, binding.backend)
end
@logs[source] = WeakRef.new(log)
end
log
end
end
# :nodoc:
private def each_log
@logs.reject! { |_, log_ref| log_ref.value.nil? }
@logs.each_value do |log_ref|
log = log_ref.value
yield log if log
end
end
# :nodoc:
private def append_backend(log : Log, level : Severity, backend : Backend)
current_backend = log.backend
case current_backend
when Nil
log.backend = backend
log.initial_level = level
when BroadcastBackend
current_backend.append(backend, level)
# initial_level needs to be recomputed since the append_backend
# might be called with the same backend as before but with a
# different (higher) level
log.initial_level = current_backend.min_level
current_backend.level = log.changed_level
else
if current_backend == backend
# if the bind applies for the same backend, the last applied
# level should be used
log.initial_level = level
else
broadcast = BroadcastBackend.new
broadcast.append(current_backend, log.initial_level)
broadcast.append(backend, level)
broadcast.level = log.changed_level
log.backend = broadcast
log.initial_level = broadcast.min_level
end
end
end
# :nodoc:
private def remove_backend(log : Log, backend : Backend)
current_backend = log.backend
case current_backend
when Nil
raise ArgumentError.new("Trying to remove backend of a log without one")
when BroadcastBackend
current_backend.remove(backend)
if (single_backend = current_backend.single_backend?)
log.backend = single_backend[0]
log.initial_level = single_backend[1]
else
log.initial_level = current_backend.min_level
end
else
log.backend = nil
log.initial_level = :none
end
end
# :nodoc:
def close : Nil
@bindings.each &.backend.close
end
# :nodoc:
def self.matches(source : String, pattern : String) : Bool
return true if source == pattern
return true if pattern == "*"
if prefix = pattern.rchop?(".*")
return true if source == prefix
# do not match foobar with foo.*
return true if source.starts_with?(prefix) && source[prefix.size] == '.'
end
false
end
end