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 +4 -4
- data/README.md +1 -81
- data/lib/tracelit/configuration.rb +30 -9
- data/lib/tracelit/instrumentation.rb +117 -55
- data/lib/tracelit/metrics.rb +84 -18
- data/lib/tracelit/rails_logger_bridge.rb +11 -1
- data/lib/tracelit/version.rb +1 -1
- data/tracelit.gemspec +1 -1
- metadata +7 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bcea7249159775e00167a1a64ca7160132286dcc622c6c49a118541a8e58d66e
|
|
4
|
+
data.tar.gz: 0ea2e6000cd63cb87be8f0859f0e2bbc1c371e15e7ab33592c3c8070fd806205
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
33
|
-
@service_name
|
|
34
|
-
@environment
|
|
35
|
-
@endpoint
|
|
36
|
-
@sample_rate
|
|
37
|
-
@enabled
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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.
|
|
76
|
-
|
|
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
|
|
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
|
data/lib/tracelit/metrics.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
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
|
|
250
|
-
#
|
|
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 =
|
|
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 —
|
|
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
|
|
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
|
|
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
|
|
data/lib/tracelit/version.rb
CHANGED
data/tracelit.gemspec
CHANGED
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.
|
|
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-
|
|
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.
|
|
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.
|
|
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: []
|