tracelit 0.1.0 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58eb691b0dfacebc7072a0ee10ebc4fb88ddc5074c659211a689532bf2e86f3b
4
- data.tar.gz: 9a73dad4b506173c24f6e64a17c227812a0ab90651ef3a08e493ab9a68c602e1
3
+ metadata.gz: bcea7249159775e00167a1a64ca7160132286dcc622c6c49a118541a8e58d66e
4
+ data.tar.gz: 0ea2e6000cd63cb87be8f0859f0e2bbc1c371e15e7ab33592c3c8070fd806205
5
5
  SHA512:
6
- metadata.gz: 7ca34cc0206da44b31ceb865848ec08ef39f015764ae7f1f20dcf6d2b9af78820de537f6994ea48a224415350878ea9c206a54f21dd434cb37146451a04a06ba
7
- data.tar.gz: 999c495a4c363af34e4bdd1671d66611c42124040f2e20172040590dbb5e6a715f9123e4550a29c2378c77d5275cede91fdc8dd46ca1830c1eabfa2b6b92eac3
6
+ metadata.gz: e2c7a0461048ab10e848f09adb946dfb75480a5c11662cd53295638013d83bac6f17b2f653e82c4e3a15e73bbbb16fdcc6033a0e86e434440098744a44563471
7
+ data.tar.gz: e1f3b97721fcbc4c35dfd8957e17da621dd02bd3027f4a69fa69eb5e5e50133be1eb78a7727a460e695125c0913249337b300e37d4e0ef569ebaa7dda0704e14
data/README.md CHANGED
@@ -8,87 +8,7 @@ Official Ruby SDK for [Tracelit](https://tracelit.io) — drop-in OpenTelemetry
8
8
 
9
9
  ## Set up with AI
10
10
 
11
- Copy the prompt below and paste it into Cursor, Claude, ChatGPT, or any AI assistant. It contains everything the AI needs to fully integrate Tracelit into your app gem, initializer, manual spans, custom metrics, and test guard.
12
-
13
- ````
14
- You are integrating the Tracelit Ruby SDK into this codebase.
15
-
16
- ## What Tracelit does
17
- Tracelit is a drop-in OpenTelemetry SDK that sends traces, metrics, and logs
18
- to https://tracelit.io via OTLP/HTTP. It auto-instruments Rails, Sidekiq,
19
- ActiveRecord, Redis, Faraday, Net::HTTP, and more with zero code changes.
20
-
21
- ## Steps
22
-
23
- ### 1. Add the gem
24
- In Gemfile add:
25
- gem "tracelit"
26
- Then run: bundle install
27
-
28
- ### 2. Create the initializer
29
- Create config/initializers/tracelit.rb with this content
30
- (replace MY_SERVICE_NAME with the actual name of this service):
31
-
32
- Tracelit.configure do |config|
33
- config.api_key = ENV["TRACELIT_API_KEY"]
34
- config.service_name = "MY_SERVICE_NAME"
35
- config.environment = ENV.fetch("RAILS_ENV", "production")
36
- config.sample_rate = 1.0
37
- config.enabled = ENV["TRACELIT_ENABLED"] != "false"
38
- end
39
-
40
- For Sinatra/Rack apps also add Tracelit.start! after the configure block.
41
- For Rails, the Railtie calls start! automatically — no extra line needed.
42
-
43
- ### 3. Disable in tests
44
- In config/environments/test.rb (or .env.test) add:
45
- TRACELIT_ENABLED=false
46
-
47
- ### 4. Add manual spans (optional)
48
- Wrap any important operation with a custom span:
49
-
50
- Tracelit.tracer.in_span("describe_the_operation") do |span|
51
- span.set_attribute("relevant.attribute", value.to_s)
52
- do_the_work
53
- end
54
-
55
- ### 5. Add custom metrics (optional)
56
- Use counters, histograms, or gauges for business-level signals:
57
-
58
- # Counter — increment by 1 each time an event occurs
59
- counter = Tracelit.metrics.counter("my.event.count",
60
- description: "...", unit: "{events}")
61
- counter.add(1, attributes: { "key" => "value" })
62
-
63
- # Histogram — record a measured value (latency, size, etc.)
64
- histogram = Tracelit.metrics.histogram("my.operation.duration",
65
- description: "...", unit: "ms")
66
- histogram.record(elapsed_ms, attributes: { "key" => "value" })
67
-
68
- # Gauge — record a current level (queue depth, pool size, etc.)
69
- gauge = Tracelit.metrics.gauge("my.queue.depth",
70
- description: "...", unit: "{items}")
71
- gauge.record(current_depth, attributes: { "queue" => "default" })
72
-
73
- ## What you get automatically (no code required)
74
- - Traces for every HTTP request, SQL query, Redis call, Sidekiq job, etc.
75
- - Metrics: http.server.request.count/duration, http.server.error.count,
76
- db.query.duration, sidekiq.job.count/duration/error.count,
77
- db.connection_pool.size/busy/idle/waiting, process.memory.rss
78
- - Rails.logger lines forwarded to Tracelit logs, correlated to the active trace
79
- - Error spans always exported even when the trace is outside the sample ratio
80
-
81
- ## Configuration reference
82
- | Option | Env var | Default |
83
- |--------------------|--------------------------|--------------------------------|
84
- | api_key | TRACELIT_API_KEY | nil (required) |
85
- | service_name | TRACELIT_SERVICE_NAME | Rails app name (required) |
86
- | environment | TRACELIT_ENVIRONMENT | "production" |
87
- | endpoint | TRACELIT_ENDPOINT | https://ingest.tracelit.app |
88
- | sample_rate | TRACELIT_SAMPLE_RATE | 1.0 |
89
- | enabled | TRACELIT_ENABLED | true |
90
- | resource_attributes| — | {} (extra span/metric tags) |
91
- ````
11
+ Want an AI assistant (Cursor, Claude, ChatGPT, etc.) to integrate Tracelit into your app automatically? Copy the contents of [`llm_prompt.txt`](./llm_prompt.txt) and paste it as your prompt. It covers gem installation, initializer setup, manual spans, custom metrics, and test guard — everything the AI needs in one shot.
92
12
 
93
13
  ---
94
14
 
@@ -29,19 +29,40 @@ module Tracelit
29
29
  attr_accessor :resource_attributes
30
30
 
31
31
  def initialize
32
- @api_key = ENV["TRACELIT_API_KEY"]
33
- @service_name = ENV["TRACELIT_SERVICE_NAME"]
34
- @environment = ENV["TRACELIT_ENVIRONMENT"] || "production"
35
- @endpoint = ENV["TRACELIT_ENDPOINT"] || "https://ingest.tracelit.app"
36
- @sample_rate = (ENV["TRACELIT_SAMPLE_RATE"] || "1.0").to_f
37
- @enabled = ENV["TRACELIT_ENABLED"] != "false"
32
+ @api_key = ENV["TRACELIT_API_KEY"]
33
+ @service_name = ENV["TRACELIT_SERVICE_NAME"]
34
+ @environment = ENV["TRACELIT_ENVIRONMENT"] || "production"
35
+ @endpoint = ENV["TRACELIT_ENDPOINT"] || "https://ingest.tracelit.app"
36
+ @sample_rate = (ENV["TRACELIT_SAMPLE_RATE"] || "1.0").to_f
37
+ @enabled = ENV["TRACELIT_ENABLED"] != "false"
38
38
  @resource_attributes = {}
39
39
  end
40
40
 
41
+ # Returns an array of human-readable error strings.
42
+ # Empty array means the configuration is valid.
43
+ # Never raises — callers decide whether to warn or abort.
44
+ def valid?
45
+ errors = []
46
+ errors << "api_key is required" if api_key.nil? || api_key.to_s.empty?
47
+
48
+ # Fix 3: check resolved_service_name so Rails apps that rely on automatic
49
+ # name inference (module_parent_name) are not incorrectly flagged.
50
+ if resolved_service_name == "unknown-service"
51
+ errors << "service_name is required (set config.service_name or TRACELIT_SERVICE_NAME)"
52
+ end
53
+
54
+ unless sample_rate.between?(0.0, 1.0)
55
+ errors << "sample_rate must be between 0.0 and 1.0 (got #{sample_rate})"
56
+ end
57
+
58
+ errors
59
+ end
60
+
61
+ # Kept for backwards compatibility. Previously raised ArgumentError;
62
+ # now a no-op because an observability SDK must never crash the host app.
63
+ # Use valid? to check for configuration errors programmatically.
41
64
  def validate!
42
- raise ArgumentError, "Tracelit.config.api_key is required" if api_key.nil? || api_key.empty?
43
- raise ArgumentError, "Tracelit.config.service_name is required" if service_name.nil? || service_name.empty?
44
- raise ArgumentError, "sample_rate must be between 0.0 and 1.0" unless sample_rate.between?(0.0, 1.0)
65
+ # no-op see valid? for soft validation
45
66
  end
46
67
 
47
68
  # Infer service name from Rails application if not explicitly set.
@@ -12,68 +12,107 @@ require_relative "metrics"
12
12
 
13
13
  module Tracelit
14
14
  module Instrumentation
15
+ SETUP_MUTEX = Mutex.new
16
+
15
17
  # Sets up the OpenTelemetry SDK with the Tracelit OTLP exporter.
16
18
  # Called once at application boot. Idempotent — safe to call multiple times.
19
+ # Never raises — a misconfigured SDK must not crash the host application.
17
20
  def self.setup(config)
18
- return if @configured
19
- return unless config.enabled
20
-
21
- config.validate!
22
-
23
- OpenTelemetry::SDK.configure do |otel|
24
- # Resource attributes identify this service in Tracelit.
25
- # These populate the `resource` Map column on every telemetry row.
26
- otel.resource = OpenTelemetry::SDK::Resources::Resource.create(
27
- {
28
- OpenTelemetry::SemanticConventions::Resource::SERVICE_NAME => config.resolved_service_name,
29
- OpenTelemetry::SemanticConventions::Resource::DEPLOYMENT_ENVIRONMENT => config.environment,
30
- "telemetry.sdk.language" => "ruby",
31
- "telemetry.sdk.name" => detect_framework,
32
- "telemetry.sdk.version" => Tracelit::VERSION,
33
- }.merge(config.resource_attributes)
34
- )
35
-
36
- # Build the OTLP exporter once — shared by both processors
37
- exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(
38
- endpoint: "#{config.endpoint}/v1/traces",
39
- headers: {
40
- "Authorization" => "Bearer #{config.api_key}",
41
- "X-Service-Name" => config.resolved_service_name,
42
- "X-Environment" => config.environment,
43
- }
44
- )
45
-
46
- # Primary processor: batches and exports sampled spans
47
- otel.add_span_processor(
48
- OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)
49
- )
50
-
51
- # Error processor: always exports error spans regardless of
52
- # sampling decision — fires on_finish after status is known
53
- otel.add_span_processor(
54
- Tracelit::ErrorSpanProcessor.new(exporter)
55
- )
56
-
57
- # Auto-instrumentation: instruments Rails, Rack, ActiveRecord,
58
- # Action View, Net::HTTP, Faraday, Redis, Sidekiq, and more.
59
- # use_all() enables every installed instrumentation gem.
60
- otel.use_all
21
+ SETUP_MUTEX.synchronize do
22
+ return if @configured
23
+ return unless config.enabled
24
+
25
+ # Fix 1: Install a clean single-line error handler before any OTel SDK
26
+ # calls so that internal OTel errors never dump raw stack traces into
27
+ # the application's logs.
28
+ OpenTelemetry.error_handler = lambda do |exception:, message:|
29
+ msg = [message, exception&.message].compact.join(" — ")
30
+ OpenTelemetry.logger.warn("[Tracelit] #{msg}")
31
+ end
32
+
33
+ # Fix 2/3: Soft validation — warn and bail out rather than raise.
34
+ # An observability SDK must never crash the host application.
35
+ errors = config.valid?
36
+ if errors.any?
37
+ OpenTelemetry.logger.warn("[Tracelit] disabled — #{errors.join(', ')}")
38
+ return
39
+ end
40
+
41
+ OpenTelemetry::SDK.configure do |otel|
42
+ # Resource attributes identify this service in Tracelit.
43
+ # These populate the `resource` Map column on every telemetry row.
44
+ otel.resource = OpenTelemetry::SDK::Resources::Resource.create(
45
+ {
46
+ OpenTelemetry::SemanticConventions::Resource::SERVICE_NAME => config.resolved_service_name,
47
+ OpenTelemetry::SemanticConventions::Resource::DEPLOYMENT_ENVIRONMENT => config.environment,
48
+ "telemetry.sdk.language" => "ruby",
49
+ "telemetry.sdk.name" => detect_framework,
50
+ "telemetry.sdk.version" => Tracelit::VERSION,
51
+ }.merge(config.resource_attributes)
52
+ )
53
+
54
+ # Build the OTLP exporter once shared by both processors
55
+ exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(
56
+ endpoint: "#{config.endpoint}/v1/traces",
57
+ headers: {
58
+ "Authorization" => "Bearer #{config.api_key}",
59
+ "X-Service-Name" => config.resolved_service_name,
60
+ "X-Environment" => config.environment,
61
+ }
62
+ )
63
+
64
+ # Primary processor: batches and exports sampled spans
65
+ otel.add_span_processor(
66
+ OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)
67
+ )
68
+
69
+ # Error processor: always exports error spans regardless of
70
+ # sampling decision — fires on_finish after status is known
71
+ otel.add_span_processor(
72
+ Tracelit::ErrorSpanProcessor.new(exporter)
73
+ )
74
+
75
+ # Auto-instrumentation: instruments Rails, Rack, ActiveRecord,
76
+ # Action View, Net::HTTP, Faraday, Redis, Sidekiq, and more.
77
+ # use_all() enables every installed instrumentation gem.
78
+ otel.use_all
79
+ end
80
+
81
+ # Set sampler after configure — Configurator does not expose
82
+ # sampler= in OTel SDK 1.x, must be set on the provider directly.
83
+ # Skip at 1.0: the default AlwaysOn sampler is correct and we do not touch it.
84
+ if config.sample_rate < 1.0
85
+ OpenTelemetry.tracer_provider.sampler = error_always_on_sampler(config.sample_rate)
86
+ end
87
+
88
+ @configured = true
89
+ @config = config
90
+
91
+ setup_logs(config)
92
+ Tracelit::Metrics.setup(config)
93
+
94
+ # Fix 5: Fork safety for Puma cluster mode and Unicorn.
95
+ # Background threads (pollers) are killed in forked worker processes.
96
+ # Process._fork (Ruby 3.1+) fires in the child after every fork so we
97
+ # can restart pollers in each worker without touching the master.
98
+ install_fork_hook(config)
99
+
100
+ # Fix 9: Flush and shut down both providers gracefully on process exit
101
+ # so the last metrics/traces batch is not lost during deploys.
102
+ at_exit { shutdown }
61
103
  end
