chronos-ruby 0.6.0.pre.1 → 0.8.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/README.md +53 -10
  4. data/contracts/apm-batch-v1.schema.json +35 -0
  5. data/contracts/dependencies-v1.schema.json +38 -0
  6. data/contracts/event-v1.schema.json +1 -1
  7. data/docs/adr/ADR-015-bounded-apm-aggregation.md +27 -0
  8. data/docs/adr/ADR-016-explicit-observability-integrations.md +25 -0
  9. data/docs/architecture.md +11 -2
  10. data/docs/compatibility.md +4 -0
  11. data/docs/configuration.md +28 -1
  12. data/docs/data-collected.md +17 -3
  13. data/docs/examples/plain-ruby.md +7 -1
  14. data/docs/modules/apm-aggregation.md +57 -0
  15. data/docs/modules/cache-observability.md +16 -0
  16. data/docs/modules/configuration.md +4 -0
  17. data/docs/modules/dependencies.md +18 -0
  18. data/docs/modules/external-http.md +27 -0
  19. data/docs/modules/rails-legacy.md +2 -2
  20. data/docs/modules/sidekiq-legacy.md +2 -2
  21. data/docs/modules/telemetry-events.md +2 -2
  22. data/docs/performance.md +29 -1
  23. data/docs/privacy-lgpd.md +16 -2
  24. data/docs/troubleshooting.md +25 -1
  25. data/lib/chronos/agent.rb +49 -0
  26. data/lib/chronos/application/apm_aggregator.rb +270 -0
  27. data/lib/chronos/application/apm_error_classifier.rb +27 -0
  28. data/lib/chronos/application/capture_telemetry.rb +45 -5
  29. data/lib/chronos/application/delivery_pipeline.rb +7 -0
  30. data/lib/chronos/application/dependency_reporter.rb +129 -0
  31. data/lib/chronos/application/remote_configuration.rb +8 -1
  32. data/lib/chronos/configuration/apm_validation.rb +76 -0
  33. data/lib/chronos/configuration.rb +35 -2
  34. data/lib/chronos/core/cache_normalizer.rb +99 -0
  35. data/lib/chronos/core/metric_aggregate.rb +113 -0
  36. data/lib/chronos/core/sql_normalizer.rb +114 -0
  37. data/lib/chronos/core/telemetry_event.rb +1 -1
  38. data/lib/chronos/integrations/net_http.rb +165 -0
  39. data/lib/chronos/integrations/rack/middleware.rb +19 -1
  40. data/lib/chronos/integrations/sidekiq.rb +2 -0
  41. data/lib/chronos/net_http.rb +4 -0
  42. data/lib/chronos/observability_facade.rb +43 -0
  43. data/lib/chronos/rails/notifications_subscriber.rb +42 -9
  44. data/lib/chronos/version.rb +1 -1
  45. data/lib/chronos.rb +23 -0
  46. 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
@@ -38,11 +38,14 @@ module Chronos
38
38
  response = @app.call(env)
39
39
  status = response[0]
40
40
  headers = response[1]
41
- add_request_breadcrumb("request completed", dynamic_request_context(base, status, headers, started_at))
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
@@ -0,0 +1,4 @@
1
+ require "net/http"
2
+ require "timeout"
3
+ require "chronos"
4
+ require "chronos/integrations/net_http"
@@ -0,0 +1,43 @@
1
+ module Chronos
2
+ # Public entry points for optional version 0.8 observability integrations.
3
+ #
4
+ # @responsibility Delegate dependency, cache, and outbound HTTP integration 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 external_http_integration_options
23
+ agent = current_agent
24
+ agent ? agent.external_http_integration_options : {:enabled => false}
25
+ rescue StandardError
26
+ {:enabled => false}
27
+ end
28
+
29
+ def cache_integration_options
30
+ agent = current_agent
31
+ agent ? agent.cache_integration_options : {:project_id => "", :key_mode => :none}
32
+ rescue StandardError
33
+ {:project_id => "", :key_mode => :none}
34
+ end
35
+
36
+ def instrument_net_http(connection, options = {})
37
+ require "chronos/net_http"
38
+ Integrations::NetHttp.install(connection, options.merge(:notifier => self))
39
+ rescue StandardError
40
+ false
41
+ end
42
+ end
43
+ 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.
@@ -29,6 +29,11 @@ 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
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
+ )
32
37
  end
33
38
 
34
39
  def install
@@ -105,7 +110,7 @@ module Chronos
105
110
  "route" => route(payload),
106
111
  "duration_ms" => duration, "parameters" => hash(value(payload, :params))
107
112
  }
108
- @notifier.record_event("request", data)
113
+ record_once("request", "request", data)
109
114
  end
110
115
 
111
116
  def render_template(payload, duration)
