chronos-ruby 0.7.0.pre.1 → 0.9.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/README.md +55 -9
  4. data/contracts/apm-batch-v1.schema.json +1 -1
  5. data/contracts/dependencies-v1.schema.json +38 -0
  6. data/contracts/deploy-v1.schema.json +26 -0
  7. data/contracts/event-v1.schema.json +15 -1
  8. data/docs/adr/ADR-016-explicit-observability-integrations.md +25 -0
  9. data/docs/adr/ADR-017-deploy-events-and-correlation.md +25 -0
  10. data/docs/architecture.md +12 -1
  11. data/docs/compatibility.md +4 -0
  12. data/docs/configuration.md +24 -3
  13. data/docs/data-collected.md +11 -4
  14. data/docs/examples/plain-ruby.md +13 -1
  15. data/docs/modules/apm-aggregation.md +2 -2
  16. data/docs/modules/cache-observability.md +16 -0
  17. data/docs/modules/configuration.md +4 -0
  18. data/docs/modules/dependencies.md +18 -0
  19. data/docs/modules/deploy-tracking.md +86 -0
  20. data/docs/modules/external-http.md +27 -0
  21. data/docs/modules/rails-legacy.md +1 -1
  22. data/docs/modules/sidekiq-legacy.md +1 -1
  23. data/docs/modules/telemetry-events.md +4 -2
  24. data/docs/performance.md +30 -1
  25. data/docs/privacy-lgpd.md +20 -2
  26. data/docs/troubleshooting.md +25 -1
  27. data/lib/chronos/agent.rb +43 -0
  28. data/lib/chronos/application/apm_aggregator.rb +7 -10
  29. data/lib/chronos/application/apm_error_classifier.rb +27 -0
  30. data/lib/chronos/application/capture_telemetry.rb +18 -1
  31. data/lib/chronos/application/dependency_reporter.rb +140 -0
  32. data/lib/chronos/application/remote_configuration.rb +3 -1
  33. data/lib/chronos/capistrano.rb +4 -0
  34. data/lib/chronos/configuration/apm_validation.rb +28 -1
  35. data/lib/chronos/configuration.rb +25 -3
  36. data/lib/chronos/core/cache_normalizer.rb +99 -0
  37. data/lib/chronos/core/correlation_context.rb +65 -0
  38. data/lib/chronos/core/deploy_normalizer.rb +102 -0
  39. data/lib/chronos/core/payload_serializer.rb +4 -1
  40. data/lib/chronos/core/telemetry_event.rb +31 -4
  41. data/lib/chronos/integrations/capistrano.rb +61 -0
  42. data/lib/chronos/integrations/net_http.rb +165 -0
  43. data/lib/chronos/net_http.rb +4 -0
  44. data/lib/chronos/observability_facade.rb +50 -0
  45. data/lib/chronos/rails/notifications_subscriber.rb +6 -5
  46. data/lib/chronos/version.rb +1 -1
  47. data/lib/chronos.rb +8 -0
  48. metadata +19 -1