104
+ end
62
105
 
63
- # Set sampler after configure — Configurator does not expose
64
- # sampler= in OTel SDK 1.x, must be set on the provider directly.
65
- # Skip at 1.0: the default AlwaysOn sampler is correct and we do not touch it.
66
- if config.sample_rate < 1.0
67
- OpenTelemetry.tracer_provider.sampler = error_always_on_sampler(config.sample_rate)
106
+ def self.reset!
107
+ SETUP_MUTEX.synchronize do
108
+ @configured = false
109
+ @config = nil
68
110
  end
69
-
70
- @configured = true
71
- setup_logs(config)
72
- Tracelit::Metrics.setup(config)
73
111
  end
74
112
 
75
- def self.reset!
76
- @configured = false
113
+ def self.shutdown
114
+ OpenTelemetry.tracer_provider.shutdown rescue nil
115
+ OpenTelemetry.meter_provider.shutdown rescue nil
77
116
  end
78
117
 
79
118
  private
@@ -124,7 +163,30 @@ module Tracelit
124
163
  # Called here (after Rails boot) so Rails.logger is already initialised.
125
164
  RailsLoggerBridge.install(logger_provider)
126
165
  rescue StandardError => e
127
- OpenTelemetry.logger.warn("Tracelit: failed to set up logs: #{e.message}")
166
+ OpenTelemetry.logger.warn("[Tracelit] failed to set up logs: #{e.message}")
167
+ end
168
+
169
+ # Fix 5: Register a Process._fork hook (Ruby 3.1+) so that background
170
+ # polling threads are restarted inside each forked Puma/Unicorn worker.
171
+ # In the parent (pid != 0) nothing changes. In the child (pid == 0) we
172
+ # restart the metric pollers so each worker reports its own stats.
173
+ def self.install_fork_hook(config)
174
+ return unless Process.respond_to?(:_fork)
175
+
176
+ hook_module = Module.new do
177
+ define_method(:_fork) do
178
+ pid = super()
179
+ if pid == 0
180
+ # We are in the child — restart pollers for this worker
181
+ Tracelit::Metrics.restart_pollers(config)
182
+ end
183
+ pid
184
+ end
185
+ end
186
+
187
+ Process.singleton_class.prepend(hook_module)
188
+ rescue StandardError => e
189
+ OpenTelemetry.logger.warn("[Tracelit] could not install fork hook: #{e.message}")
128
190
  end