@@ -117,10 +122,15 @@ module Chronos
117
122
  end
118
123
 
119
124
  def sql(payload, duration)
120
- data = {
121
- "name" => value(payload, :name).to_s, "cached" => value(payload, :cached) == true,
122
- "duration_ms" => duration
125
+ metadata = {
126
+ :name => value(payload, :name), :cached => value(payload, :cached),
127
+ :adapter => value(payload, :adapter), :connection => value(payload, :connection),
128
+ :role => value(payload, :connection_role) || value(payload, :role),
129
+ :shard => value(payload, :connection_shard) || value(payload, :shard),
130
+ :exception_object => value(payload, :exception_object), :exception => value(payload, :exception)
123
131
  }
132
+ metadata[:source] = sampled_query_source if duration >= slow_query_threshold
133
+ data = @sql_normalizer.call(value(payload, :sql), metadata).merge("duration_ms" => duration)
124
134
  @notifier.record_event("query", data)
125
135
  end
126
136
 
@@ -142,10 +152,7 @@ module Chronos
142
152
  end
143
153
 
144
154
  def cache(name, payload, duration)
145
- data = {
146
- "operation" => name.split(".").first, "store" => value(payload, :store).to_s,
147
- "hit" => value(payload, :hit), "duration_ms" => duration
148
- }
155
+ data = @cache_normalizer.call(name, payload).merge("duration_ms" => duration)
149
156
  @notifier.record_event("cache", data)
150
157
  end
151
158
 
@@ -198,6 +205,32 @@ module Chronos
198
205
  rescue StandardError
199
206
  ""
200
207
  end
208
+
209
+ def record_once(key, event_type, payload)
210
+ if @notifier.respond_to?(:record_event_once)
211
+ @notifier.record_event_once(key, event_type, payload)
212
+ else
213
+ @notifier.record_event(event_type, payload)
214
+ end
215
+ end
216
+
217
+ def slow_query_threshold
218
+ options = @notifier.respond_to?(:apm_integration_options) ? @notifier.apm_integration_options : {}
219
+ (options[:slow_query_threshold_ms] || 500.0).to_f
220
+ rescue StandardError
221
+ 500.0
222
+ end
223
+
224
+ def sampled_query_source
225
+ options = @notifier.respond_to?(:apm_integration_options) ? @notifier.apm_integration_options : {}
226
+ root = options[:root_directory].to_s
227
+ frame = caller.find do |line|
228
+ (root.empty? || line.start_with?(root)) && line !~ %r{/lib/chronos/}
229
+ end
230
+ frame.to_s.sub(/:in .*/, "")[0, 256]
231
+ rescue StandardError
232
+ ""
233
+ end
201
234
  end
202
235
  end
203
236
  end
@@ -1,4 +1,4 @@
1
1
  module Chronos
2
2
  # Current version of the legacy Chronos Ruby agent.
3
- VERSION = "0.6.0.pre.1".freeze
3
+ VERSION = "0.8.0.pre.1".freeze
4
4
  end
data/lib/chronos.rb CHANGED
@@ -16,6 +16,9 @@ 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"
21
+ require "chronos/core/cache_normalizer"
19
22
  require "chronos/ports/transport"
20
23
  require "chronos/ports/context_store"
21
24
  require "chronos/internal/safe_logger"
@@ -30,8 +33,12 @@ require "chronos/application/circuit_breaker"
30
33
  require "chronos/application/remote_configuration"
31
34
  require "chronos/application/delivery_pipeline"
32
35
  require "chronos/application/capture_exception"
36
+ require "chronos/application/apm_error_classifier"
37
+ require "chronos/application/apm_aggregator"
38
+ require "chronos/application/dependency_reporter"
33
39
  require "chronos/application/capture_telemetry"
34
40
  require "chronos/agent"
41
+ require "chronos/observability_facade"
35
42
  require "chronos/integrations"
36
43
  require "chronos/integrations/rack"
37
44
  require "chronos/integrations/rack/middleware"
@@ -52,6 +59,8 @@ require "chronos/integrations/rack/middleware"
52
59
  # end
53
60
  # Chronos.notify(RuntimeError.new("failed"))
54
61
  module Chronos
62
+ extend ObservabilityFacade
63
+
55
64
  @mutex = Mutex.new
56
65
  @agent = nil
57
66
 
@@ -104,6 +113,20 @@ module Chronos
104
113
  false
105
114
  end
106
115
 
116
+ def record_event_once(key, event_type, payload = {}, context = {})
117
+ agent = current_agent
118
+ agent ? agent.record_event_once(key, event_type, payload, context) : false
119
+ rescue StandardError
120
+ false
121
+ end
122
+
123
+ def apm_integration_options
124
+ agent = current_agent
125
+ agent ? agent.apm_integration_options : {:enabled => false}
126
+ rescue StandardError
127
+ {:enabled => false}
128
+ end
129
+
107
130
  # Returns only trace/request identifiers for optional process-boundary adapters.
108
131
  def propagation_context
109
132
  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.6.0.pre.1
4
+ version: 0.8.0.pre.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antonio Jefferson
@@ -86,6 +86,8 @@ files:
86
86
  - LICENSE.txt
87
87
  - README.md
88
88
  - SECURITY.md
89
+ - contracts/apm-batch-v1.schema.json
90
+ - contracts/dependencies-v1.schema.json
89
91
  - contracts/event-v1.schema.json
90
92
  - contracts/rack-context-v1.schema.json
91
93
  - contracts/sidekiq-job-v1.schema.json
@@ -99,13 +101,19 @@ files:
99
101
  - docs/adr/ADR-012-rack-context-isolation.md
100
102
  - docs/adr/ADR-013-legacy-rails-notifications.md
101
103
  - docs/adr/ADR-014-sidekiq-envelope-context.md
104
+ - docs/adr/ADR-015-bounded-apm-aggregation.md
105
+ - docs/adr/ADR-016-explicit-observability-integrations.md
102
106
  - docs/architecture.md
103
107
  - docs/compatibility.md
104
108
  - docs/configuration.md
105
109
  - docs/data-collected.md
106
110
  - docs/examples/plain-ruby.md
111
+ - docs/modules/apm-aggregation.md
107
112
  - docs/modules/async-queue.md
113
+ - docs/modules/cache-observability.md
108
114
  - docs/modules/configuration.md
115
+ - docs/modules/dependencies.md
116
+ - docs/modules/external-http.md
109
117
  - docs/modules/notice-pipeline.md
110
118
  - docs/modules/rack-context.md
111
119
  - docs/modules/rails-legacy.md
@@ -125,19 +133,25 @@ files:
125
133
  - lib/chronos/adapters/thread_local_context_store.rb
126
134
  - lib/chronos/agent.rb
127
135
  - lib/chronos/application.rb
136
+ - lib/chronos/application/apm_aggregator.rb
137
+ - lib/chronos/application/apm_error_classifier.rb
128
138
  - lib/chronos/application/capture_exception.rb
129
139
  - lib/chronos/application/capture_telemetry.rb
130
140
  - lib/chronos/application/circuit_breaker.rb
131
141
  - lib/chronos/application/delivery_pipeline.rb
142
+ - lib/chronos/application/dependency_reporter.rb
132
143
  - lib/chronos/application/remote_configuration.rb
133
144
  - lib/chronos/application/retry_policy.rb
134
145
  - lib/chronos/configuration.rb
146
+ - lib/chronos/configuration/apm_validation.rb
135
147
  - lib/chronos/configuration/snapshot.rb
136
148
  - lib/chronos/configuration/validation.rb
137
149
  - lib/chronos/core.rb
138
150
  - lib/chronos/core/backtrace_parser.rb
139
151
  - lib/chronos/core/breadcrumb.rb
152
+ - lib/chronos/core/cache_normalizer.rb
140
153
  - lib/chronos/core/exception_cause_collector.rb
154
+ - lib/chronos/core/metric_aggregate.rb
141
155
  - lib/chronos/core/notice.rb
142
156
  - lib/chronos/core/notice_builder.rb
143
157
  - lib/chronos/core/payload_serializer.rb
@@ -145,10 +159,12 @@ files:
145
159
  - lib/chronos/core/safe_serializer.rb
146
160
  - lib/chronos/core/sanitizer.rb
147
161
  - lib/chronos/core/sensitive_value_filter.rb
162
+ - lib/chronos/core/sql_normalizer.rb
148
163
  - lib/chronos/core/telemetry_event.rb
149
164
  - lib/chronos/errors.rb
150
165
  - lib/chronos/integrations.rb
151
166
  - lib/chronos/integrations/job_payload.rb
167
+ - lib/chronos/integrations/net_http.rb
152
168
  - lib/chronos/integrations/rack.rb
153
169
  - lib/chronos/integrations/rack/middleware.rb
154
170
  - lib/chronos/integrations/sidekiq.rb
@@ -157,6 +173,8 @@ files:
157
173
  - lib/chronos/internal/memory_backlog.rb
158
174
  - lib/chronos/internal/safe_logger.rb
159
175
  - lib/chronos/internal/worker_pool.rb
176
+ - lib/chronos/net_http.rb
177
+ - lib/chronos/observability_facade.rb
160
178
  - lib/chronos/ports.rb
161
179
  - lib/chronos/ports/context_store.rb
162
180
  - lib/chronos/ports/transport.rb