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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/README.md +101 -181
  4. data/contracts/integration-verification-response-v1.schema.json +76 -0
  5. data/docs/adr/ADR-007-feature-detection.md +25 -0
  6. data/docs/adr/ADR-008-context-store.md +25 -0
  7. data/docs/adr/ADR-009-sampling.md +25 -0
  8. data/docs/adr/ADR-010-opentelemetry-interoperability.md +25 -0
  9. data/docs/adr/ADR-018-pre-1.0-hardening.md +6 -2
  10. data/docs/compatibility.md +30 -23
  11. data/docs/data-collected.md +3 -0
  12. data/docs/deprecation-policy.md +1 -1
  13. data/docs/migration-from-airbrake.md +1 -1
  14. data/docs/modules/breadcrumbs.md +23 -0
  15. data/docs/modules/context.md +21 -0
  16. data/docs/modules/deploys.md +23 -0
  17. data/docs/modules/integration-verification.md +65 -0
  18. data/docs/modules/job-monitoring.md +22 -0
  19. data/docs/modules/request-monitoring.md +20 -0
  20. data/docs/modules/runtime-metrics.md +22 -0
  21. data/docs/modules/sampling.md +22 -0
  22. data/docs/modules/sidekiq-legacy.md +1 -1
  23. data/docs/modules/sql-monitoring.md +22 -0
  24. data/docs/performance.md +13 -2
  25. data/docs/protocol-v1.md +3 -1
  26. data/docs/release-1.0-readiness.md +17 -15
  27. data/docs/security-review.md +4 -3
  28. data/docs/troubleshooting.md +6 -0
  29. data/lib/chronos/adapters/net_http_transport.rb +36 -2
  30. data/lib/chronos/agent.rb +21 -0
  31. data/lib/chronos/application/delivery_pipeline.rb +6 -3
  32. data/lib/chronos/application/verify_integration.rb +262 -0
  33. data/lib/chronos/core/integration_verification_result.rb +108 -0
  34. data/lib/chronos/errors.rb +11 -0
  35. data/lib/chronos/ports/transport.rb +18 -1
  36. data/lib/chronos/rails/railtie.rb +5 -0
  37. data/lib/chronos/rake_tasks.rb +41 -0
  38. data/lib/chronos/version.rb +1 -1
  39. data/lib/chronos.rb +26 -0
  40. 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 { |connection| connection.request(request) }
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
- result = deliver_with_backlog(event)
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
- false
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
@@ -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
@@ -1,4 +1,4 @@
1
1
  module Chronos
2
2
  # Current version of the legacy Chronos Ruby agent.
3
- VERSION = "0.9.0.pre.3".freeze
3
+ VERSION = "1.0.0".freeze
4
4
  end
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