129
191
  end
130
192
  end
@@ -4,16 +4,16 @@ require "opentelemetry/metrics"
4
4
  require "opentelemetry-metrics-sdk"
5
5
  require "opentelemetry/exporter/otlp_metrics"
6
6
 
7
+ # Fix 8: Set at module load time with ||= so user-configured values are never
8
+ # clobbered. Moving this out of setup() also prevents the mutation from
9
+ # re-firing on every setup call in test scenarios.
10
+ ENV["OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"] ||= "delta"
11
+
7
12
  module Tracelit
8
13
  module Metrics
9
14
  # Sets up the OpenTelemetry MeterProvider with OTLP exporter.
10
15
  # Called once from Instrumentation.setup after trace setup.
11
16
  def self.setup(config)
12
- # Force delta temporality for all instruments. The SDK aggregation classes
13
- # (Sum, ExplicitBucketHistogram) read this env var at construction time;
14
- # there is no constructor keyword on MetricsExporter for this in v0.8.0.
15
- ENV["OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"] = "delta"
16
-
17
17
  exporter = OpenTelemetry::Exporter::OTLP::Metrics::MetricsExporter.new(
18
18
  endpoint: "#{config.endpoint}/v1/metrics",
19
19
  headers: {
@@ -46,13 +46,25 @@ module Tracelit
46
46
  install_connection_pool_poller if defined?(::ActiveRecord)
47
47
  install_memory_poller
48
48
  rescue StandardError => e
49
- OpenTelemetry.logger.warn("Tracelit: failed to set up metrics: #{e.message}")
49
+ OpenTelemetry.logger.warn("[Tracelit] failed to set up metrics: #{e.message}")
50
50
  end
51
51
 
52
52
  def self.meter
53
53
  @meter
54
54
  end
55
55
 
56
+ # Fix 5 (support): Called from the Process._fork hook in Instrumentation
57
+ # to restart background polling threads inside each forked Puma/Unicorn
58
+ # worker. The parent-process threads are dead in the child; this revives them.
59
+ def self.restart_pollers(config)
60
+ @connection_pool_poller_installed = false
61
+ @memory_poller_installed = false
62
+ install_connection_pool_poller if defined?(::ActiveRecord)
63
+ install_memory_poller
64
+ rescue StandardError => e
65
+ OpenTelemetry.logger.warn("[Tracelit] failed to restart pollers after fork: #{e.message}")
66
+ end
67
+
56
68
  # Exposes a counter for manual instrumentation in user code:
57
69
  # Tracelit::Metrics.counter("orders.placed").add(1)
58
70
  def self.counter(name, description: "", unit: "")
@@ -81,7 +93,13 @@ module Tracelit
81
93
  # http.server.request.duration — histogram in milliseconds
82
94
  # http.server.error.count — counter for 5xx responses
83
95
  # db.query.duration — histogram for ActiveRecord time per request
96
+ #
97
+ # Fix 6: guarded against double-registration so reset! + re-setup in tests
98
+ # or Rails code-reloading scenarios does not duplicate metric counts.
84
99
  def self.install_rails_subscriber
100
+ return if @rails_subscriber_installed
101
+ @rails_subscriber_installed = true
102
+
85
103
  request_counter = @meter.create_counter(
86
104
  "http.server.request.count",
87
105
  description: "Total HTTP requests processed",
@@ -107,12 +125,15 @@ module Tracelit
107
125
  )
108
126
 
109
127
  ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
110
- event = ActiveSupport::Notifications::Event.new(*args)
128
+ event = ActiveSupport::Notifications::Event.new(*args)
111
129
  payload = event.payload
112
130
 
113
131
  attrs = {
132
+ # Fix 7: use controller#action (stable, low-cardinality route template)
133
+ # instead of payload[:path] which contains raw IDs and causes metric
134
+ # cardinality explosion on apps with resource IDs in URLs.
135
+ "http.route" => "#{payload[:controller]}##{payload[:action]}",
114
136
  "http.method" => payload[:method].to_s,
115
- "http.route" => payload[:path].to_s,
116
137
  "http.status_code" => payload[:status].to_s,
117
138
  "controller" => payload[:controller].to_s,
118
139
  "action" => payload[:action].to_s,
@@ -137,7 +158,12 @@ module Tracelit
137
158
  # Installs a Sidekiq server middleware that emits per-job metrics.
138
159
  # Uses a dynamically defined class so the instrument references are
139
160
  # captured in the closure without global state.
161
+ #
162
+ # Fix 6: guarded against double-registration.
140
163
  def self.install_sidekiq_middleware
164
+ return if @sidekiq_middleware_installed
165
+ @sidekiq_middleware_installed = true
166
+
141
167
  job_counter = @meter.create_counter(
142
168
  "sidekiq.job.count",
143
169
  description: "Total Sidekiq jobs processed",
@@ -192,13 +218,18 @@ module Tracelit
192
218
  end
193
219
  end
194
220
  rescue StandardError => e
195
- warn "Tracelit: failed to install Sidekiq middleware: #{e.message}"
221
+ OpenTelemetry.logger.warn("[Tracelit] failed to install Sidekiq middleware: #{e.message}")
196
222
  end
197
223
 
198
224
  # Polls ActiveRecord connection pool stats every 30 seconds on a daemon
199
225
  # thread and records them as gauges. Does not require a live connection
200
226
  # at install time — errors during polling are silently retried next cycle.
227
+ #
228
+ # Fix 11: version-safe pool access that works on Rails 6.0–8.x.
201
229
  def self.install_connection_pool_poller
230
+ return if @connection_pool_poller_installed
231
+ @connection_pool_poller_installed = true
232
+
202
233
  pool_size = @meter.create_gauge(
203
234
  "db.connection_pool.size",
204
235
  description: "Maximum connections in the pool",
@@ -228,9 +259,30 @@ module Tracelit
228
259
  loop do
229
260
  sleep 30
230
261
  begin
231
- pool = ActiveRecord::Base.connection_pool
232
- stat = pool.stat
233
- attrs = { "db.system" => pool.pool_config.db_config.adapter.to_s }
262
+ # Fix 11a: Rails 7.2 soft-deprecated connection_pool on the base
263
+ # class. Use the connection handler when available; fall back for
264
+ # Rails 6.0 compatibility.
265
+ pool = if ActiveRecord::Base.respond_to?(:connection_handler)
266
+ ActiveRecord::Base.connection_handler
267
+ .retrieve_connection_pool(ActiveRecord::Base.connection_specification_name) rescue
268
+ ActiveRecord::Base.connection_pool
269
+ else
270
+ ActiveRecord::Base.connection_pool
271
+ end
272
+
273
+ next unless pool
274
+
275
+ stat = pool.stat
276
+
277
+ # Fix 11b: pool_config.db_config was added in Rails 6.1.
278
+ # Fall back gracefully on older setups.
279
+ adapter = if pool.respond_to?(:pool_config)
280
+ pool.pool_config.db_config.adapter.to_s rescue "unknown"
281
+ else
282
+ "unknown"
283
+ end
284
+
285
+ attrs = { "db.system" => adapter }
234
286
  pool_size.record(stat[:size], attributes: attrs)
235
287
  pool_busy.record(stat[:busy], attributes: attrs)
236
288
  pool_idle.record(stat[:idle], attributes: attrs)
@@ -243,12 +295,19 @@ module Tracelit
243
295
  thread.abort_on_exception = false
244
296
  thread
245
297
  rescue StandardError => e
246
- warn "Tracelit: failed to install connection pool poller: #{e.message}"
298
+ OpenTelemetry.logger.warn("[Tracelit] failed to install connection pool poller: #{e.message}")
247
299
  end
248
300
 
249
- # Polls process RSS memory every 60 seconds on a daemon thread using ps,
250
- # which works on both macOS (arm64-darwin) and Linux without /proc.
301
+ # Polls process RSS memory every 60 seconds on a daemon thread.
302
+ #
303
+ # Fix 12: On Linux use /proc/self/status (always present, no subprocess).
304
+ # Fall back to `ps` on macOS/BSD. The previous implementation always used
305
+ # a shell backtick which spawns a child process and fails silently in
306
+ # minimal Docker containers that lack procps.
251
307
  def self.install_memory_poller
308
+ return if @memory_poller_installed
309
+ @memory_poller_installed = true
310
+
252
311
  memory_gauge = @meter.create_gauge(
253
312
  "process.memory.rss",
254
313
  description: "Process resident set size (RSS)",
@@ -262,7 +321,14 @@ module Tracelit
262
321
  loop do
263
322
  sleep 60
264
323
  begin
265
- rss_kb = `ps -o rss= -p #{pid} 2>/dev/null`.strip.to_i
324
+ rss_kb = if File.exist?("/proc/self/status")
325
+ # Linux: read VmRSS from /proc — no subprocess, always available
326
+ File.read("/proc/self/status")[/VmRSS:\s+(\d+)/, 1].to_i
327
+ else
328
+ # macOS / BSD fallback
329
+ `ps -o rss= -p #{Integer(pid)} 2>/dev/null`.strip.to_i
330
+ end
331
+
266
332
  next if rss_kb == 0
267
333
 
268
334
  rss_mb = rss_kb / 1024.0
@@ -271,14 +337,14 @@ module Tracelit
271
337
  "process.runtime" => "ruby",
272
338
  })
273
339
  rescue StandardError
274
- # Ignore — ps may not be available in all environments
340
+ # Ignore — environment may not support RSS polling
275
341
  end
276
342
  end
277
343
  end
278
344
  thread.abort_on_exception = false
279
345
  thread
280
346
  rescue StandardError => e
281
- warn "Tracelit: failed to install memory poller: #{e.message}"
347
+ OpenTelemetry.logger.warn("[Tracelit] failed to install memory poller: #{e.message}")
282
348
  end
283
349
  end
284
350
  end
@@ -39,7 +39,7 @@ module Tracelit
39
39
  )
40
40
  end
41
41
  rescue StandardError => e
42
- warn "Tracelit: failed to install Rails logger bridge: #{e.message}"
42
+ warn "[Tracelit] failed to install Rails logger bridge: #{e.message}"
43
43
  end
44
44
 
45
45
  # OTelLogger is a Logger subclass whose add method emits an OTel LogRecord
@@ -59,6 +59,14 @@ module Tracelit
59
59
  end
60
60
 
61
61
  def add(severity, message = nil, progname = nil)
62
+ # Fix 10: Re-entrancy guard. OTel SDK internals can call OpenTelemetry.logger
63
+ # which may route back through Rails.logger → this method. Without the
64
+ # guard, that creates a feedback loop where every OTel internal warning
65
+ # triggers another OTel log emit, infinitely.
66
+ return if Thread.current[:tracelit_logging]
67
+
68
+ Thread.current[:tracelit_logging] = true
69
+
62
70
  severity_number = SEVERITY_MAP[severity.to_i] || 9
63
71
  severity_text = ::Logger::SEV_LABEL[severity.to_i] || "ANY"
64
72
 
@@ -76,6 +84,8 @@ module Tracelit
76
84
  )
77
85
  rescue StandardError
78
86
  # Never let OTel errors surface to the application
87
+ ensure
88
+ Thread.current[:tracelit_logging] = nil
79
89
  end
80
90
  alias_method :log, :add
81
91
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tracelit
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
data/tracelit.gemspec CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
14
14
  spec.homepage = "https://tracelit.io"
15
15
  spec.license = "MIT"
16
16
 
17
- spec.required_ruby_version = ">= 3.0"
17
+ spec.required_ruby_version = ">= 3.3"
18
18
 
19
19
  spec.files = Dir[
20
20
  "lib/**/*.rb",
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tracelit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tracelit
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-27 00:00:00.000000000 Z
11
+ date: 2026-04-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: opentelemetry-sdk
@@ -131,7 +131,7 @@ homepage: https://tracelit.io
131
131
  licenses:
132
132
  - MIT
133
133
  metadata: {}
134
- post_install_message:
134
+ post_install_message:
135
135
  rdoc_options: []
136
136
  require_paths:
137
137
  - lib
@@ -139,15 +139,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
139
139
  requirements:
140
140
  - - ">="
141
141
  - !ruby/object:Gem::Version
142
- version: '3.0'
142
+ version: '3.3'
143
143
  required_rubygems_version: !ruby/object:Gem::Requirement
144
144
  requirements:
145
145
  - - ">="
146
146
  - !ruby/object:Gem::Version
147
147
  version: '0'
148
148
  requirements: []
149
- rubygems_version: 3.0.3.1
150
- signing_key:
149
+ rubygems_version: 3.5.22
150
+ signing_key:
151
151
  specification_version: 4
152
152
  summary: Official Ruby SDK for Tracelit backend observability
153
153
  test_files: []