@@ -0,0 +1,165 @@
1
+ module Chronos
2
+ module Integrations
3
+ # Optional per-instance Net::HTTP instrumentation without a global monkey patch.
4
+ #
5
+ # @responsibility Install one idempotent request wrapper on an explicit HTTP connection.
6
+ # @motivation Legacy Net::HTTP has no middleware callback but must remain safe and optional.
7
+ # @limits Class convenience methods and uninstrumented instances are deliberately untouched.
8
+ # @collaborators Net::HTTP-compatible connection, request objects, and Chronos notifier.
9
+ # @thread_safety Installation is synchronized per object; request state is call-local.
10
+ # @compatibility Net::HTTP on Ruby 2.2.10 through Ruby 2.6.
11
+ # @example
12
+ # Chronos::Integrations::NetHttp.install(Net::HTTP.new("api.example.com"))
13
+ # @errors Instrumentation errors are contained; original HTTP errors are re-raised unchanged.
14
+ # @performance Adds bounded hashes and clock reads; never reads request/response bodies.
15
+ module NetHttp
16
+ INSTALLED_KEY = :@__chronos_net_http_installed
17
+ OPTIONS_KEY = :@__chronos_net_http_options
18
+
19
+ class << self
20
+ def install(connection, options = {})
21
+ notifier = options[:notifier] || Chronos
22
+ return false unless enabled?(notifier)
23
+ return false unless connection.respond_to?(:request) && connection.respond_to?(:address)
24
+
25
+ mutex_for(connection).synchronize do
26
+ return false if connection.instance_variable_get(INSTALLED_KEY)
27
+
28
+ connection.instance_variable_set(OPTIONS_KEY, instrumentation_options(notifier, options))
29
+ connection.singleton_class.send(:prepend, InstrumentedRequest)
30
+ connection.instance_variable_set(INSTALLED_KEY, true)
31
+ end
32
+ true
33
+ rescue StandardError
34
+ false
35
+ end
36
+
37
+ private
38
+
39
+ def enabled?(notifier)
40
+ return true unless notifier.respond_to?(:external_http_integration_options)
41
+
42
+ notifier.external_http_integration_options[:enabled] == true
43
+ rescue StandardError
44
+ false
45
+ end
46
+
47
+ def instrumentation_options(notifier, options)
48
+ configured = if notifier.respond_to?(:external_http_integration_options)
49
+ notifier.external_http_integration_options
50
+ else
51
+ {}
52
+ end
53
+ {
54
+ :notifier => notifier,
55
+ :clock => options[:clock] || proc { monotonic_time },
56
+ :trace_headers => configured.fetch(:trace_headers, true)
57
+ }
58
+ end
59
+
60
+ def mutex_for(connection)
61
+ key = :@__chronos_net_http_mutex
62
+ connection.instance_variable_get(key) || connection.instance_variable_set(key, Mutex.new)
63
+ end
64
+
65
+ def monotonic_time
66
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
67
+ rescue StandardError
68
+ Time.now.to_f
69
+ end
70
+ end
71
+
72
+ # Request wrapper prepended only to an explicitly selected HTTP object.
73
+ #
74
+ # @responsibility Inject bounded trace headers and record outcome/timing around `request`.
75
+ # @motivation Preserve the native API, streaming block, and exception semantics.
76
+ # @limits It never reads URL path/query, Authorization, request body, headers, or response body.
77
+ # @collaborators NetHttp installation options and Chronos record_event.
78
+ # @thread_safety All request observations use local variables.
79
+ # @compatibility Net::HTTP#request signature on Ruby 2.2.10 through Ruby 2.6.
80
+ # @example
81
+ # connection.request(Net::HTTP::Get.new("/health"))
82
+ # @errors Original StandardError values are recorded by class and re-raised unchanged.
83
+ # @performance Two clock reads and one asynchronous telemetry observation per call.
84
+ module InstrumentedRequest
85
+ def request(request, body = nil, &block)
86
+ options = instance_variable_get(NetHttp::OPTIONS_KEY)
87
+ started_at = options[:clock].call
88
+ inject_trace_headers(request, options) if options[:trace_headers]
89
+ response = super(request, body, &block)
90
+ record_external_http(options, request, started_at, response, nil)
91
+ response
92
+ rescue StandardError => error
93
+ record_external_http(options, request, started_at, nil, error) if options && started_at
94
+ raise
95
+ end
96
+
97
+ private
98
+
99
+ def inject_trace_headers(request, options)
100
+ return unless request.respond_to?(:[]) && request.respond_to?(:[]=)
101
+
102
+ context = if options[:notifier].respond_to?(:propagation_context)
103
+ options[:notifier].propagation_context
104
+ else
105
+ {}
106
+ end
107
+ set_header(request, "X-Chronos-Trace-ID", context["trace_id"] || context[:trace_id])
108
+ set_header(request, "X-Chronos-Request-ID", context["request_id"] || context[:request_id])
109
+ rescue StandardError
110
+ nil
111
+ end
112
+
113
+ def set_header(request, name, value)
114
+ request[name] = value.to_s if request[name].to_s.empty? && !value.to_s.empty?
115
+ end
116
+
117
+ def record_external_http(options, request, started_at, response, error)
118
+ payload = {
119
+ "host" => sanitized_external_host,
120
+ "method" => request.respond_to?(:method) ? request.method.to_s.upcase : "",
121
+ "status" => response_status(response),
122
+ "duration_ms" => ((options[:clock].call - started_at) * 1000.0).round(3),
123
+ "timeout" => timeout_error?(error),
124
+ "connection_error" => connection_error?(error),
125
+ "error_class" => error ? error.class.name.to_s : ""
126
+ }
127
+ payload.delete_if { |_key, value| value.nil? || value == "" }
128
+ options[:notifier].record_event("external_http", payload)
129
+ rescue StandardError
130
+ false
131
+ end
132
+
133
+ def response_status(response)
134
+ response.code.to_i if response && response.respond_to?(:code)
135
+ end
136
+
137
+ def sanitized_external_host
138
+ host = respond_to?(:address) ? address.to_s.downcase.sub(/\.\z/, "") : ""
139
+ host = host.scrub("?") if host.respond_to?(:scrub)
140
+ host.bytesize > 253 ? host.byteslice(0, 253) : host
141
+ rescue StandardError
142
+ ""
143
+ end
144
+
145
+ def timeout_error?(error)
146
+ return false unless error
147
+
148
+ error.is_a?(Timeout::Error) || !error.class.name.to_s.match(/Timeout/).nil?
149
+ rescue StandardError
150
+ false
151
+ end
152
+
153
+ def connection_error?(error)
154
+ return false unless error
155
+ return false if timeout_error?(error)
156
+
157
+ error.is_a?(SystemCallError) || error.is_a?(IOError) ||
158
+ !error.class.name.to_s.match(/SocketError|OpenSSL::SSL::SSLError/).nil?
159
+ rescue StandardError
160
+ false
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,4 @@
1
+ require "net/http"
2
+ require "timeout"
3
+ require "chronos"
4
+ require "chronos/integrations/net_http"
@@ -0,0 +1,50 @@
1
+ module Chronos
2
+ # Public entry points for optional observability and deployment integrations.
3
+ #
4
+ # @responsibility Delegate dependency, cache, outbound HTTP, and deploy calls to the agent.
5
+ # @motivation Keep the main public facade small while preserving one stable Chronos namespace.
6
+ # @limits It installs only explicitly supplied Net::HTTP connections and never enables features.
7
+ # @collaborators Chronos::Agent and Chronos::Integrations::NetHttp.
8
+ # @thread_safety Agent access uses the facade's synchronized current_agent lookup.
9
+ # @compatibility Ruby 2.2.10 through Ruby 2.6.
10
+ # @example
11
+ # Chronos.instrument_net_http(Net::HTTP.new("api.example.com"))
12
+ # @errors Integration failures return false or safe disabled options.
13
+ # @performance Delegation is constant-time; dependency collection remains at-most-once.
14
+ module ObservabilityFacade
15
+ def report_dependencies
16
+ agent = current_agent
17
+ agent ? agent.report_dependencies : false
18
+ rescue StandardError
19
+ false
20
+ end
21
+
22
+ def notify_deploy(attributes = {}, timeout = Agent::DEFAULT_FLUSH_TIMEOUT)
23
+ agent = current_agent
24
+ agent ? agent.notify_deploy(attributes, timeout) : false
25
+ rescue StandardError
26
+ false
27
+ end
28
+
29
+ def external_http_integration_options
30
+ agent = current_agent
31
+ agent ? agent.external_http_integration_options : {:enabled => false}
32
+ rescue StandardError
33
+ {:enabled => false}
34
+ end
35
+
36
+ def cache_integration_options
37
+ agent = current_agent
38
+ agent ? agent.cache_integration_options : {:project_id => "", :key_mode => :none}
39
+ rescue StandardError
40
+ {:project_id => "", :key_mode => :none}
41
+ end
42
+
43
+ def instrument_net_http(connection, options = {})
44
+ require "chronos/net_http"
45
+ Integrations::NetHttp.install(connection, options.merge(:notifier => self))
46
+ rescue StandardError
47
+ false
48
+ end
49
+ end
50
+ end
@@ -4,7 +4,7 @@ module Chronos
4
4
  #
