logbrew-sdk 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +219 -23
- data/examples/Makefile +9 -1
- data/examples/first_useful_telemetry.rb +118 -0
- data/examples/http_trace_correlation.rb +99 -0
- data/examples/real_user_smoke.rb +15 -0
- data/lib/logbrew/operation_tracing.rb +179 -0
- data/lib/logbrew/product_timeline.rb +153 -0
- data/lib/logbrew/support_ticket.rb +187 -0
- data/lib/logbrew/trace.rb +298 -0
- data/lib/logbrew/traceparent.rb +145 -0
- data/lib/logbrew.rb +71 -8
- metadata +9 -2
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module LogBrew
|
|
6
|
+
TraceContext = Struct.new(:trace_id, :span_id, :parent_span_id, :trace_flags, :sampled, keyword_init: true)
|
|
7
|
+
|
|
8
|
+
class TraceScope
|
|
9
|
+
def initialize(scope_id)
|
|
10
|
+
@scope_id = scope_id
|
|
11
|
+
@closed = false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def close
|
|
15
|
+
return if @closed
|
|
16
|
+
|
|
17
|
+
LogBrew::Trace.close_scope(@scope_id)
|
|
18
|
+
@closed = true
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Request-local trace context for app-owned logs, errors, actions, and metrics.
|
|
23
|
+
module Trace
|
|
24
|
+
STACK_KEY = :logbrew_trace_stack
|
|
25
|
+
private_constant :STACK_KEY
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
def current
|
|
30
|
+
entry = stack.last
|
|
31
|
+
entry && entry[:context]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def activate(context)
|
|
35
|
+
unless context.is_a?(TraceContext)
|
|
36
|
+
raise SdkError.new("validation_error", "trace context must be a LogBrew::TraceContext")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
scope_id = Object.new.object_id
|
|
40
|
+
stack << { id: scope_id, context: context }
|
|
41
|
+
TraceScope.new(scope_id)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def with_context(context)
|
|
45
|
+
scope = activate(context)
|
|
46
|
+
yield context
|
|
47
|
+
ensure
|
|
48
|
+
scope.close if scope
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def close_scope(scope_id)
|
|
52
|
+
entries = stack
|
|
53
|
+
index = entries.rindex { |entry| entry[:id] == scope_id }
|
|
54
|
+
entries.delete_at(index) unless index.nil?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def continue_or_create(traceparent)
|
|
58
|
+
text = traceparent.to_s.strip
|
|
59
|
+
return create_root if text.empty?
|
|
60
|
+
|
|
61
|
+
from_traceparent(text)
|
|
62
|
+
rescue SdkError
|
|
63
|
+
create_root
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def from_traceparent(traceparent)
|
|
67
|
+
context = Traceparent.parse(traceparent)
|
|
68
|
+
create(
|
|
69
|
+
trace_id: context.trace_id,
|
|
70
|
+
span_id: generate_span_id,
|
|
71
|
+
parent_span_id: context.parent_span_id,
|
|
72
|
+
trace_flags: context.trace_flags
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def create_root
|
|
77
|
+
create(trace_id: generate_trace_id, span_id: generate_span_id, trace_flags: "01")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def create(trace_id:, span_id:, trace_flags: "01", parent_span_id: nil)
|
|
81
|
+
normalized_traceparent = Traceparent.create(trace_id: trace_id, span_id: span_id, trace_flags: trace_flags)
|
|
82
|
+
_version, normalized_trace_id, normalized_span_id, normalized_flags = normalized_traceparent.split("-")
|
|
83
|
+
normalized_parent_span_id = nil
|
|
84
|
+
unless parent_span_id.nil?
|
|
85
|
+
parent_traceparent = Traceparent.create(
|
|
86
|
+
trace_id: normalized_trace_id,
|
|
87
|
+
span_id: parent_span_id,
|
|
88
|
+
trace_flags: normalized_flags
|
|
89
|
+
)
|
|
90
|
+
normalized_parent_span_id = parent_traceparent.split("-")[2]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
TraceContext.new(
|
|
94
|
+
trace_id: normalized_trace_id,
|
|
95
|
+
span_id: normalized_span_id,
|
|
96
|
+
parent_span_id: normalized_parent_span_id,
|
|
97
|
+
trace_flags: normalized_flags,
|
|
98
|
+
sampled: (normalized_flags.to_i(16) & 1) == 1
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def create_headers(context = current)
|
|
103
|
+
return {} unless context
|
|
104
|
+
|
|
105
|
+
Traceparent.create_headers(
|
|
106
|
+
trace_id: context.trace_id,
|
|
107
|
+
span_id: context.span_id,
|
|
108
|
+
trace_flags: context.trace_flags
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def metadata(context = current)
|
|
113
|
+
return {} unless context
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
"traceId" => context.trace_id,
|
|
117
|
+
"spanId" => context.span_id,
|
|
118
|
+
"traceFlags" => context.trace_flags,
|
|
119
|
+
"traceSampled" => context.sampled
|
|
120
|
+
}.tap do |payload|
|
|
121
|
+
payload["parentSpanId"] = context.parent_span_id unless context.parent_span_id.nil?
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def add_metadata(target, context = current)
|
|
126
|
+
return target unless context
|
|
127
|
+
|
|
128
|
+
metadata(context).each do |key, value|
|
|
129
|
+
target[key] = value unless target.key?(key) || target.key?(key.to_sym)
|
|
130
|
+
end
|
|
131
|
+
target
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def merge_attributes(attributes, context = current)
|
|
135
|
+
return attributes unless context && attributes.is_a?(Hash)
|
|
136
|
+
|
|
137
|
+
metadata_value = attributes.key?("metadata") ? attributes["metadata"] : attributes[:metadata]
|
|
138
|
+
return attributes unless metadata_value.nil? || metadata_value.is_a?(Hash)
|
|
139
|
+
|
|
140
|
+
copied = attributes.dup
|
|
141
|
+
merged_metadata = metadata_value.nil? ? {} : metadata_value.dup
|
|
142
|
+
add_metadata(merged_metadata, context)
|
|
143
|
+
if copied.key?(:metadata) && !copied.key?("metadata")
|
|
144
|
+
copied[:metadata] = merged_metadata
|
|
145
|
+
else
|
|
146
|
+
copied["metadata"] = merged_metadata
|
|
147
|
+
end
|
|
148
|
+
copied
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def from_rack_env(env)
|
|
152
|
+
existing = env_value(env, "logbrew.trace")
|
|
153
|
+
return existing if existing.is_a?(TraceContext)
|
|
154
|
+
|
|
155
|
+
traceparent = env_value(env, "HTTP_TRACEPARENT") || env_value(env, "traceparent") || env_value(env, "logbrew.traceparent")
|
|
156
|
+
return continue_or_create(traceparent) unless traceparent.nil? || traceparent.empty?
|
|
157
|
+
|
|
158
|
+
trace_id = env_value(env, "logbrew.trace_id")
|
|
159
|
+
span_id = env_value(env, "logbrew.span_id")
|
|
160
|
+
if trace_id && span_id
|
|
161
|
+
begin
|
|
162
|
+
return create(
|
|
163
|
+
trace_id: trace_id,
|
|
164
|
+
span_id: span_id,
|
|
165
|
+
parent_span_id: env_value(env, "logbrew.parent_span_id"),
|
|
166
|
+
trace_flags: env_value(env, "logbrew.trace_flags") || "01"
|
|
167
|
+
)
|
|
168
|
+
rescue SdkError
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
create_root
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def generate_trace_id
|
|
177
|
+
loop do
|
|
178
|
+
value = SecureRandom.hex(16)
|
|
179
|
+
return value unless value.delete("0").empty?
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def generate_span_id
|
|
184
|
+
loop do
|
|
185
|
+
value = SecureRandom.hex(8)
|
|
186
|
+
return value unless value.delete("0").empty?
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def stack
|
|
191
|
+
Thread.current[STACK_KEY] ||= []
|
|
192
|
+
end
|
|
193
|
+
private_class_method :stack
|
|
194
|
+
|
|
195
|
+
def env_value(env, key)
|
|
196
|
+
return nil unless env.respond_to?(:[])
|
|
197
|
+
|
|
198
|
+
value = env[key]
|
|
199
|
+
return value if value.is_a?(TraceContext)
|
|
200
|
+
return nil if value.nil?
|
|
201
|
+
|
|
202
|
+
text = value.to_s
|
|
203
|
+
text.empty? ? nil : text
|
|
204
|
+
end
|
|
205
|
+
private_class_method :env_value
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
module TraceClientMethods
|
|
209
|
+
def issue(id, timestamp, attributes)
|
|
210
|
+
super(id, timestamp, Trace.merge_attributes(attributes))
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def log(id, timestamp, attributes)
|
|
214
|
+
super(id, timestamp, Trace.merge_attributes(attributes))
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def metric(id, timestamp, attributes)
|
|
218
|
+
super(id, timestamp, Trace.merge_attributes(attributes))
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def action(id, timestamp, attributes)
|
|
222
|
+
super(id, timestamp, Trace.merge_attributes(attributes))
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
module TraceLoggerMethods
|
|
227
|
+
private
|
|
228
|
+
|
|
229
|
+
def logbrew_metadata(severity, message, progname)
|
|
230
|
+
Trace.add_metadata(super)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
module TraceRackMiddlewareMethods
|
|
235
|
+
def call(env)
|
|
236
|
+
trace_context = Trace.from_rack_env(env)
|
|
237
|
+
env["logbrew.trace"] = trace_context if env.respond_to?(:[]=)
|
|
238
|
+
Trace.with_context(trace_context) { super(env) }
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
private
|
|
242
|
+
|
|
243
|
+
def capture_request_span(env, status_code, elapsed_ms, status)
|
|
244
|
+
context = rack_trace_context(env)
|
|
245
|
+
attributes = {
|
|
246
|
+
name: request_name(env),
|
|
247
|
+
traceId: context ? context.trace_id : trace_id(env),
|
|
248
|
+
spanId: context ? context.span_id : span_id(env),
|
|
249
|
+
status: status,
|
|
250
|
+
durationMs: elapsed_ms,
|
|
251
|
+
metadata: request_metadata(env, status_code)
|
|
252
|
+
}
|
|
253
|
+
attributes[:parentSpanId] = context.parent_span_id if context && context.parent_span_id
|
|
254
|
+
|
|
255
|
+
@client.span(next_event_id("span"), logbrew_timestamp, attributes)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def trace_id(env)
|
|
259
|
+
context = rack_trace_context(env)
|
|
260
|
+
return context.trace_id if context
|
|
261
|
+
|
|
262
|
+
super
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def span_id(env)
|
|
266
|
+
context = rack_trace_context(env)
|
|
267
|
+
return context.span_id if context
|
|
268
|
+
|
|
269
|
+
super
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def request_metadata(env, status_code)
|
|
273
|
+
Trace.add_metadata(super, rack_trace_context(env))
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def exception_metadata(env, error)
|
|
277
|
+
Trace.add_metadata(super, rack_trace_context(env))
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def rack_trace_context(env)
|
|
281
|
+
trace = env["logbrew.trace"] if env.respond_to?(:[])
|
|
282
|
+
trace.is_a?(TraceContext) ? trace : Trace.current
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
module TraceRailsErrorSubscriberMethods
|
|
287
|
+
private
|
|
288
|
+
|
|
289
|
+
def rails_metadata(error, handled, severity, context, source)
|
|
290
|
+
Trace.add_metadata(super)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
Client.prepend(TraceClientMethods)
|
|
295
|
+
Logger.prepend(TraceLoggerMethods)
|
|
296
|
+
RackMiddleware.prepend(TraceRackMiddlewareMethods)
|
|
297
|
+
RailsErrorSubscriber.prepend(TraceRailsErrorSubscriberMethods)
|
|
298
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LogBrew
|
|
4
|
+
TraceparentContext = Struct.new(:version, :trace_id, :parent_span_id, :trace_flags, :sampled, keyword_init: true)
|
|
5
|
+
TraceparentSpanInput = Struct.new(:name, :span_id, :status, :duration_ms, :metadata, keyword_init: true) do
|
|
6
|
+
def initialize(name:, span_id:, status: "ok", duration_ms: nil, metadata: nil)
|
|
7
|
+
super(
|
|
8
|
+
name: name,
|
|
9
|
+
span_id: span_id,
|
|
10
|
+
status: status,
|
|
11
|
+
duration_ms: duration_ms,
|
|
12
|
+
metadata: metadata
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Dependency-free W3C traceparent helpers for explicit app-owned propagation.
|
|
18
|
+
module Traceparent
|
|
19
|
+
VERSION = "00"
|
|
20
|
+
private_constant :VERSION
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def parse(traceparent)
|
|
25
|
+
Validation.require_non_empty("traceparent", traceparent)
|
|
26
|
+
parts = traceparent.to_s.strip.downcase.split("-")
|
|
27
|
+
raise SdkError.new("validation_error", "traceparent must have four fields") unless parts.length == 4
|
|
28
|
+
|
|
29
|
+
version, trace_id, parent_span_id, trace_flags = parts
|
|
30
|
+
require_version(version)
|
|
31
|
+
require_trace_id(trace_id)
|
|
32
|
+
require_span_id("traceparent parent span id", parent_span_id)
|
|
33
|
+
flags = normalize_trace_flags(trace_flags)
|
|
34
|
+
|
|
35
|
+
TraceparentContext.new(
|
|
36
|
+
version: version,
|
|
37
|
+
trace_id: trace_id,
|
|
38
|
+
parent_span_id: parent_span_id,
|
|
39
|
+
trace_flags: flags,
|
|
40
|
+
sampled: (flags.to_i(16) & 1) == 1
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def create(trace_id:, span_id:, trace_flags: "01")
|
|
45
|
+
normalized_trace_id = normalize_trace_id(trace_id)
|
|
46
|
+
normalized_span_id = normalize_span_id("traceparent span id", span_id)
|
|
47
|
+
flags = normalize_trace_flags(trace_flags)
|
|
48
|
+
|
|
49
|
+
"#{VERSION}-#{normalized_trace_id}-#{normalized_span_id}-#{flags}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def create_headers(trace_id:, span_id:, trace_flags: "01")
|
|
53
|
+
{ "traceparent" => create(trace_id: trace_id, span_id: span_id, trace_flags: trace_flags) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def span_attributes_from_traceparent(traceparent, input)
|
|
57
|
+
context = traceparent.is_a?(TraceparentContext) ? traceparent : parse(traceparent)
|
|
58
|
+
attributes = {
|
|
59
|
+
"name" => required_name(input.name),
|
|
60
|
+
"traceId" => context.trace_id,
|
|
61
|
+
"spanId" => normalize_span_id("span spanId", input.span_id),
|
|
62
|
+
"parentSpanId" => context.parent_span_id,
|
|
63
|
+
"status" => normalize_status(input.status)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
unless input.duration_ms.nil?
|
|
67
|
+
duration_ms = Validation.require_finite_number("span durationMs", input.duration_ms)
|
|
68
|
+
raise SdkError.new("validation_error", "span durationMs must be non-negative") if duration_ms.negative?
|
|
69
|
+
|
|
70
|
+
attributes["durationMs"] = duration_ms
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
metadata = Validation.require_metadata(input.metadata)
|
|
74
|
+
attributes["metadata"] = metadata unless metadata.nil?
|
|
75
|
+
attributes
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def require_version(version)
|
|
79
|
+
unless version.length == 2 && lower_hex?(version) && version != "ff"
|
|
80
|
+
raise SdkError.new("validation_error", "traceparent version must be two hex characters and not ff")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
private_class_method :require_version
|
|
84
|
+
|
|
85
|
+
def normalize_trace_id(trace_id)
|
|
86
|
+
normalized = trace_id.to_s.strip.downcase
|
|
87
|
+
require_trace_id(normalized)
|
|
88
|
+
normalized
|
|
89
|
+
end
|
|
90
|
+
private_class_method :normalize_trace_id
|
|
91
|
+
|
|
92
|
+
def require_trace_id(trace_id)
|
|
93
|
+
unless trace_id.length == 32 && lower_hex?(trace_id) && !all_zero?(trace_id)
|
|
94
|
+
raise SdkError.new("validation_error", "traceparent trace id must be 32 non-zero hex characters")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
private_class_method :require_trace_id
|
|
98
|
+
|
|
99
|
+
def normalize_span_id(label, span_id)
|
|
100
|
+
normalized = span_id.to_s.strip.downcase
|
|
101
|
+
require_span_id(label, normalized)
|
|
102
|
+
normalized
|
|
103
|
+
end
|
|
104
|
+
private_class_method :normalize_span_id
|
|
105
|
+
|
|
106
|
+
def require_span_id(label, span_id)
|
|
107
|
+
unless span_id.length == 16 && lower_hex?(span_id) && !all_zero?(span_id)
|
|
108
|
+
raise SdkError.new("validation_error", "#{label} must be 16 non-zero hex characters")
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
private_class_method :require_span_id
|
|
112
|
+
|
|
113
|
+
def normalize_trace_flags(trace_flags)
|
|
114
|
+
normalized = trace_flags.to_s.strip.downcase
|
|
115
|
+
unless normalized.length == 2 && lower_hex?(normalized)
|
|
116
|
+
raise SdkError.new("validation_error", "traceparent flags must be two hex characters")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
normalized
|
|
120
|
+
end
|
|
121
|
+
private_class_method :normalize_trace_flags
|
|
122
|
+
|
|
123
|
+
def required_name(name)
|
|
124
|
+
Validation.require_non_empty("span name", name)
|
|
125
|
+
name.to_s.strip
|
|
126
|
+
end
|
|
127
|
+
private_class_method :required_name
|
|
128
|
+
|
|
129
|
+
def normalize_status(status)
|
|
130
|
+
Validation.require_allowed_value("span status", status, LogBrew::SPAN_STATUSES)
|
|
131
|
+
status
|
|
132
|
+
end
|
|
133
|
+
private_class_method :normalize_status
|
|
134
|
+
|
|
135
|
+
def lower_hex?(value)
|
|
136
|
+
value.match?(/\A[0-9a-f]+\z/)
|
|
137
|
+
end
|
|
138
|
+
private_class_method :lower_hex?
|
|
139
|
+
|
|
140
|
+
def all_zero?(value)
|
|
141
|
+
value.delete("0").empty?
|
|
142
|
+
end
|
|
143
|
+
private_class_method :all_zero?
|
|
144
|
+
end
|
|
145
|
+
end
|
data/lib/logbrew.rb
CHANGED
|
@@ -9,10 +9,26 @@ require "timeout"
|
|
|
9
9
|
require "uri"
|
|
10
10
|
|
|
11
11
|
module LogBrew
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
SEVERITY_VALUES = %w[trace debug info warn warning error fatal critical].freeze
|
|
13
|
+
SEVERITY_ALIASES = {
|
|
14
|
+
"trace" => "info",
|
|
15
|
+
"debug" => "info",
|
|
16
|
+
"info" => "info",
|
|
17
|
+
"warn" => "warning",
|
|
18
|
+
"warning" => "warning",
|
|
19
|
+
"error" => "error",
|
|
20
|
+
"fatal" => "critical",
|
|
21
|
+
"critical" => "critical"
|
|
22
|
+
}.freeze
|
|
14
23
|
SPAN_STATUSES = %w[ok error].freeze
|
|
15
24
|
ACTION_STATUSES = %w[queued running success failure].freeze
|
|
25
|
+
METRIC_TEMPORALITIES_BY_KIND = {
|
|
26
|
+
"counter" => %w[delta cumulative].freeze,
|
|
27
|
+
"gauge" => %w[instant].freeze,
|
|
28
|
+
"histogram" => %w[delta cumulative].freeze
|
|
29
|
+
}.freeze
|
|
30
|
+
METRIC_KINDS = METRIC_TEMPORALITIES_BY_KIND.keys.freeze
|
|
31
|
+
NON_NEGATIVE_METRIC_KINDS = %w[counter histogram].freeze
|
|
16
32
|
|
|
17
33
|
class SdkError < StandardError
|
|
18
34
|
attr_reader :code
|
|
@@ -158,11 +174,11 @@ module LogBrew
|
|
|
158
174
|
class Logger < ::Logger
|
|
159
175
|
DEFAULT_LOGGER_NAME = "ruby-logger"
|
|
160
176
|
SEVERITY_TO_LOGBREW_LEVEL = {
|
|
161
|
-
::Logger::DEBUG => "
|
|
177
|
+
::Logger::DEBUG => "info",
|
|
162
178
|
::Logger::INFO => "info",
|
|
163
179
|
::Logger::WARN => "warning",
|
|
164
180
|
::Logger::ERROR => "error",
|
|
165
|
-
::Logger::FATAL => "
|
|
181
|
+
::Logger::FATAL => "critical",
|
|
166
182
|
::Logger::UNKNOWN => "error"
|
|
167
183
|
}.freeze
|
|
168
184
|
|
|
@@ -275,7 +291,7 @@ module LogBrew
|
|
|
275
291
|
end
|
|
276
292
|
|
|
277
293
|
def logbrew_level(severity)
|
|
278
|
-
SEVERITY_TO_LOGBREW_LEVEL.fetch(severity.to_i, severity.to_i >= ::Logger::
|
|
294
|
+
SEVERITY_TO_LOGBREW_LEVEL.fetch(severity.to_i, severity.to_i >= ::Logger::FATAL ? "critical" : "info")
|
|
279
295
|
end
|
|
280
296
|
|
|
281
297
|
def event_logger_name(progname)
|
|
@@ -314,7 +330,6 @@ module LogBrew
|
|
|
314
330
|
value.is_a?(Float) && value.finite?
|
|
315
331
|
end
|
|
316
332
|
end
|
|
317
|
-
|
|
318
333
|
# Rack-compatible middleware for Rails, Sinatra, and other Rack-based Ruby apps.
|
|
319
334
|
#
|
|
320
335
|
# The middleware captures completed requests as span events and unhandled app
|
|
@@ -699,6 +714,15 @@ module LogBrew
|
|
|
699
714
|
end
|
|
700
715
|
end
|
|
701
716
|
|
|
717
|
+
def require_finite_number(label, value)
|
|
718
|
+
unless value.is_a?(Integer) || value.is_a?(Float)
|
|
719
|
+
raise SdkError.new("validation_error", "#{label} must be a finite number")
|
|
720
|
+
end
|
|
721
|
+
raise SdkError.new("validation_error", "#{label} must be a finite number") if numeric_nan?(value)
|
|
722
|
+
|
|
723
|
+
value
|
|
724
|
+
end
|
|
725
|
+
|
|
702
726
|
def read(attributes, key)
|
|
703
727
|
return nil unless attributes.is_a?(Hash)
|
|
704
728
|
|
|
@@ -764,6 +788,10 @@ module LogBrew
|
|
|
764
788
|
push_event("span", id, timestamp, validate_span(attributes))
|
|
765
789
|
end
|
|
766
790
|
|
|
791
|
+
def metric(id, timestamp, attributes)
|
|
792
|
+
push_event("metric", id, timestamp, validate_metric(attributes))
|
|
793
|
+
end
|
|
794
|
+
|
|
767
795
|
def action(id, timestamp, attributes)
|
|
768
796
|
push_event("action", id, timestamp, validate_action(attributes))
|
|
769
797
|
end
|
|
@@ -858,7 +886,7 @@ module LogBrew
|
|
|
858
886
|
title = Validation.read(attributes, "title")
|
|
859
887
|
level = Validation.read(attributes, "level")
|
|
860
888
|
Validation.require_non_empty("issue title", title)
|
|
861
|
-
|
|
889
|
+
level = normalize_severity("issue level", level)
|
|
862
890
|
with_metadata(
|
|
863
891
|
{
|
|
864
892
|
"title" => title,
|
|
@@ -875,7 +903,7 @@ module LogBrew
|
|
|
875
903
|
message = Validation.read(attributes, "message")
|
|
876
904
|
level = Validation.read(attributes, "level")
|
|
877
905
|
Validation.require_non_empty("log message", message)
|
|
878
|
-
|
|
906
|
+
level = normalize_severity("log level", level)
|
|
879
907
|
with_metadata(
|
|
880
908
|
{
|
|
881
909
|
"message" => message,
|
|
@@ -888,6 +916,11 @@ module LogBrew
|
|
|
888
916
|
)
|
|
889
917
|
end
|
|
890
918
|
|
|
919
|
+
def normalize_severity(label, level)
|
|
920
|
+
Validation.require_allowed_value(label, level, SEVERITY_VALUES)
|
|
921
|
+
SEVERITY_ALIASES.fetch(level)
|
|
922
|
+
end
|
|
923
|
+
|
|
891
924
|
def validate_span(attributes)
|
|
892
925
|
name = Validation.read(attributes, "name")
|
|
893
926
|
trace_id = Validation.read(attributes, "traceId")
|
|
@@ -919,6 +952,32 @@ module LogBrew
|
|
|
919
952
|
)
|
|
920
953
|
end
|
|
921
954
|
|
|
955
|
+
def validate_metric(attributes)
|
|
956
|
+
name = Validation.read(attributes, "name")
|
|
957
|
+
kind = Validation.read(attributes, "kind")
|
|
958
|
+
unit = Validation.read(attributes, "unit")
|
|
959
|
+
temporality = Validation.read(attributes, "temporality")
|
|
960
|
+
Validation.require_non_empty("metric name", name)
|
|
961
|
+
Validation.require_allowed_value("metric kind", kind, METRIC_KINDS)
|
|
962
|
+
value = Validation.require_finite_number("metric value", Validation.read(attributes, "value"))
|
|
963
|
+
Validation.require_non_empty("metric unit", unit)
|
|
964
|
+
Validation.require_allowed_value("metric temporality for #{kind}", temporality, METRIC_TEMPORALITIES_BY_KIND[kind])
|
|
965
|
+
if NON_NEGATIVE_METRIC_KINDS.include?(kind) && value.negative?
|
|
966
|
+
raise SdkError.new("validation_error", "metric #{kind} value must be non-negative")
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
with_metadata(
|
|
970
|
+
{
|
|
971
|
+
"name" => name,
|
|
972
|
+
"kind" => kind,
|
|
973
|
+
"value" => value,
|
|
974
|
+
"unit" => unit,
|
|
975
|
+
"temporality" => temporality
|
|
976
|
+
},
|
|
977
|
+
attributes
|
|
978
|
+
)
|
|
979
|
+
end
|
|
980
|
+
|
|
922
981
|
def validate_action(attributes)
|
|
923
982
|
name = Validation.read(attributes, "name")
|
|
924
983
|
status = Validation.read(attributes, "status")
|
|
@@ -934,3 +993,7 @@ module LogBrew
|
|
|
934
993
|
end
|
|
935
994
|
end
|
|
936
995
|
end
|
|
996
|
+
|
|
997
|
+
%w[product_timeline traceparent trace operation_tracing support_ticket].each do |path|
|
|
998
|
+
require_relative "logbrew/#{path}"
|
|
999
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: logbrew-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- LogBrew
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Public LogBrew Ruby SDK for building, validating, and flushing event
|
|
14
14
|
batches.
|
|
@@ -19,9 +19,16 @@ extra_rdoc_files: []
|
|
|
19
19
|
files:
|
|
20
20
|
- README.md
|
|
21
21
|
- examples/Makefile
|
|
22
|
+
- examples/first_useful_telemetry.rb
|
|
23
|
+
- examples/http_trace_correlation.rb
|
|
22
24
|
- examples/readme_example.rb
|
|
23
25
|
- examples/real_user_smoke.rb
|
|
24
26
|
- lib/logbrew.rb
|
|
27
|
+
- lib/logbrew/operation_tracing.rb
|
|
28
|
+
- lib/logbrew/product_timeline.rb
|
|
29
|
+
- lib/logbrew/support_ticket.rb
|
|
30
|
+
- lib/logbrew/trace.rb
|
|
31
|
+
- lib/logbrew/traceparent.rb
|
|
25
32
|
homepage: https://github.com/LogBrewCo/sdk
|
|
26
33
|
licenses:
|
|
27
34
|
- MIT
|