tracelit 0.1.1 → 0.1.3

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: 5e3ee121cb2164e53dfa5ba358f760e84686bdbb26c94196ee26fac6ed40f9ca
4
- data.tar.gz: 8eb2b4285c0b360098b412f22a9f629c064cc4b35aa83dff2e5ae095734b6e27
3
+ metadata.gz: 86262dd99fea01fd4acc26d9f1ab54b5915773a6cceb6b6c8ee6ed7f5dcda873
4
+ data.tar.gz: 0c20478da0332cee72943c947e35fd47a298406672d719cccd2ee7a437261307
5
5
  SHA512:
6
- metadata.gz: 25ce2b06f568a1f4553269ffe52366e2b63aab998f03ab2fbb0f33de52a39901ad32268b99a1bd2e61b9cc9c2e81825e66771986741f9176e4a6e4c8890da848
7
- data.tar.gz: 542aa2c3a219425fcfddaa44fada6e37610020a82819f0dfd14808a22fd8abadd0dfe0280cd3f5a7cfdaaaa2662a17658b0105f46baba80a2d9164506e084a12
6
+ metadata.gz: 3a5dfff925583f87d19772888916e24d7864ab4b63b341a53393db72fe0bdf7b4caaaf582a88a5583edb2167f399d498dfd055c76ad42c228ba7c4acbe6baa28
7
+ data.tar.gz: 22143eb50a74f98d630e227e4aa695685b06914ce4ef72b8b5eaa84a142a5da20793a5f1e5fd694fab210dbda53d0bb358c90dbc9a19331b63d5f903f80df9bd
@@ -29,19 +29,68 @@ 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
+ # Resolves the current commit SHA with zero developer friction.
42
+ # Checks common CI/CD environment variables first, then falls back
43
+ # to running `git rev-parse HEAD` in the project directory.
44
+ def resolved_commit_sha
45
+ return @resolved_commit_sha if defined?(@resolved_commit_sha)
46
+
47
+ sha = ENV["COMMIT_SHA"] ||
48
+ ENV["GIT_COMMIT_SHA"] ||
49
+ ENV["GIT_COMMIT"] ||
50
+ ENV["GITHUB_SHA"] ||
51
+ ENV["HEROKU_SLUG_COMMIT"] ||
52
+ ENV["SOURCE_VERSION"] || # Heroku alt
53
+ ENV["RENDER_GIT_COMMIT"] || # Render
54
+ ENV["FLY_APP_VERSION"] || # Fly.io
55
+ ENV["RAILWAY_GIT_COMMIT_SHA"] # Railway
56
+
57
+ if sha.nil? || sha.empty?
58
+ begin
59
+ sha = `git rev-parse HEAD 2>/dev/null`.strip
60
+ sha = nil if sha.empty?
61
+ rescue StandardError
62
+ sha = nil
63
+ end
64
+ end
65
+
66
+ @resolved_commit_sha = sha
67
+ end
68
+
69
+ # Returns an array of human-readable error strings.
70
+ # Empty array means the configuration is valid.
71
+ # Never raises — callers decide whether to warn or abort.
72
+ def valid?
73
+ errors = []
74
+ errors << "api_key is required" if api_key.nil? || api_key.to_s.empty?
75
+
76
+ # Fix 3: check resolved_service_name so Rails apps that rely on automatic
77
+ # name inference (module_parent_name) are not incorrectly flagged.
78
+ if resolved_service_name == "unknown-service"
79
+ errors << "service_name is required (set config.service_name or TRACELIT_SERVICE_NAME)"
80
+ end
81
+
82
+ unless sample_rate.between?(0.0, 1.0)
83
+ errors << "sample_rate must be between 0.0 and 1.0 (got #{sample_rate})"
84
+ end
85
+
86
+ errors
87
+ end
88
+
89
+ # Kept for backwards compatibility. Previously raised ArgumentError;
90
+ # now a no-op because an observability SDK must never crash the host app.
91
+ # Use valid? to check for configuration errors programmatically.
41
92
  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)
93
+ # no-op see valid? for soft validation
45
94
  end
46
95
 
47
96
  # Infer service name from Rails application if not explicitly set.