5
5
  # @responsibility Subscribe once and normalize controller, view, SQL, mailer, job, and cache events.
6
6
  # @motivation Support Rails 4.2 and 5.2 through feature detection and public notification APIs.
7
- # @limits It never sends SQL text, bind values, cache keys, mail bodies, or job arguments.
7
+ # @limits It never sends SQL text, binds, raw cache keys/values, mail bodies, or job arguments.
8
8
  # @collaborators ActiveSupport::Notifications and the Chronos facade.
9
9
  # @thread_safety Subscription registry is mutex-protected; callbacks own their state.
10
10
  # @compatibility ActiveSupport notification argument shapes from Rails 4.2 through 5.2.
@@ -30,6 +30,10 @@ module Chronos
30
30
  @notifier = notifier
31
31
  @notifications = notifications || active_support_notifications
32
32
  @sql_normalizer = Core::SqlNormalizer.new
33
+ cache_options = notifier.respond_to?(:cache_integration_options) ? notifier.cache_integration_options : {}
34
+ @cache_normalizer = Core::CacheNormalizer.new(
35
+ cache_options[:project_id].to_s, cache_options[:key_mode] || :none
36
+ )
33
37
  end
34
38
 
35
39
  def install
@@ -148,10 +152,7 @@ module Chronos
148
152
  end
