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 +4 -4
- data/lib/tracelit/configuration.rb +58 -9
- data/lib/tracelit/instrumentation.rb +115 -49
- data/lib/tracelit/metrics.rb +84 -18
- data/lib/tracelit/rails_logger_bridge.rb +11 -1
- data/lib/tracelit/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 86262dd99fea01fd4acc26d9f1ab54b5915773a6cceb6b6c8ee6ed7f5dcda873
|
|
4
|
+
data.tar.gz: 0c20478da0332cee72943c947e35fd47a298406672d719cccd2ee7a437261307
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
+
# 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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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.
|
|
76
|
-
|
|
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
|
|
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
|
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
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.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-
|
|
11
|
+
date: 2026-05-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: opentelemetry-sdk
|