@@ -12,68 +12,111 @@ 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
- {
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
+ base_attrs = {
28
45
  OpenTelemetry::SemanticConventions::Resource::SERVICE_NAME => config.resolved_service_name,
29
46
  OpenTelemetry::SemanticConventions::Resource::DEPLOYMENT_ENVIRONMENT => config.environment,
30
47
  "telemetry.sdk.language" => "ruby",
31
48
  "telemetry.sdk.name" => detect_framework,
32
49
  "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
50
  }
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
51
+ sha = config.resolved_commit_sha
52
+ base_attrs["service.commit_sha"] = sha if sha
53
+
54
+ otel.resource = OpenTelemetry::SDK::Resources::Resource.create(
55
+ base_attrs.merge(config.resource_attributes)
56
+ )
57
+
58
+ # Build the OTLP exporter once shared by both processors
59
+ exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(
60
+ endpoint: "#{config.endpoint}/v1/traces",
61
+ headers: {
62
+ "Authorization" => "Bearer #{config.api_key}",
63
+ "X-Service-Name" => config.resolved_service_name,
64
+ "X-Environment" => config.environment,
65
+ }
66
+ )
67
+
68
+ # Primary processor: batches and exports sampled spans
69
+ otel.add_span_processor(
70
+ OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)
71
+ )
72
+
73
+ # Error processor: always exports error spans regardless of
74
+ # sampling decision — fires on_finish after status is known
75
+ otel.add_span_processor(
76
+ Tracelit::ErrorSpanProcessor.new(exporter)
77
+ )
78
+
79
+ # Auto-instrumentation: instruments Rails, Rack, ActiveRecord,
80
+ # Action View, Net::HTTP, Faraday, Redis, Sidekiq, and more.
81
+ # use_all() enables every installed instrumentation gem.
82
+ otel.use_all
83
+ end
84
+
85
+ # Set sampler after configure — Configurator does not expose
86
+ # sampler= in OTel SDK 1.x, must be set on the provider directly.
87
+ # Skip at 1.0: the default AlwaysOn sampler is correct and we do not touch it.
88
+ if config.sample_rate < 1.0
89
+ OpenTelemetry.tracer_provider.sampler = error_always_on_sampler(config.sample_rate)
90
+ end
91
+
92
+ @configured = true
93
+ @config = config
94
+
95
+ setup_logs(config)
96
+ Tracelit::Metrics.setup(config)
97
+
98
+ # Fix 5: Fork safety for Puma cluster mode and Unicorn.
99
+ # Background threads (pollers) are killed in forked worker processes.
100
+ # Process._fork (Ruby 3.1+) fires in the child after every fork so we
101
+ # can restart pollers in each worker without touching the master.
102
+ install_fork_hook(config)
103
+
104
+ # Fix 9: Flush and shut down both providers gracefully on process exit
105
+ # so the last metrics/traces batch is not lost during deploys.
106
+ at_exit { shutdown }
61
107
  end
108
+ end
62
109
 
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)
110
+ def self.reset!
111
+ SETUP_MUTEX.synchronize do
112
+ @configured = false
113
+ @config = nil
68
114
  end
69
-
70
- @configured = true
71
- setup_logs(config)
72
- Tracelit::Metrics.setup(config)
73
115
  end
74
116
 
75
- def self.reset!
76
- @configured = false
117
+ def self.shutdown
118
+ OpenTelemetry.tracer_provider.shutdown rescue nil
119
+ OpenTelemetry.meter_provider.shutdown rescue nil
77
120
  end
78
121
 
79
122
  private
@@ -124,7 +167,30 @@ module Tracelit
124
167
  # Called here (after Rails boot) so Rails.logger is already initialised.
125
168
  RailsLoggerBridge.install(logger_provider)
126
169
  rescue StandardError => e
127
- OpenTelemetry.logger.warn("Tracelit: failed to set up logs: #{e.message}")
170
+ OpenTelemetry.logger.warn("[Tracelit] failed to set up logs: #{e.message}")
171
+ end
172
+
173
+ # Fix 5: Register a Process._fork hook (Ruby 3.1+) so that background
174
+ # polling threads are restarted inside each forked Puma/Unicorn worker.
175
+ # In the parent (pid != 0) nothing changes. In the child (pid == 0) we
176
+ # restart the metric pollers so each worker reports its own stats.
177
+ def self.install_fork_hook(config)
178
+ return unless Process.respond_to?(:_fork)
179
+
180
+ hook_module = Module.new do
181
+ define_method(:_fork) do
182
+ pid = super()
183
+ if pid == 0
184
+ # We are in the child — restart pollers for this worker
185
+ Tracelit::Metrics.restart_pollers(config)
186
+ end
187
+ pid
188
+ end
189
+ end
190
+
191
+ Process.singleton_class.prepend(hook_module)
192
+ rescue StandardError => e
193
+ OpenTelemetry.logger.warn("[Tracelit] could not install fork hook: #{e.message}")
128
194
  end
129
195
  end
130
196
  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.1"
4
+ VERSION = "0.1.3"
5
5
  end
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.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tracelit
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-27 00:00:00.000000000 Z
11
+ date: 2026-05-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: opentelemetry-sdk