chronos-ruby 0.6.0.pre.1 → 0.7.0.pre.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/CHANGELOG.md +24 -0
- data/README.md +27 -8
- data/contracts/apm-batch-v1.schema.json +35 -0
- data/contracts/event-v1.schema.json +1 -1
- data/docs/adr/ADR-015-bounded-apm-aggregation.md +27 -0
- data/docs/architecture.md +6 -2
- data/docs/compatibility.md +2 -0
- data/docs/configuration.md +18 -1
- data/docs/data-collected.md +10 -1
- data/docs/modules/apm-aggregation.md +57 -0
- data/docs/modules/configuration.md +2 -0
- data/docs/modules/rails-legacy.md +1 -1
- data/docs/modules/sidekiq-legacy.md +2 -2
- data/docs/modules/telemetry-events.md +1 -1
- data/docs/performance.md +14 -1
- data/docs/privacy-lgpd.md +7 -2
- data/docs/troubleshooting.md +12 -0
- data/lib/chronos/agent.rb +25 -0
- data/lib/chronos/application/apm_aggregator.rb +273 -0
- data/lib/chronos/application/capture_telemetry.rb +45 -5
- data/lib/chronos/application/delivery_pipeline.rb +7 -0
- data/lib/chronos/application/remote_configuration.rb +6 -1
- data/lib/chronos/configuration/apm_validation.rb +60 -0
- data/lib/chronos/configuration.rb +22 -2
- data/lib/chronos/core/metric_aggregate.rb +113 -0
- data/lib/chronos/core/sql_normalizer.rb +114 -0
- data/lib/chronos/core/telemetry_event.rb +1 -1
- data/lib/chronos/integrations/rack/middleware.rb +19 -1
- data/lib/chronos/integrations/sidekiq.rb +2 -0
- data/lib/chronos/rails/notifications_subscriber.rb +36 -4
- data/lib/chronos/version.rb +1 -1
- data/lib/chronos.rb +17 -0
- metadata +8 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
|
|
3
|
+
module Chronos
|
|
4
|
+
module Core
|
|
5
|
+
# Produces bounded, value-free SQL metadata for aggregation and local signals.
|
|
6
|
+
#
|
|
7
|
+
# @responsibility Remove comments/literals and derive operation, table, and fingerprint.
|
|
8
|
+
# @motivation SQL metrics need stable low-cardinality identity without bind values.
|
|
9
|
+
# @limits It is a defensive lexer, not a complete dialect-specific SQL parser.
|
|
10
|
+
# @collaborators Rails notification adapter and APM aggregator.
|
|
11
|
+
# @thread_safety Instances are stateless and safe to share.
|
|
12
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6; no ActiveRecord dependency.
|
|
13
|
+
# @example
|
|
14
|
+
# SqlNormalizer.new.call("SELECT * FROM users WHERE id = 42")
|
|
15
|
+
# @errors Malformed or unreadable input returns bounded unknown metadata.
|
|
16
|
+
# @performance Input and normalized output are capped before fingerprinting.
|
|
17
|
+
class SqlNormalizer
|
|
18
|
+
MAX_SQL_BYTES = 4096
|
|
19
|
+
MAX_NORMALIZED_BYTES = 512
|
|
20
|
+
KEYWORDS = %w(
|
|
21
|
+
select insert update delete from into where join left right inner outer on set values
|
|
22
|
+
and or order group by having limit offset as distinct begin commit rollback savepoint release
|
|
23
|
+
).freeze
|
|
24
|
+
|
|
25
|
+
def call(sql, metadata = {})
|
|
26
|
+
normalized = normalize(sql)
|
|
27
|
+
{
|
|
28
|
+
"adapter" => adapter(metadata), "operation" => operation(normalized),
|
|
29
|
+
"table" => table(normalized), "normalized_query" => normalized,
|
|
30
|
+
"fingerprint" => Digest::SHA256.hexdigest(normalized),
|
|
31
|
+
"name" => value(metadata, :name).to_s,
|
|
32
|
+
"cached" => value(metadata, :cached) == true,
|
|
33
|
+
"role" => optional_string(metadata, :role, :connection_role),
|
|
34
|
+
"shard" => optional_string(metadata, :shard, :connection_shard),
|
|
35
|
+
"source" => bounded(value(metadata, :source).to_s, 256),
|
|
36
|
+
"error_class" => error_class(metadata)
|
|
37
|
+
}.delete_if { |_key, child| child.nil? || child == "" }
|
|
38
|
+
rescue StandardError
|
|
39
|
+
{"operation" => "UNKNOWN", "normalized_query" => "", "fingerprint" => Digest::SHA256.hexdigest("")}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def normalize(sql)
|
|
45
|
+
text = bounded(sql.to_s, MAX_SQL_BYTES)
|
|
46
|
+
text = text.gsub(%r{/\*.*?\*/}m, " ").gsub(/--[^\r\n]*/, " ")
|
|
47
|
+
text = text.gsub(/'(?:''|[^'])*'/, "?")
|
|
48
|
+
text = text.gsub(/\$([A-Za-z_][A-Za-z0-9_]*)?\$.*?\$\1\$/m, "?")
|
|
49
|
+
text = text.gsub(/\b(?:0x[0-9a-f]+|\d+(?:\.\d+)?)\b/i, "?")
|
|
50
|
+
text = text.gsub(/\b(?:true|false|null)\b/i, "?")
|
|
51
|
+
text = text.gsub(/\(\s*\?(?:\s*,\s*\?)+\s*\)/, "(?)")
|
|
52
|
+
text = text.gsub(/\s+/, " ").strip
|
|
53
|
+
KEYWORDS.each { |keyword| text.gsub!(/\b#{keyword}\b/i, keyword.upcase) }
|
|
54
|
+
bounded(text, MAX_NORMALIZED_BYTES)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def operation(normalized)
|
|
58
|
+
candidate = normalized.to_s.split(/\s+/, 2).first.to_s.upcase
|
|
59
|
+
candidate =~ /\A[A-Z]+\z/ ? candidate : "UNKNOWN"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def table(normalized)
|
|
63
|
+
match = normalized.match(/\b(?:FROM|INTO|UPDATE|JOIN)\s+["`\[]?([A-Za-z0-9_.-]+)/i)
|
|
64
|
+
bounded(match && match[1].to_s, 128)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def adapter(metadata)
|
|
68
|
+
direct = value(metadata, :adapter)
|
|
69
|
+
return bounded(direct.to_s, 64) unless direct.to_s.empty?
|
|
70
|
+
|
|
71
|
+
connection = value(metadata, :connection)
|
|
72
|
+
return "" unless connection && connection.respond_to?(:adapter_name)
|
|
73
|
+
|
|
74
|
+
bounded(connection.adapter_name.to_s, 64)
|
|
75
|
+
rescue StandardError
|
|
76
|
+
""
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def error_class(metadata)
|
|
80
|
+
error = value(metadata, :exception_object) || value(metadata, :exception)
|
|
81
|
+
return error.class.name.to_s if error.is_a?(Exception)
|
|
82
|
+
return Array(error).first.to_s unless error.nil?
|
|
83
|
+
|
|
84
|
+
""
|
|
85
|
+
rescue StandardError
|
|
86
|
+
""
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def optional_string(metadata, *names)
|
|
90
|
+
names.each do |name|
|
|
91
|
+
candidate = value(metadata, name)
|
|
92
|
+
return bounded(candidate.to_s, 64) unless candidate.to_s.empty?
|
|
93
|
+
end
|
|
94
|
+
""
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def value(hash, key)
|
|
98
|
+
return nil unless hash.is_a?(Hash)
|
|
99
|
+
|
|
100
|
+
hash.key?(key) ? hash[key] : hash[key.to_s]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def bounded(value, limit)
|
|
104
|
+
text = value.to_s
|
|
105
|
+
text = text.scrub("?") if text.respond_to?(:scrub)
|
|
106
|
+
return text if text.bytesize <= limit
|
|
107
|
+
|
|
108
|
+
text.byteslice(0, limit)
|
|
109
|
+
rescue StandardError
|
|
110
|
+
""
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -16,7 +16,7 @@ module Chronos
|
|
|
16
16
|
# @errors Unsupported types raise ArgumentError during construction.
|
|
17
17
|
# @performance Construction is linear in supplied context and payload size.
|
|
18
18
|
class TelemetryEvent
|
|
19
|
-
TYPES = %w(request query job cache).freeze
|
|
19
|
+
TYPES = %w(request query job cache metric_batch).freeze
|
|
20
20
|
|
|
21
21
|
attr_reader :event_id, :event_type, :timestamp, :context, :payload
|
|
22
22
|
|
|
@@ -38,11 +38,14 @@ module Chronos
|
|
|
38
38
|
response = @app.call(env)
|
|
39
39
|
status = response[0]
|
|
40
40
|
headers = response[1]
|
|
41
|
-
|
|
41
|
+
context = dynamic_request_context(base, status, headers, started_at)
|
|
42
|
+
add_request_breadcrumb("request completed", context)
|
|
43
|
+
record_request_metric(context)
|
|
42
44
|
response
|
|
43
45
|
rescue Exception => error # rubocop:disable Lint/RescueException
|
|
44
46
|
context = dynamic_request_context(base, 500, nil, started_at)
|
|
45
47
|
notify_safely(error, context)
|
|
48
|
+
record_request_metric(context)
|
|
46
49
|
raise
|
|
47
50
|
end
|
|
48
51
|
|
|
@@ -141,6 +144,21 @@ module Chronos
|
|
|
141
144
|
)
|
|
142
145
|
end
|
|
143
146
|
|
|
147
|
+
def record_request_metric(context)
|
|
148
|
+
request = context[:context]["request"]
|
|
149
|
+
payload = {
|
|
150
|
+
"kind" => "rack", "route" => request["route"], "method" => request["method"],
|
|
151
|
+
"status" => request["status"], "duration_ms" => request["duration_ms"]
|
|
152
|
+
}
|
|
153
|
+
if @notifier.respond_to?(:record_event_once)
|
|
154
|
+
@notifier.record_event_once("request", "request", payload)
|
|
155
|
+
elsif @notifier.respond_to?(:record_event)
|
|
156
|
+
@notifier.record_event("request", payload)
|
|
157
|
+
end
|
|
158
|
+
rescue StandardError
|
|
159
|
+
false
|
|
160
|
+
end
|
|
161
|
+
|
|
144
162
|
def hash_value(value)
|
|
145
163
|
value.is_a?(Hash) ? value : {}
|
|
146
164
|
end
|
|
@@ -176,6 +176,8 @@ module Chronos
|
|
|
176
176
|
propagated = {} unless propagated.is_a?(Hash)
|
|
177
177
|
{
|
|
178
178
|
:context => propagated.merge("job" => job_context(payload)),
|
|
179
|
+
:parameters => {"arguments" => payload["arguments"]},
|
|
180
|
+
:tags => payload["tags"],
|
|
179
181
|
:__chronos_captured_exceptions => {}
|
|
180
182
|
}
|
|
181
183
|
end
|
|
@@ -29,6 +29,7 @@ module Chronos
|
|
|
29
29
|
def initialize(notifier = Chronos, notifications = nil)
|
|
30
30
|
@notifier = notifier
|
|
31
31
|
@notifications = notifications || active_support_notifications
|
|
32
|
+
@sql_normalizer = Core::SqlNormalizer.new
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
def install
|
|
@@ -105,7 +106,7 @@ module Chronos
|
|
|
105
106
|
"route" => route(payload),
|
|
106
107
|
"duration_ms" => duration, "parameters" => hash(value(payload, :params))
|
|
107
108
|
}
|
|
108
|
-
|
|
109
|
+
record_once("request", "request", data)
|
|
109
110
|
end
|
|
110
111
|
|
|
111
112
|
def render_template(payload, duration)
|
|
@@ -117,10 +118,15 @@ module Chronos
|
|
|
117
118
|
end
|
|
118
119
|
|
|
119
120
|
def sql(payload, duration)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
metadata = {
|
|
122
|
+
:name => value(payload, :name), :cached => value(payload, :cached),
|
|
123
|
+
:adapter => value(payload, :adapter), :connection => value(payload, :connection),
|
|
124
|
+
:role => value(payload, :connection_role) || value(payload, :role),
|
|
125
|
+
:shard => value(payload, :connection_shard) || value(payload, :shard),
|
|
126
|
+
:exception_object => value(payload, :exception_object), :exception => value(payload, :exception)
|
|
123
127
|
}
|
|
128
|
+
metadata[:source] = sampled_query_source if duration >= slow_query_threshold
|
|
129
|
+
data = @sql_normalizer.call(value(payload, :sql), metadata).merge("duration_ms" => duration)
|
|
124
130
|
@notifier.record_event("query", data)
|
|
125
131
|
end
|
|
126
132
|
|
|
@@ -198,6 +204,32 @@ module Chronos
|
|
|
198
204
|
rescue StandardError
|
|
199
205
|
""
|
|
200
206
|
end
|
|
207
|
+
|
|
208
|
+
def record_once(key, event_type, payload)
|
|
209
|
+
if @notifier.respond_to?(:record_event_once)
|
|
210
|
+
@notifier.record_event_once(key, event_type, payload)
|
|
211
|
+
else
|
|
212
|
+
@notifier.record_event(event_type, payload)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def slow_query_threshold
|
|
217
|
+
options = @notifier.respond_to?(:apm_integration_options) ? @notifier.apm_integration_options : {}
|
|
218
|
+
(options[:slow_query_threshold_ms] || 500.0).to_f
|
|
219
|
+
rescue StandardError
|
|
220
|
+
500.0
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def sampled_query_source
|
|
224
|
+
options = @notifier.respond_to?(:apm_integration_options) ? @notifier.apm_integration_options : {}
|
|
225
|
+
root = options[:root_directory].to_s
|
|
226
|
+
frame = caller.find do |line|
|
|
227
|
+
(root.empty? || line.start_with?(root)) && line !~ %r{/lib/chronos/}
|
|
228
|
+
end
|
|
229
|
+
frame.to_s.sub(/:in .*/, "")[0, 256]
|
|
230
|
+
rescue StandardError
|
|
231
|
+
""
|
|
232
|
+
end
|
|
201
233
|
end
|
|
202
234
|
end
|
|
203
235
|
end
|
data/lib/chronos/version.rb
CHANGED
data/lib/chronos.rb
CHANGED
|
@@ -16,6 +16,8 @@ require "chronos/core/sanitizer"
|
|
|
16
16
|
require "chronos/core/safe_serializer"
|
|
17
17
|
require "chronos/core/payload_serializer"
|
|
18
18
|
require "chronos/core/telemetry_event"
|
|
19
|
+
require "chronos/core/sql_normalizer"
|
|
20
|
+
require "chronos/core/metric_aggregate"
|
|
19
21
|
require "chronos/ports/transport"
|
|
20
22
|
require "chronos/ports/context_store"
|
|
21
23
|
require "chronos/internal/safe_logger"
|
|
@@ -30,6 +32,7 @@ require "chronos/application/circuit_breaker"
|
|
|
30
32
|
require "chronos/application/remote_configuration"
|
|
31
33
|
require "chronos/application/delivery_pipeline"
|
|
32
34
|
require "chronos/application/capture_exception"
|
|
35
|
+
require "chronos/application/apm_aggregator"
|
|
33
36
|
require "chronos/application/capture_telemetry"
|
|
34
37
|
require "chronos/agent"
|
|
35
38
|
require "chronos/integrations"
|
|
@@ -104,6 +107,20 @@ module Chronos
|
|
|
104
107
|
false
|
|
105
108
|
end
|
|
106
109
|
|
|
110
|
+
def record_event_once(key, event_type, payload = {}, context = {})
|
|
111
|
+
agent = current_agent
|
|
112
|
+
agent ? agent.record_event_once(key, event_type, payload, context) : false
|
|
113
|
+
rescue StandardError
|
|
114
|
+
false
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def apm_integration_options
|
|
118
|
+
agent = current_agent
|
|
119
|
+
agent ? agent.apm_integration_options : {:enabled => false}
|
|
120
|
+
rescue StandardError
|
|
121
|
+
{:enabled => false}
|
|
122
|
+
end
|
|
123
|
+
|
|
107
124
|
# Returns only trace/request identifiers for optional process-boundary adapters.
|
|
108
125
|
def propagation_context
|
|
109
126
|
agent = current_agent
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: chronos-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0.pre.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Antonio Jefferson
|
|
@@ -86,6 +86,7 @@ files:
|
|
|
86
86
|
- LICENSE.txt
|
|
87
87
|
- README.md
|
|
88
88
|
- SECURITY.md
|
|
89
|
+
- contracts/apm-batch-v1.schema.json
|
|
89
90
|
- contracts/event-v1.schema.json
|
|
90
91
|
- contracts/rack-context-v1.schema.json
|
|
91
92
|
- contracts/sidekiq-job-v1.schema.json
|
|
@@ -99,11 +100,13 @@ files:
|
|
|
99
100
|
- docs/adr/ADR-012-rack-context-isolation.md
|
|
100
101
|
- docs/adr/ADR-013-legacy-rails-notifications.md
|
|
101
102
|
- docs/adr/ADR-014-sidekiq-envelope-context.md
|
|
103
|
+
- docs/adr/ADR-015-bounded-apm-aggregation.md
|
|
102
104
|
- docs/architecture.md
|
|
103
105
|
- docs/compatibility.md
|
|
104
106
|
- docs/configuration.md
|
|
105
107
|
- docs/data-collected.md
|
|
106
108
|
- docs/examples/plain-ruby.md
|
|
109
|
+
- docs/modules/apm-aggregation.md
|
|
107
110
|
- docs/modules/async-queue.md
|
|
108
111
|
- docs/modules/configuration.md
|
|
109
112
|
- docs/modules/notice-pipeline.md
|
|
@@ -125,6 +128,7 @@ files:
|
|
|
125
128
|
- lib/chronos/adapters/thread_local_context_store.rb
|
|
126
129
|
- lib/chronos/agent.rb
|
|
127
130
|
- lib/chronos/application.rb
|
|
131
|
+
- lib/chronos/application/apm_aggregator.rb
|
|
128
132
|
- lib/chronos/application/capture_exception.rb
|
|
129
133
|
- lib/chronos/application/capture_telemetry.rb
|
|
130
134
|
- lib/chronos/application/circuit_breaker.rb
|
|
@@ -132,12 +136,14 @@ files:
|
|
|
132
136
|
- lib/chronos/application/remote_configuration.rb
|
|
133
137
|
- lib/chronos/application/retry_policy.rb
|
|
134
138
|
- lib/chronos/configuration.rb
|
|
139
|
+
- lib/chronos/configuration/apm_validation.rb
|
|
135
140
|
- lib/chronos/configuration/snapshot.rb
|
|
136
141
|
- lib/chronos/configuration/validation.rb
|
|
137
142
|
- lib/chronos/core.rb
|
|
138
143
|
- lib/chronos/core/backtrace_parser.rb
|
|
139
144
|
- lib/chronos/core/breadcrumb.rb
|
|
140
145
|
- lib/chronos/core/exception_cause_collector.rb
|
|
146
|
+
- lib/chronos/core/metric_aggregate.rb
|
|
141
147
|
- lib/chronos/core/notice.rb
|
|
142
148
|
- lib/chronos/core/notice_builder.rb
|
|
143
149
|
- lib/chronos/core/payload_serializer.rb
|
|
@@ -145,6 +151,7 @@ files:
|
|
|
145
151
|
- lib/chronos/core/safe_serializer.rb
|
|
146
152
|
- lib/chronos/core/sanitizer.rb
|
|
147
153
|
- lib/chronos/core/sensitive_value_filter.rb
|
|
154
|
+
- lib/chronos/core/sql_normalizer.rb
|
|
148
155
|
- lib/chronos/core/telemetry_event.rb
|
|
149
156
|
- lib/chronos/errors.rb
|
|
150
157
|
- lib/chronos/integrations.rb
|