149
153
 
150
154
  def cache(name, payload, duration)
151
- data = {
152
- "operation" => name.split(".").first, "store" => value(payload, :store).to_s,
153
- "hit" => value(payload, :hit), "duration_ms" => duration
154
- }
155
+ data = @cache_normalizer.call(name, payload).merge("duration_ms" => duration)
155
156
  @notifier.record_event("cache", data)
156
157
  end
157
158
 
@@ -1,4 +1,4 @@
1
1
  module Chronos
2
2
  # Current version of the legacy Chronos Ruby agent.
3
- VERSION = "0.7.0.pre.1".freeze
3
+ VERSION = "0.9.0.pre.1".freeze
4
4
  end
data/lib/chronos.rb CHANGED
@@ -14,10 +14,13 @@ require "chronos/core/notice_builder"
14
14
  require "chronos/core/sensitive_value_filter"
15
15
  require "chronos/core/sanitizer"
16
16
  require "chronos/core/safe_serializer"
17
+ require "chronos/core/correlation_context"
18
+ require "chronos/core/deploy_normalizer"
17
19
  require "chronos/core/payload_serializer"
18
20
  require "chronos/core/telemetry_event"
19
21
  require "chronos/core/sql_normalizer"
20
22
  require "chronos/core/metric_aggregate"
23
+ require "chronos/core/cache_normalizer"
21
24
  require "chronos/ports/transport"
22
25
  require "chronos/ports/context_store"
23
26
  require "chronos/internal/safe_logger"
@@ -32,9 +35,12 @@ require "chronos/application/circuit_breaker"
32
35
  require "chronos/application/remote_configuration"
33
36
  require "chronos/application/delivery_pipeline"
34
37
  require "chronos/application/capture_exception"
38
+ require "chronos/application/apm_error_classifier"
35
39
  require "chronos/application/apm_aggregator"
40
+ require "chronos/application/dependency_reporter"
36
41
  require "chronos/application/capture_telemetry"
37
42
  require "chronos/agent"
43
+ require "chronos/observability_facade"
38
44
  require "chronos/integrations"
39
45
  require "chronos/integrations/rack"
40
46
  require "chronos/integrations/rack/middleware"
@@ -55,6 +61,8 @@ require "chronos/integrations/rack/middleware"
55
61
  # end
56
62
  # Chronos.notify(RuntimeError.new("failed"))
57
63
  module Chronos
64
+ extend ObservabilityFacade
65
+
58
66
  @mutex = Mutex.new
59
67
  @agent = nil
60
68
 
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.7.0.pre.1
4
+ version: 0.9.0.pre.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antonio Jefferson
@@ -87,6 +87,8 @@ files:
87
87
  - README.md
88
88
  - SECURITY.md
