chronos-ruby 0.9.0.pre.3 → 1.0.0
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 +34 -0
- data/README.md +101 -181
- data/contracts/integration-verification-response-v1.schema.json +76 -0
- data/docs/adr/ADR-007-feature-detection.md +25 -0
- data/docs/adr/ADR-008-context-store.md +25 -0
- data/docs/adr/ADR-009-sampling.md +25 -0
- data/docs/adr/ADR-010-opentelemetry-interoperability.md +25 -0
- data/docs/adr/ADR-018-pre-1.0-hardening.md +6 -2
- data/docs/compatibility.md +30 -23
- data/docs/data-collected.md +3 -0
- data/docs/deprecation-policy.md +1 -1
- data/docs/migration-from-airbrake.md +1 -1
- data/docs/modules/breadcrumbs.md +23 -0
- data/docs/modules/context.md +21 -0
- data/docs/modules/deploys.md +23 -0
- data/docs/modules/integration-verification.md +65 -0
- data/docs/modules/job-monitoring.md +22 -0
- data/docs/modules/request-monitoring.md +20 -0
- data/docs/modules/runtime-metrics.md +22 -0
- data/docs/modules/sampling.md +22 -0
- data/docs/modules/sidekiq-legacy.md +1 -1
- data/docs/modules/sql-monitoring.md +22 -0
- data/docs/performance.md +13 -2
- data/docs/protocol-v1.md +3 -1
- data/docs/release-1.0-readiness.md +17 -15
- data/docs/security-review.md +4 -3
- data/docs/troubleshooting.md +6 -0
- data/lib/chronos/adapters/net_http_transport.rb +36 -2
- data/lib/chronos/agent.rb +21 -0
- data/lib/chronos/application/delivery_pipeline.rb +6 -3
- data/lib/chronos/application/verify_integration.rb +262 -0
- data/lib/chronos/core/integration_verification_result.rb +108 -0
- data/lib/chronos/errors.rb +11 -0
- data/lib/chronos/ports/transport.rb +18 -1
- data/lib/chronos/rails/railtie.rb +5 -0
- data/lib/chronos/rake_tasks.rb +41 -0
- data/lib/chronos/version.rb +1 -1
- data/lib/chronos.rb +26 -0
- metadata +35 -4
|
@@ -22,6 +22,7 @@ module Chronos
|
|
|
22
22
|
class NetHttpTransport
|
|
23
23
|
EVENT_PATH = "/api/v1/events".freeze
|
|
24
24
|
REMOTE_CONFIGURATION_HEADER = "X-Chronos-Remote-Configuration".freeze
|
|
25
|
+
RESPONSE_BODY_MAX_BYTES = 8192
|
|
25
26
|
|
|
26
27
|
def initialize(config, logger = nil)
|
|
27
28
|
@config = config
|
|
@@ -71,7 +72,11 @@ module Chronos
|
|
|
71
72
|
request["Idempotency-Key"] = event.event_id
|
|
72
73
|
request.body = request_body(event.body)
|
|
73
74
|
request["Content-Encoding"] = "gzip" if @config.gzip
|
|
74
|
-
http.start
|
|
75
|
+
http.start do |connection|
|
|
76
|
+
connection.request(request) do |response|
|
|
77
|
+
read_bounded_response(response)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
75
80
|
end
|
|
76
81
|
|
|
77
82
|
def endpoint_uri
|
|
@@ -101,7 +106,7 @@ module Chronos
|
|
|
101
106
|
|
|
102
107
|
def classify(response)
|
|
103
108
|
code = response.code.to_i
|
|
104
|
-
options = {:status_code => code}
|
|
109
|
+
options = {:status_code => code, :response => parse_response(response)}
|
|
105
110
|
if code >= 200 && code < 300
|
|
106
111
|
options[:remote_configuration] = parse_remote_configuration(response)
|
|
107
112
|
return Ports::TransportResult.new(:success, options)
|
|
@@ -116,6 +121,35 @@ module Chronos
|
|
|
116
121
|
Ports::TransportResult.new(:client_error, options)
|
|
117
122
|
end
|
|
118
123
|
|
|
124
|
+
def read_bounded_response(response)
|
|
125
|
+
body = ""
|
|
126
|
+
overflow = false
|
|
127
|
+
response.read_body do |chunk|
|
|
128
|
+
next if overflow
|
|
129
|
+
|
|
130
|
+
remaining = RESPONSE_BODY_MAX_BYTES - body.bytesize
|
|
131
|
+
if chunk.bytesize > remaining
|
|
132
|
+
overflow = true
|
|
133
|
+
else
|
|
134
|
+
body << chunk
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
response.instance_variable_set(:@chronos_response_body, overflow ? nil : body)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def parse_response(response)
|
|
141
|
+
content_type = response["Content-Type"].to_s.split(";", 2).first
|
|
142
|
+
return nil unless content_type == "application/json"
|
|
143
|
+
|
|
144
|
+
body = response.instance_variable_get(:@chronos_response_body)
|
|
145
|
+
return nil if body.nil? || body.empty?
|
|
146
|
+
|
|
147
|
+
parsed = JSON.parse(body)
|
|
148
|
+
parsed.is_a?(Hash) ? parsed : nil
|
|
149
|
+
rescue JSON::ParserError, EncodingError
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
119
153
|
def parse_remote_configuration(response)
|
|
120
154
|
return nil unless @config.remote_configuration
|
|
121
155
|
|
data/lib/chronos/agent.rb
CHANGED
|
@@ -50,6 +50,24 @@ module Chronos
|
|
|
50
50
|
@capture.call_sync(exception, context_for_capture(context))
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
def verify_integration
|
|
54
|
+
@verification.call
|
|
55
|
+
rescue StandardError => error
|
|
56
|
+
@logger.warn("Chronos integration verification failed: #{error.class}")
|
|
57
|
+
Core::IntegrationVerificationResult.new(
|
|
58
|
+
:success => false,
|
|
59
|
+
:status => "verification_failed",
|
|
60
|
+
:credentials_valid => nil,
|
|
61
|
+
:event => {"id" => nil, "received" => false},
|
|
62
|
+
:receiver => {"name" => "chronos", "status" => "not_checked", "received_at" => nil},
|
|
63
|
+
:error => {
|
|
64
|
+
"code" => "verification_failed",
|
|
65
|
+
"message" => "Chronos integration verification failed locally.",
|
|
66
|
+
"guidance" => "Review the Chronos configuration and retry."
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
53
71
|
def ignore_if(&block)
|
|
54
72
|
@ignore_policy.add(&block)
|
|
55
73
|
end
|
|
@@ -202,6 +220,9 @@ module Chronos
|
|
|
202
220
|
def initialize_observability(options)
|
|
203
221
|
@dependency_reporter = options[:dependency_reporter] || Application::DependencyReporter.new(@config)
|
|
204
222
|
@deploy_normalizer = options[:deploy_normalizer] || Core::DeployNormalizer.new(@config)
|
|
223
|
+
@verification = options[:verification] || Application::VerifyIntegration.new(
|
|
224
|
+
@config, @delivery_pipeline, @logger
|
|
225
|
+
)
|
|
205
226
|
end
|
|
206
227
|
|
|
207
228
|
def build_context_store(strategy)
|
|
@@ -42,13 +42,16 @@ module Chronos
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def deliver_sync(event)
|
|
45
|
+
deliver_sync_result(event).success?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def deliver_sync_result(event)
|
|
45
49
|
record_pre_delivery(event)
|
|
46
|
-
|
|
47
|
-
result && result.success?
|
|
50
|
+
deliver_with_backlog(event)
|
|
48
51
|
rescue StandardError => error
|
|
49
52
|
diagnose(error)
|
|
50
53
|
store_in_backlog(event)
|
|
51
|
-
|
|
54
|
+
Ports::TransportResult.new(:network_error, :error => error.class.name)
|
|
52
55
|
end
|
|
53
56
|
|
|
54
57
|
# WorkerPool delivery entry point. Events were counted when accepted into the queue.
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
module Chronos
|
|
4
|
+
module Application
|
|
5
|
+
# Sends and verifies one synthetic exception against the configured Chronos receiver.
|
|
6
|
+
#
|
|
7
|
+
# @responsibility Build an identified test notice, deliver it synchronously, and validate its acknowledgement.
|
|
8
|
+
# @motivation Verify credentials and end-to-end ingestion without mistaking an empty 2xx for confirmation.
|
|
9
|
+
# @limits It exposes only allowlisted project/receiver fields and never returns raw server responses.
|
|
10
|
+
# @collaborators NoticeBuilder, PayloadSerializer, DeliveryPipeline, and IntegrationVerificationResult.
|
|
11
|
+
# @thread_safety Calls allocate independent IDs and immutable results; collaborators synchronize delivery.
|
|
12
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6; independent of Rails.
|
|
13
|
+
# @example
|
|
14
|
+
# result = verifier.call
|
|
15
|
+
# result.success? #=> true or false
|
|
16
|
+
# @errors Network, protocol, configuration, and receiver failures become structured results.
|
|
17
|
+
# @performance Performs one bounded synchronous verification plus configured bounded retries.
|
|
18
|
+
class VerifyIntegration
|
|
19
|
+
SCHEMA_VERSION = "1.0".freeze
|
|
20
|
+
VERIFICATION_KIND = "integration_verification".freeze
|
|
21
|
+
VERIFICATION_TAG = "chronos-integration-verification".freeze
|
|
22
|
+
|
|
23
|
+
def initialize(config, delivery_pipeline, logger = nil, options = {})
|
|
24
|
+
@config = config
|
|
25
|
+
@delivery_pipeline = delivery_pipeline
|
|
26
|
+
@logger = logger || Internal::SafeLogger.new(config.logger)
|
|
27
|
+
@uuid_generator = options[:uuid_generator] || proc { SecureRandom.uuid }
|
|
28
|
+
@notice_builder = options[:notice_builder] || Core::NoticeBuilder.new(config)
|
|
29
|
+
@serializer = options[:serializer] || Core::PayloadSerializer.new(
|
|
30
|
+
config,
|
|
31
|
+
nil,
|
|
32
|
+
:max_payload_size => proc { @delivery_pipeline.max_payload_size }
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def call
|
|
37
|
+
verification_id = bounded(@uuid_generator.call, 128)
|
|
38
|
+
return local_failure("configuration_invalid", verification_id, configuration_guidance) unless configured?
|
|
39
|
+
|
|
40
|
+
event = build_event(verification_id)
|
|
41
|
+
transport_result = @delivery_pipeline.deliver_sync_result(event)
|
|
42
|
+
classify(transport_result, verification_id, event.event_id)
|
|
43
|
+
rescue StandardError => error
|
|
44
|
+
@logger.warn("Chronos integration verification failed: #{error.class}")
|
|
45
|
+
local_failure("verification_failed", verification_id, "Review the Chronos configuration and retry.")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def configured?
|
|
51
|
+
!@config.project_id.to_s.empty? && !@config.project_key.to_s.empty? && !@config.host.to_s.empty?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def build_event(verification_id)
|
|
55
|
+
marker = {
|
|
56
|
+
"schema_version" => SCHEMA_VERSION,
|
|
57
|
+
"verification_id" => verification_id,
|
|
58
|
+
"kind" => VERIFICATION_KIND,
|
|
59
|
+
"test" => true
|
|
60
|
+
}
|
|
61
|
+
notice = @notice_builder.call(
|
|
62
|
+
IntegrationVerificationError.new("Chronos integration verification test"),
|
|
63
|
+
:severity => "info",
|
|
64
|
+
:context => {"integration_verification" => marker},
|
|
65
|
+
:tags => [VERIFICATION_TAG],
|
|
66
|
+
:fingerprint => VERIFICATION_TAG
|
|
67
|
+
)
|
|
68
|
+
@serializer.call(notice)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def classify(result, verification_id, event_id)
|
|
72
|
+
return classify_success(result.response, verification_id, event_id) if result.success?
|
|
73
|
+
if inactive_response?(result, verification_id, event_id)
|
|
74
|
+
return project_inactive(result.response, verification_id, event_id)
|
|
75
|
+
end
|
|
76
|
+
return invalid_credentials(verification_id, event_id) if [401, 403].include?(result.status_code)
|
|
77
|
+
return rate_limited(verification_id, event_id) if result.status == :rate_limited
|
|
78
|
+
return receiver_unavailable(verification_id, event_id) if unavailable?(result)
|
|
79
|
+
return receiver_internal_error(verification_id, event_id) if result.status == :server_error
|
|
80
|
+
|
|
81
|
+
request_rejected(verification_id, event_id)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def classify_success(response, verification_id, event_id)
|
|
85
|
+
unless valid_acknowledgement?(response, verification_id, event_id)
|
|
86
|
+
return failure("invalid_response", verification_id, event_id,
|
|
87
|
+
:receiver_status => "reachable",
|
|
88
|
+
:message => "Chronos returned a response outside the verification contract.",
|
|
89
|
+
:guidance => "Update Chronos to the integration verification response v1 contract.")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
Core::IntegrationVerificationResult.new(
|
|
93
|
+
:success => true,
|
|
94
|
+
:status => "verified",
|
|
95
|
+
:verification_id => verification_id,
|
|
96
|
+
:credentials_valid => true,
|
|
97
|
+
:event => {"id" => event_id, "received" => true},
|
|
98
|
+
:project => safe_project(response["project"]),
|
|
99
|
+
:receiver => safe_receiver(response["receiver"]),
|
|
100
|
+
:error => nil
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def valid_acknowledgement?(response, verification_id, event_id)
|
|
105
|
+
valid_response_structure?(response) &&
|
|
106
|
+
acknowledgement_matches?(response, verification_id, event_id) &&
|
|
107
|
+
active_project?(response["project"]) && operational_receiver?(response["receiver"])
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def valid_response_structure?(response)
|
|
111
|
+
exact_keys?(response, %w(
|
|
112
|
+
schema_version success status verification_id credentials_valid event_received
|
|
113
|
+
event project receiver error
|
|
114
|
+
)) &&
|
|
115
|
+
exact_keys?(response["event"], %w(id)) &&
|
|
116
|
+
exact_keys?(response["project"], %w(id name status environment)) &&
|
|
117
|
+
exact_keys?(response["receiver"], %w(name status received_at))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def acknowledgement_matches?(response, verification_id, event_id)
|
|
121
|
+
response["schema_version"] == SCHEMA_VERSION && response["verification_id"] == verification_id &&
|
|
122
|
+
response["status"] == "accepted" && response["success"] == true &&
|
|
123
|
+
response["credentials_valid"] == true && response["event_received"] == true &&
|
|
124
|
+
hash_value(response["event"])["id"] == event_id && response["error"].nil?
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def active_project?(project)
|
|
128
|
+
hash_value(project)["id"] == @config.project_id.to_s && hash_value(project)["status"] == "active"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def operational_receiver?(receiver)
|
|
132
|
+
hash_value(receiver)["status"] == "operational"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def inactive_response?(result, verification_id, event_id)
|
|
136
|
+
response = result.response
|
|
137
|
+
result.status_code == 403 && valid_response_structure?(response) &&
|
|
138
|
+
inactive_state?(response) && inactive_correlation?(response, verification_id, event_id)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def inactive_state?(response)
|
|
142
|
+
response["schema_version"] == SCHEMA_VERSION && response["success"] == false &&
|
|
143
|
+
response["status"] == "project_inactive" && response["credentials_valid"] == true &&
|
|
144
|
+
response["event_received"] == false && hash_value(response["project"])["status"] == "inactive" &&
|
|
145
|
+
valid_error?(response["error"])
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def inactive_correlation?(response, verification_id, event_id)
|
|
149
|
+
response["verification_id"] == verification_id && hash_value(response["event"])["id"] == event_id &&
|
|
150
|
+
hash_value(response["project"])["id"] == @config.project_id.to_s
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def valid_error?(error)
|
|
154
|
+
exact_keys?(error, %w(code message guidance)) && error["code"] == "project_inactive"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def project_inactive(response, verification_id, event_id)
|
|
158
|
+
failure("project_inactive", verification_id, event_id,
|
|
159
|
+
:credentials_valid => true, :receiver_status => "reachable",
|
|
160
|
+
:message => "The Chronos project is inactive and did not accept the verification event.",
|
|
161
|
+
:guidance => "Activate the project in Chronos or select an active project, then retry.",
|
|
162
|
+
:project => safe_project(response["project"]))
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def invalid_credentials(verification_id, event_id)
|
|
166
|
+
failure("invalid_credentials", verification_id, event_id,
|
|
167
|
+
:credentials_valid => false, :receiver_status => "reachable",
|
|
168
|
+
:message => "Chronos rejected the project credentials.",
|
|
169
|
+
:guidance => "Create an active project API key, then confirm project_id and project_key.")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def rate_limited(verification_id, event_id)
|
|
173
|
+
failure("rate_limited", verification_id, event_id,
|
|
174
|
+
:receiver_status => "reachable",
|
|
175
|
+
:message => "Chronos temporarily rate limited the verification request.",
|
|
176
|
+
:guidance => "Wait before retrying and review the project ingestion limits.")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def receiver_internal_error(verification_id, event_id)
|
|
180
|
+
failure("receiver_internal_error", verification_id, event_id,
|
|
181
|
+
:receiver_status => "error",
|
|
182
|
+
:message => "Chronos encountered an internal error while processing the verification.",
|
|
183
|
+
:guidance => "Retry later or contact the Chronos operator with the verification_id.")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def receiver_unavailable(verification_id, event_id)
|
|
187
|
+
failure("receiver_unavailable", verification_id, event_id,
|
|
188
|
+
:receiver_status => "unavailable",
|
|
189
|
+
:message => "The Chronos receiver is unavailable or could not be reached.",
|
|
190
|
+
:guidance => "Check the host, DNS, TLS, network access, and Chronos service status.")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def request_rejected(verification_id, event_id)
|
|
194
|
+
failure("request_rejected", verification_id, event_id,
|
|
195
|
+
:receiver_status => "reachable",
|
|
196
|
+
:message => "Chronos rejected the integration verification request.",
|
|
197
|
+
:guidance => "Confirm that the gem and Chronos support the verification v1 contract.")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def unavailable?(result)
|
|
201
|
+
[:network_error, :request_timeout, :circuit_open].include?(result.status) ||
|
|
202
|
+
[502, 503, 504].include?(result.status_code)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def failure(status, verification_id, event_id, options = {})
|
|
206
|
+
Core::IntegrationVerificationResult.new(
|
|
207
|
+
:success => false,
|
|
208
|
+
:status => status,
|
|
209
|
+
:verification_id => verification_id,
|
|
210
|
+
:credentials_valid => options[:credentials_valid],
|
|
211
|
+
:event => {"id" => event_id, "received" => false},
|
|
212
|
+
:project => options[:project],
|
|
213
|
+
:receiver => {"name" => "chronos", "status" => options[:receiver_status], "received_at" => nil},
|
|
214
|
+
:error => {"code" => status, "message" => options[:message], "guidance" => options[:guidance]}
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def local_failure(status, verification_id, guidance)
|
|
219
|
+
failure(status, verification_id, nil,
|
|
220
|
+
:receiver_status => "not_checked",
|
|
221
|
+
:message => "Chronos integration verification could not be started.",
|
|
222
|
+
:guidance => guidance)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def configuration_guidance
|
|
226
|
+
"Configure a non-empty project_id, project_key, and HTTPS host before running verification."
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def safe_project(value)
|
|
230
|
+
safe_fields(value, "id" => 128, "name" => 128, "status" => 32, "environment" => 128)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def safe_receiver(value)
|
|
234
|
+
safe_fields(value, "name" => 64, "status" => 32, "received_at" => 64)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def safe_fields(value, fields)
|
|
238
|
+
source = hash_value(value)
|
|
239
|
+
fields.each_with_object({}) do |(name, limit), result|
|
|
240
|
+
result[name] = source[name].nil? ? nil : bounded(source[name], limit)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def hash_value(value)
|
|
245
|
+
value.is_a?(Hash) ? value : {}
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def exact_keys?(value, keys)
|
|
249
|
+
value.is_a?(Hash) && value.keys.sort == keys.sort
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def bounded(value, limit)
|
|
253
|
+
text = value.to_s.encode("UTF-8", :invalid => :replace, :undef => :replace, :replace => "?")
|
|
254
|
+
return text if text.bytesize <= limit
|
|
255
|
+
|
|
256
|
+
text.byteslice(0, limit).to_s.encode("UTF-8", :invalid => :replace, :undef => :replace, :replace => "?")
|
|
257
|
+
rescue StandardError
|
|
258
|
+
""
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Chronos
|
|
4
|
+
module Core
|
|
5
|
+
# Immutable result returned by an explicit Chronos integration verification.
|
|
6
|
+
#
|
|
7
|
+
# @responsibility Expose a bounded, JSON-safe verification outcome to Ruby and Rake callers.
|
|
8
|
+
# @motivation Report credential, receiver, and acknowledgement state without exposing raw responses.
|
|
9
|
+
# @limits It never includes project keys, response bodies, stack traces, or receiver internals.
|
|
10
|
+
# @collaborators VerifyIntegration and Chronos::RakeTasks.
|
|
11
|
+
# @thread_safety The object and all nested values are immutable after construction.
|
|
12
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6; independent of Rails.
|
|
13
|
+
# @example
|
|
14
|
+
# result = Chronos.verify_integration
|
|
15
|
+
# puts result.to_json
|
|
16
|
+
# @errors Invalid input is normalized to safe empty values.
|
|
17
|
+
# @performance Construction visits only the small allowlisted verification response.
|
|
18
|
+
class IntegrationVerificationResult
|
|
19
|
+
SCHEMA_VERSION = "1.0".freeze
|
|
20
|
+
|
|
21
|
+
attr_reader :status, :verification_id, :credentials_valid, :event,
|
|
22
|
+
:project, :receiver, :error
|
|
23
|
+
|
|
24
|
+
def initialize(attributes = {})
|
|
25
|
+
@success = attributes[:success] == true
|
|
26
|
+
@status = safe_string(attributes[:status], 64)
|
|
27
|
+
@verification_id = optional_string(attributes[:verification_id], 128)
|
|
28
|
+
@credentials_valid = boolean_or_nil(attributes[:credentials_valid])
|
|
29
|
+
@event = immutable_copy(hash_value(attributes[:event]))
|
|
30
|
+
@project = optional_hash(attributes[:project])
|
|
31
|
+
@receiver = optional_hash(attributes[:receiver])
|
|
32
|
+
@error = optional_hash(attributes[:error])
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def success?
|
|
37
|
+
@success
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_h
|
|
41
|
+
{
|
|
42
|
+
"schema_version" => SCHEMA_VERSION,
|
|
43
|
+
"success" => success?,
|
|
44
|
+
"status" => status,
|
|
45
|
+
"verification_id" => verification_id,
|
|
46
|
+
"credentials_valid" => credentials_valid,
|
|
47
|
+
"event_received" => event["received"] == true,
|
|
48
|
+
"event" => event,
|
|
49
|
+
"project" => project,
|
|
50
|
+
"receiver" => receiver,
|
|
51
|
+
"error" => error
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_json(*arguments)
|
|
56
|
+
JSON.generate(to_h, *arguments)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def optional_hash(value)
|
|
62
|
+
value.is_a?(Hash) ? immutable_copy(value) : nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def hash_value(value)
|
|
66
|
+
value.is_a?(Hash) ? value : {}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def boolean_or_nil(value)
|
|
70
|
+
[true, false].include?(value) ? value : nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def optional_string(value, limit)
|
|
74
|
+
return nil if value.nil?
|
|
75
|
+
|
|
76
|
+
safe_string(value, limit)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def safe_string(value, limit)
|
|
80
|
+
text = value.is_a?(String) || value.is_a?(Symbol) ? value.to_s : ""
|
|
81
|
+
text = text.encode("UTF-8", :invalid => :replace, :undef => :replace, :replace => "?")
|
|
82
|
+
return text if text.bytesize <= limit
|
|
83
|
+
|
|
84
|
+
text.byteslice(0, limit).to_s.encode("UTF-8", :invalid => :replace, :undef => :replace, :replace => "?")
|
|
85
|
+
rescue StandardError
|
|
86
|
+
""
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def immutable_copy(value)
|
|
90
|
+
copy = case value
|
|
91
|
+
when Hash
|
|
92
|
+
value.each_with_object({}) do |(key, child), result|
|
|
93
|
+
result[safe_string(key, 64)] = immutable_copy(child)
|
|
94
|
+
end
|
|
95
|
+
when Array
|
|
96
|
+
value.first(20).map { |child| immutable_copy(child) }
|
|
97
|
+
when String
|
|
98
|
+
safe_string(value, 512)
|
|
99
|
+
when NilClass, TrueClass, FalseClass, Numeric
|
|
100
|
+
value
|
|
101
|
+
else
|
|
102
|
+
""
|
|
103
|
+
end
|
|
104
|
+
copy.freeze
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
data/lib/chronos/errors.rb
CHANGED
|
@@ -21,4 +21,15 @@ module Chronos
|
|
|
21
21
|
# @example
|
|
22
22
|
# Chronos.configure { |config| config.project_key = nil }
|
|
23
23
|
class ConfigurationError < Error; end
|
|
24
|
+
|
|
25
|
+
# Synthetic exception sent only by an explicit integration verification.
|
|
26
|
+
#
|
|
27
|
+
# @responsibility Give verification notices a stable, recognizable exception class.
|
|
28
|
+
# @motivation Let the Chronos receiver distinguish an integration check from an application failure.
|
|
29
|
+
# @limits It is never raised into the host application and carries no application data.
|
|
30
|
+
# @thread_safety A fresh instance is created for each verification.
|
|
31
|
+
# @compatibility Ruby 2.2.10 and newer legacy runtimes.
|
|
32
|
+
# @example
|
|
33
|
+
# Chronos.verify_integration
|
|
34
|
+
class IntegrationVerificationError < Error; end
|
|
24
35
|
end
|
|
@@ -8,7 +8,7 @@ module Chronos
|
|
|
8
8
|
# @thread_safety Immutable after construction.
|
|
9
9
|
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
10
10
|
class TransportResult
|
|
11
|
-
attr_reader :status, :status_code, :retry_after, :error, :remote_configuration
|
|
11
|
+
attr_reader :status, :status_code, :retry_after, :error, :remote_configuration, :response
|
|
12
12
|
|
|
13
13
|
def initialize(status, options = {})
|
|
14
14
|
@status = status
|
|
@@ -16,6 +16,7 @@ module Chronos
|
|
|
16
16
|
@retry_after = options[:retry_after]
|
|
17
17
|
@error = options[:error]
|
|
18
18
|
@remote_configuration = options[:remote_configuration]
|
|
19
|
+
@response = deep_freeze(options[:response])
|
|
19
20
|
freeze
|
|
20
21
|
end
|
|
21
22
|
|
|
@@ -26,6 +27,22 @@ module Chronos
|
|
|
26
27
|
def retryable?
|
|
27
28
|
[:request_timeout, :rate_limited, :server_error, :network_error].include?(status)
|
|
28
29
|
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def deep_freeze(value)
|
|
34
|
+
if value.is_a?(Hash)
|
|
35
|
+
value.each do |key, child|
|
|
36
|
+
deep_freeze(key)
|
|
37
|
+
deep_freeze(child)
|
|
38
|
+
end
|
|
39
|
+
elsif value.is_a?(Array)
|
|
40
|
+
value.each { |child| deep_freeze(child) }
|
|
41
|
+
end
|
|
42
|
+
value.freeze
|
|
43
|
+
rescue StandardError
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
29
46
|
end
|
|
30
47
|
|
|
31
48
|
# Conceptual transport port implemented by delivery adapters.
|
|
@@ -16,6 +16,11 @@ module Chronos
|
|
|
16
16
|
initializer "chronos.install", :after => :load_config_initializers do |application|
|
|
17
17
|
Chronos::Rails::Installer.new.install(application)
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
rake_tasks do
|
|
21
|
+
require "chronos/rake_tasks"
|
|
22
|
+
Chronos::RakeTasks.install(:load_environment => true)
|
|
23
|
+
end
|
|
19
24
|
end
|
|
20
25
|
end
|
|
21
26
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "rake"
|
|
3
|
+
require "chronos"
|
|
4
|
+
|
|
5
|
+
module Chronos
|
|
6
|
+
# Installs explicit Rake commands supplied by the Chronos gem.
|
|
7
|
+
#
|
|
8
|
+
# @responsibility Register the integration verification task with deterministic JSON output.
|
|
9
|
+
# @motivation Give Rails and plain Ruby operators one repeatable end-to-end credential check.
|
|
10
|
+
# @limits It never reads environment variables, prints credentials, or modifies application data.
|
|
11
|
+
# @collaborators Rake and Chronos.verify_integration.
|
|
12
|
+
# @thread_safety Installation checks the process-global Rake registry and is intended for boot time.
|
|
13
|
+
# @compatibility Rake versions compatible with Ruby 2.2.10 through Ruby 2.6.
|
|
14
|
+
# @example
|
|
15
|
+
# require "chronos/rake_tasks"
|
|
16
|
+
# Chronos::RakeTasks.install
|
|
17
|
+
# @errors Verification failures produce JSON and a nonzero process status.
|
|
18
|
+
# @performance The task performs one explicit synchronous verification when invoked.
|
|
19
|
+
module RakeTasks
|
|
20
|
+
extend ::Rake::DSL
|
|
21
|
+
|
|
22
|
+
TASK_NAME = "chronos:verify_integration".freeze
|
|
23
|
+
|
|
24
|
+
def self.install(options = {})
|
|
25
|
+
return false if ::Rake::Task.task_defined?(TASK_NAME)
|
|
26
|
+
|
|
27
|
+
output = options[:output] || $stdout
|
|
28
|
+
exiter = options[:exit] || proc { |status| exit(status) }
|
|
29
|
+
load_environment = options[:load_environment] || ::Rake::Task.task_defined?("environment")
|
|
30
|
+
prerequisites = load_environment ? ["environment"] : []
|
|
31
|
+
|
|
32
|
+
desc "Send an identified fake error and verify Chronos credentials and ingestion"
|
|
33
|
+
task TASK_NAME => prerequisites do
|
|
34
|
+
result = Chronos.verify_integration
|
|
35
|
+
output.puts(JSON.generate(result.to_h))
|
|
36
|
+
exiter.call(1) unless result.success?
|
|
37
|
+
end
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/chronos/version.rb
CHANGED
data/lib/chronos.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "chronos/ports"
|
|
|
7
7
|
require "chronos/adapters"
|
|
8
8
|
require "chronos/internal"
|
|
9
9
|
require "chronos/core/notice"
|
|
10
|
+
require "chronos/core/integration_verification_result"
|
|
10
11
|
require "chronos/core/backtrace_parser"
|
|
11
12
|
require "chronos/core/exception_cause_collector"
|
|
12
13
|
require "chronos/core/runtime_info"
|
|
@@ -36,6 +37,7 @@ require "chronos/application/remote_configuration"
|
|
|
36
37
|
require "chronos/application/ignore_policy"
|
|
37
38
|
require "chronos/application/delivery_pipeline"
|
|
38
39
|
require "chronos/application/capture_exception"
|
|
40
|
+
require "chronos/application/verify_integration"
|
|
39
41
|
require "chronos/application/apm_error_classifier"
|
|
40
42
|
require "chronos/application/apm_aggregator"
|
|
41
43
|
require "chronos/application/dependency_reporter"
|
|
@@ -95,6 +97,21 @@ module Chronos # rubocop:disable Metrics/ModuleLength
|
|
|
95
97
|
false
|
|
96
98
|
end
|
|
97
99
|
|
|
100
|
+
def verify_integration
|
|
101
|
+
agent = current_agent
|
|
102
|
+
return agent.verify_integration if agent
|
|
103
|
+
|
|
104
|
+
verification_failure(
|
|
105
|
+
"not_configured", "Chronos is not configured.",
|
|
106
|
+
"Run Chronos.configure before verifying the integration."
|
|
107
|
+
)
|
|
108
|
+
rescue StandardError
|
|
109
|
+
verification_failure(
|
|
110
|
+
"verification_failed", "Chronos integration verification failed locally.",
|
|
111
|
+
"Review the Chronos configuration and retry."
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
98
115
|
def with_context(context = {})
|
|
99
116
|
agent = current_agent
|
|
100
117
|
return yield unless agent
|
|
@@ -186,6 +203,15 @@ module Chronos # rubocop:disable Metrics/ModuleLength
|
|
|
186
203
|
def current_agent
|
|
187
204
|
@mutex.synchronize { @agent }
|
|
188
205
|
end
|
|
206
|
+
|
|
207
|
+
def verification_failure(status, message, guidance)
|
|
208
|
+
Core::IntegrationVerificationResult.new(
|
|
209
|
+
:success => false, :status => status, :credentials_valid => nil,
|
|
210
|
+
:event => {"id" => nil, "received" => false},
|
|
211
|
+
:receiver => {"name" => "chronos", "status" => "not_checked", "received_at" => nil},
|
|
212
|
+
:error => {"code" => status, "message" => message, "guidance" => guidance}
|
|
213
|
+
)
|
|
214
|
+
end
|
|
189
215
|
end
|
|
190
216
|
end
|
|
191
217
|
|