89
89
  - contracts/apm-batch-v1.schema.json
90
+ - contracts/dependencies-v1.schema.json
91
+ - contracts/deploy-v1.schema.json
90
92
  - contracts/event-v1.schema.json
91
93
  - contracts/rack-context-v1.schema.json
92
94
  - contracts/sidekiq-job-v1.schema.json
@@ -101,6 +103,8 @@ files:
101
103
  - docs/adr/ADR-013-legacy-rails-notifications.md
102
104
  - docs/adr/ADR-014-sidekiq-envelope-context.md
103
105
  - docs/adr/ADR-015-bounded-apm-aggregation.md
106
+ - docs/adr/ADR-016-explicit-observability-integrations.md
107
+ - docs/adr/ADR-017-deploy-events-and-correlation.md
104
108
  - docs/architecture.md
105
109
  - docs/compatibility.md
106
110
  - docs/configuration.md
@@ -108,7 +112,11 @@ files:
108
112
  - docs/examples/plain-ruby.md
109
113
  - docs/modules/apm-aggregation.md
110
114
  - docs/modules/async-queue.md
115
+ - docs/modules/cache-observability.md
111
116
  - docs/modules/configuration.md
117
+ - docs/modules/dependencies.md
118
+ - docs/modules/deploy-tracking.md
119
+ - docs/modules/external-http.md
112
120
  - docs/modules/notice-pipeline.md
113
121
  - docs/modules/rack-context.md
114
122
  - docs/modules/rails-legacy.md
@@ -129,12 +137,15 @@ files:
129
137
  - lib/chronos/agent.rb
130
138
  - lib/chronos/application.rb
131
139
  - lib/chronos/application/apm_aggregator.rb
140
+ - lib/chronos/application/apm_error_classifier.rb
132
141
  - lib/chronos/application/capture_exception.rb
133
142
  - lib/chronos/application/capture_telemetry.rb
134
143
  - lib/chronos/application/circuit_breaker.rb
135
144
  - lib/chronos/application/delivery_pipeline.rb
145
+ - lib/chronos/application/dependency_reporter.rb
136
146
  - lib/chronos/application/remote_configuration.rb
137
147
  - lib/chronos/application/retry_policy.rb
148
+ - lib/chronos/capistrano.rb
138
149
  - lib/chronos/configuration.rb
139
150
  - lib/chronos/configuration/apm_validation.rb
140
151
  - lib/chronos/configuration/snapshot.rb
@@ -142,6 +153,9 @@ files:
142
153
  - lib/chronos/core.rb
143
154
  - lib/chronos/core/backtrace_parser.rb
144
155
  - lib/chronos/core/breadcrumb.rb
156
+ - lib/chronos/core/cache_normalizer.rb
157
+ - lib/chronos/core/correlation_context.rb
158
+ - lib/chronos/core/deploy_normalizer.rb
145
159
  - lib/chronos/core/exception_cause_collector.rb
146
160
  - lib/chronos/core/metric_aggregate.rb
147
161
  - lib/chronos/core/notice.rb
@@ -155,7 +169,9 @@ files:
155
169
  - lib/chronos/core/telemetry_event.rb
156
170
  - lib/chronos/errors.rb
157
171
  - lib/chronos/integrations.rb
172
+ - lib/chronos/integrations/capistrano.rb
158
173
  - lib/chronos/integrations/job_payload.rb
174
+ - lib/chronos/integrations/net_http.rb
159
175
  - lib/chronos/integrations/rack.rb
160
176
  - lib/chronos/integrations/rack/middleware.rb
161
177
  - lib/chronos/integrations/sidekiq.rb
@@ -164,6 +180,8 @@ files:
164
180
  - lib/chronos/internal/memory_backlog.rb
165
181
  - lib/chronos/internal/safe_logger.rb
166
182
  - lib/chronos/internal/worker_pool.rb
183
+ - lib/chronos/net_http.rb
184
+ - lib/chronos/observability_facade.rb
167
185
  - lib/chronos/ports.rb
168
186
  - lib/chronos/ports/context_store.rb
169
187
  - lib/chronos/ports/transport.rb