opentrace 0.1.0 → 0.2.0

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: ba760f415dab6abc4ce324ec9f63aaaa961d4cd115555fce4b4f11fa718c1327
4
- data.tar.gz: 2d40538689afa5fafa0ab678d5b81f64a2ee2cccbf8b69143f1f0786422b71fa
3
+ metadata.gz: b4a10bb3e35d7526cb02f721c1040ad9f568fa6058d17d1e7aaf1bf31d42ecd6
4
+ data.tar.gz: 64f7700651c457151c9d9a961a8bba205cf88d0746265e6eebaa7f928e2c0f9b
5
5
  SHA512:
6
- metadata.gz: c4e5a1c1f97d166b2905f139c9035876a627f0affd604092fead4a64a8795913ab8e4dec1b86c7c2b337ce1fc070c776105e758fb16624fb28e5342bbcf0eb5c
7
- data.tar.gz: 7f3a034f7fbc791d73201e57a0ac25fe4307a92436873fa0256517887e19a689d11e1d5fd29c9f203894e3847557ebb417f8c9e8d5fff897a1fac7f15b62d4bd
6
+ metadata.gz: fcd303032ee0ae96aeb6602ffdd3a60df2972e58cfff1c6499f5cc17ea57c89a0ffe6e5e092a9eb7c0ccb55f798afcfbce13f34ac789e70908d890a42da5699f
7
+ data.tar.gz: 54f2c8bfd37f9a36b9f32aa49b76a6846335ab0063e804fb35aa491892d18d4a560c410e9204b667ebd5e57cfe522114d677f8973b788a897084a6ce12e62bb1
data/README.md CHANGED
@@ -1,8 +1,29 @@
1
1
  # OpenTrace Ruby
2
2
 
3
- A thin, safe Ruby client that forwards structured application logs to an [OpenTrace](https://github.com/opentrace/opentrace) server over HTTP.
4
-
5
- **This gem will never crash or slow down your application.** All network errors are swallowed silently. If the server is unreachable, logs are dropped.
3
+ [![Gem Version](https://badge.fury.io/rb/opentrace.svg)](https://rubygems.org/gems/opentrace)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
+
6
+ A thin, safe Ruby client that forwards structured application logs to an [OpenTrace](https://github.com/adham90/opentrace-ruby) server over HTTP.
7
+
8
+ **This gem will never crash or slow down your application.** All network errors are swallowed silently. If the server is unreachable, logs are dropped -- your app continues running normally.
9
+
10
+ ## Features
11
+
12
+ - **Zero-risk integration** -- all errors swallowed, never raises to host app
13
+ - **Async dispatch** -- logs are queued in-memory and sent via a background thread
14
+ - **Batch sending** -- groups logs into configurable batches for efficient network usage
15
+ - **Bounded queue** -- caps at 1,000 entries to prevent memory bloat
16
+ - **Smart truncation** -- oversized payloads are truncated instead of silently dropped
17
+ - **Rails integration** -- auto-instruments controllers, SQL queries, and ActiveJob via Railtie
18
+ - **Rack middleware** -- propagates `request_id` via thread-local storage
19
+ - **Logger wrapper** -- drop-in replacement that forwards to OpenTrace while keeping your original logger
20
+ - **Rails 7.1+ BroadcastLogger** -- native support via `broadcast_to`
21
+ - **TaggedLogging** -- preserves `ActiveSupport::TaggedLogging` tags in metadata
22
+ - **Context support** -- attach global metadata to every log via Hash or Proc
23
+ - **Level filtering** -- `min_level` config to control which severities are forwarded
24
+ - **Auto-enrichment** -- every log includes `hostname`, `pid`, and `git_sha` automatically
25
+ - **Exception helper** -- `OpenTrace.error` captures class, message, and cleaned backtrace
26
+ - **Runtime controls** -- enable/disable logging at runtime without restarting
6
27
 
7
28
  ## Installation
8
29
 
@@ -18,24 +39,77 @@ Then run:
18
39
  bundle install
19
40
  ```
20
41
 
42
+ Or install directly:
43
+
44
+ ```bash
45
+ gem install opentrace
46
+ ```
47
+
48
+ ## Quick Start
49
+
50
+ ```ruby
51
+ OpenTrace.configure do |c|
52
+ c.endpoint = "https://opentrace.example.com"
53
+ c.api_key = ENV["OPENTRACE_API_KEY"]
54
+ c.service = "my-app"
55
+ end
56
+
57
+ OpenTrace.log("INFO", "User signed in", { user_id: 42 })
58
+ ```
59
+
60
+ That's it. Logs are queued and sent asynchronously -- your code never blocks.
61
+
21
62
  ## Configuration
22
63
 
23
64
  ```ruby
24
65
  OpenTrace.configure do |c|
25
- c.endpoint = "https://opentrace.example.com" # required
26
- c.api_key = ENV["OPENTRACE_API_KEY"] # required
27
- c.service = "billing-api" # required
28
- c.environment = "production" # optional
29
- c.timeout = 1.0 # optional, seconds (default: 1.0)
30
- c.enabled = true # optional (default: true)
66
+ # Required
67
+ c.endpoint = "https://opentrace.example.com"
68
+ c.api_key = ENV["OPENTRACE_API_KEY"]
69
+ c.service = "billing-api"
70
+
71
+ # Optional
72
+ c.environment = "production" # default: nil
73
+ c.timeout = 1.0 # HTTP timeout in seconds (default: 1.0)
74
+ c.enabled = true # default: true
75
+ c.min_level = :info # minimum level to forward (default: :debug)
76
+ c.batch_size = 50 # logs per batch (default: 50)
77
+ c.flush_interval = 5.0 # seconds between flushes (default: 5.0)
78
+
79
+ # Global context -- attached to every log entry
80
+ c.context = { deploy_version: "v1.2.3" }
81
+ # Or use a Proc for dynamic context:
82
+ c.context = -> { { tenant_id: Current.tenant&.id } }
83
+
84
+ # Auto-populated (override if needed)
85
+ c.hostname = Socket.gethostname # auto-detected
86
+ c.pid = Process.pid # auto-detected
87
+ c.git_sha = ENV["REVISION"] # checks REVISION, GIT_SHA, HEROKU_SLUG_COMMIT
88
+
89
+ # SQL logging (Rails only)
90
+ c.sql_logging = true # default: true
91
+ c.sql_duration_threshold_ms = 100.0 # only log queries slower than this (default: 0.0 = all)
31
92
  end
32
93
  ```
33
94
 
34
95
  If any required field (`endpoint`, `api_key`, `service`) is missing or empty, the gem **disables itself automatically**. No errors, no logs sent.
35
96
 
97
+ ### Level Filtering
98
+
99
+ Control which log levels are forwarded with `min_level`:
100
+
101
+ ```ruby
102
+ OpenTrace.configure do |c|
103
+ # ...
104
+ c.min_level = :warn # only forward WARN, ERROR, and FATAL
105
+ end
106
+ ```
107
+
108
+ Available levels: `:debug`, `:info`, `:warn`, `:error`, `:fatal`
109
+
36
110
  ## Usage
37
111
 
38
- ### Direct logging
112
+ ### Direct Logging
39
113
 
40
114
  ```ruby
41
115
  OpenTrace.log("INFO", "User signed in", { user_id: 42, ip: "1.2.3.4" })
@@ -52,7 +126,24 @@ OpenTrace.log("ERROR", "Payment failed", {
52
126
 
53
127
  Pass `trace_id` inside metadata and it will be promoted to a top-level field automatically.
54
128
 
55
- ### Logger wrapper
129
+ ### Exception Logging
130
+
131
+ Use `OpenTrace.error` to log exceptions with automatic class, message, and backtrace extraction:
132
+
133
+ ```ruby
134
+ begin
135
+ dangerous_operation
136
+ rescue => e
137
+ OpenTrace.error(e, { user_id: current_user.id, action: "checkout" })
138
+ end
139
+ ```
140
+
141
+ This captures:
142
+ - `exception_class` -- the exception class name
143
+ - `exception_message` -- truncated to 500 characters
144
+ - `backtrace` -- cleaned (Rails backtrace cleaner or gem-filtered), limited to 15 frames
145
+
146
+ ### Logger Wrapper
56
147
 
57
148
  Wrap any Ruby `Logger` to forward all log output to OpenTrace while keeping the original logger working exactly as before:
58
149
 
@@ -66,13 +157,35 @@ logger.info("This goes to STDOUT and to OpenTrace")
66
157
  logger.error("So does this")
67
158
  ```
68
159
 
69
- You can attach default metadata to every log from this logger:
160
+ Attach default metadata to every log from this logger:
70
161
 
71
162
  ```ruby
72
163
  logger = OpenTrace::Logger.new(original_logger, metadata: { component: "worker" })
164
+ logger.info("Processing job")
165
+ # metadata: { component: "worker" }
73
166
  ```
74
167
 
75
- ### Rails
168
+ ### Global Context
169
+
170
+ Attach metadata to **every** log entry using `config.context`:
171
+
172
+ ```ruby
173
+ # Static context
174
+ OpenTrace.configure do |c|
175
+ # ...
176
+ c.context = { deploy_version: "v1.2.3", region: "us-east-1" }
177
+ end
178
+
179
+ # Dynamic context (evaluated on each log call)
180
+ OpenTrace.configure do |c|
181
+ # ...
182
+ c.context = -> { { request_id: Thread.current[:request_id], tenant: Current.tenant&.slug } }
183
+ end
184
+ ```
185
+
186
+ Context has the lowest priority -- caller-provided metadata overrides context values.
187
+
188
+ ## Rails Integration
76
189
 
77
190
  In a Rails app, add an initializer:
78
191
 
@@ -86,29 +199,97 @@ OpenTrace.configure do |c|
86
199
  end
87
200
  ```
88
201
 
89
- The gem auto-detects Rails and will:
202
+ The gem auto-detects Rails and provides the following integrations automatically:
203
+
204
+ ### Rack Middleware
205
+
206
+ Automatically inserted into the middleware stack. Captures `request_id` from `action_dispatch.request_id` or `HTTP_X_REQUEST_ID` and makes it available via `OpenTrace.current_request_id`. All logs within a request automatically include the `request_id`.
207
+
208
+ ### Logger Wrapping
209
+
210
+ - **Rails 7.1+**: Uses `BroadcastLogger#broadcast_to` to register as a broadcast target (non-invasive)
211
+ - **Pre-7.1**: Wraps `Rails.logger` with `OpenTrace::Logger` which delegates to the original and forwards to OpenTrace
212
+
213
+ All your existing `Rails.logger.info(...)` calls automatically get forwarded to OpenTrace.
214
+
215
+ ### Controller Subscriber
216
+
217
+ Subscribes to `process_action.action_controller` and captures:
218
+
219
+ | Field | Description |
220
+ |---|---|
221
+ | `request_id` | From ActionDispatch |
222
+ | `controller` | Controller class name |
223
+ | `action` | Action name |
224
+ | `method` | HTTP method (GET, POST, etc.) |
225
+ | `path` | Request path |
226
+ | `status` | HTTP response status code |
227
+ | `duration_ms` | Request duration in milliseconds |
228
+ | `user_id` | Auto-captured if controller responds to `current_user` |
229
+ | `params` | Filtered request parameters (respects `filter_parameters`) |
230
+ | `exception_class` | Exception class (if raised) |
231
+ | `exception_message` | Exception message (if raised) |
232
+ | `backtrace` | Cleaned backtrace (if exception raised) |
233
+
234
+ Log levels are set automatically:
235
+ - **ERROR** -- exceptions or 5xx status
236
+ - **WARN** -- 4xx status
237
+ - **INFO** -- everything else
238
+
239
+ ### SQL Query Subscriber
240
+
241
+ Subscribes to `sql.active_record` and logs every query with:
242
+
243
+ | Field | Description |
244
+ |---|---|
245
+ | `sql_name` | Query name (e.g., "User Load") |
246
+ | `sql` | Query text (truncated to 1000 chars) |
247
+ | `sql_duration_ms` | Query duration in milliseconds |
248
+ | `sql_cached` | Whether the result was cached |
249
+ | `sql_table` | Extracted table name for filtering |
250
+
251
+ SCHEMA queries (migrations, structure dumps) are automatically skipped. Queries over 1 second are logged as `WARN`, all others as `DEBUG`.
252
+
253
+ Configure SQL logging:
254
+
255
+ ```ruby
256
+ OpenTrace.configure do |c|
257
+ # ...
258
+ c.sql_logging = true # enable/disable (default: true)
259
+ c.sql_duration_threshold_ms = 100.0 # only log slow queries (default: 0.0 = all)
260
+ end
261
+ ```
262
+
263
+ ### ActiveJob Subscriber
264
+
265
+ Subscribes to `perform.active_job` and logs every job execution with:
90
266
 
91
- - Wrap `Rails.logger` so all log output is forwarded to OpenTrace
92
- - Subscribe to `process_action.action_controller` notifications to capture:
93
- - `request_id`
94
- - `controller` and `action`
95
- - `method`, `path`, `status`, `duration_ms`
96
- - `user_id` (if your controller responds to `current_user`)
267
+ | Field | Description |
268
+ |---|---|
269
+ | `job_class` | Job class name |
270
+ | `job_id` | Unique job ID |
271
+ | `queue_name` | Queue the job ran on |
272
+ | `executions` | Attempt number |
273
+ | `duration_ms` | Execution duration |
274
+ | `job_arguments` | Serialized arguments (truncated to 512 bytes) |
275
+ | `exception_class` | Exception class (if failed) |
276
+ | `exception_message` | Exception message (if failed) |
277
+ | `backtrace` | Cleaned backtrace (if failed) |
97
278
 
98
- Requests that return 5xx status codes are logged as `ERROR`, everything else as `INFO`.
279
+ Failed jobs are logged as `ERROR`, successful jobs as `INFO`.
99
280
 
100
281
  ### TaggedLogging
101
282
 
102
283
  If your wrapped logger uses `ActiveSupport::TaggedLogging`, tags are preserved and injected into the metadata:
103
284
 
104
285
  ```ruby
105
- Rails.logger.tagged("RequestID-123") do
286
+ Rails.logger.tagged("RequestID-123", "UserID-42") do
106
287
  Rails.logger.info("Processing request")
107
- # metadata will include: { tags: ["RequestID-123"] }
288
+ # metadata: { tags: ["RequestID-123", "UserID-42"] }
108
289
  end
109
290
  ```
110
291
 
111
- ## Runtime controls
292
+ ## Runtime Controls
112
293
 
113
294
  ```ruby
114
295
  OpenTrace.enabled? # check if logging is active
@@ -116,7 +297,7 @@ OpenTrace.disable! # turn off (logs are silently dropped)
116
297
  OpenTrace.enable! # turn back on
117
298
  ```
118
299
 
119
- ## Graceful shutdown
300
+ ## Graceful Shutdown
120
301
 
121
302
  If your app needs a clean shutdown (e.g. a Sidekiq worker), drain the queue before exiting:
122
303
 
@@ -126,19 +307,24 @@ OpenTrace.shutdown(timeout: 5)
126
307
 
127
308
  This gives the background thread up to 5 seconds to send any remaining queued logs.
128
309
 
129
- ## How it works
310
+ ## How It Works
311
+
312
+ ```
313
+ Your App --log()--> [In-Memory Queue] --background thread--> POST /api/logs --> OpenTrace Server
314
+ ```
130
315
 
131
316
  - Logs are serialized to JSON and pushed onto an in-memory queue
132
- - A single background thread reads from the queue and sends each payload via `POST /api/logs`
317
+ - A single background thread reads from the queue and sends batches via `POST /api/logs`
133
318
  - The thread is started lazily on the first log call -- no threads are created at boot
134
319
  - If the queue exceeds 1,000 items, new logs are dropped (oldest are preserved)
135
- - Payloads larger than 32 KB are dropped
320
+ - Payloads exceeding 32 KB are intelligently truncated (backtrace, params, SQL removed first)
321
+ - If still too large after truncation, the payload is split and retried in smaller batches
136
322
  - All network errors (timeouts, connection refused, DNS failures) are swallowed silently
137
323
  - The HTTP timeout defaults to 1 second
138
324
 
139
- ## Log payload format
325
+ ## Log Payload Format
140
326
 
141
- Each log is sent as a JSON object:
327
+ Each log is sent as a JSON object to `POST /api/logs`:
142
328
 
143
329
  ```json
144
330
  {
@@ -150,26 +336,31 @@ Each log is sent as a JSON object:
150
336
  "message": "PG::UniqueViolation",
151
337
  "metadata": {
152
338
  "user_id": 42,
153
- "request_id": "req-456"
339
+ "request_id": "req-456",
340
+ "hostname": "web-01",
341
+ "pid": 12345,
342
+ "git_sha": "a1b2c3d"
154
343
  }
155
344
  }
156
345
  ```
157
346
 
158
- | Field | Type | Required |
159
- |---------------|--------|----------|
160
- | `timestamp` | string | yes |
161
- | `level` | string | yes |
162
- | `message` | string | yes |
163
- | `service` | string | no |
164
- | `environment` | string | no |
165
- | `trace_id` | string | no |
166
- | `metadata` | object | no |
347
+ | Field | Type | Required |
348
+ |---|---|---|
349
+ | `timestamp` | string (ISO 8601) | yes |
350
+ | `level` | string | yes |
351
+ | `message` | string | yes |
352
+ | `service` | string | no |
353
+ | `environment` | string | no |
354
+ | `trace_id` | string | no |
355
+ | `metadata` | object | no |
356
+
357
+ The server accepts a single JSON object or an array of objects.
167
358
 
168
359
  ## Requirements
169
360
 
170
- - Ruby 3.0+
171
- - Rails 6+ (optional, auto-detected)
361
+ - Ruby >= 3.0
362
+ - Rails >= 6 (optional, auto-detected)
172
363
 
173
364
  ## License
174
365
 
175
- MIT
366
+ [MIT](LICENSE)
@@ -15,11 +15,14 @@ module OpenTrace
15
15
  @queue = Thread::Queue.new
16
16
  @mutex = Mutex.new
17
17
  @thread = nil
18
+ @pid = Process.pid
18
19
  end
19
20
 
20
21
  def enqueue(payload)
21
22
  return unless @config.enabled?
22
23
 
24
+ reset_after_fork! if forked?
25
+
23
26
  # Drop newest if queue is full
24
27
  return if @queue.size >= MAX_QUEUE_SIZE
25
28
 
@@ -34,15 +37,34 @@ module OpenTrace
34
37
 
35
38
  private
36
39
 
40
+ def forked?
41
+ Process.pid != @pid
42
+ end
43
+
44
+ def reset_after_fork!
45
+ # After fork, the old thread/queue/mutex from the parent are dead.
46
+ # Re-create everything cleanly in the child process.
47
+ @pid = Process.pid
48
+ @queue = Thread::Queue.new
49
+ @mutex = Mutex.new
50
+ @thread = nil
51
+ end
52
+
37
53
  def ensure_thread_running
38
54
  return if @thread&.alive?
39
55
 
40
- @mutex.synchronize do
56
+ # Use try_lock so we never block the calling thread.
57
+ # If another thread is already spawning the dispatch thread, skip —
58
+ # the next enqueue will see it alive.
59
+ return unless @mutex.try_lock
60
+ begin
41
61
  return if @thread&.alive?
42
62
 
43
63
  @thread = Thread.new { dispatch_loop }
44
64
  @thread.abort_on_exception = false
45
65
  @thread.report_on_exception = false
66
+ ensure
67
+ @mutex.unlock
46
68
  end
47
69
  end
48
70
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenTrace
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/opentrace.rb CHANGED
@@ -90,11 +90,11 @@ module OpenTrace
90
90
  end
91
91
 
92
92
  def current_request_id
93
- Thread.current[:opentrace_request_id]
93
+ Fiber[:opentrace_request_id]
94
94
  end
95
95
 
96
96
  def current_request_id=(id)
97
- Thread.current[:opentrace_request_id] = id
97
+ Fiber[:opentrace_request_id] = id
98
98
  end
99
99
 
100
100
  def shutdown(timeout: 5)
@@ -106,12 +106,23 @@ module OpenTrace
106
106
  @config = nil
107
107
  @client = nil
108
108
  @static_context = nil
109
+ @at_exit_registered = nil
109
110
  end
110
111
 
111
112
  private
112
113
 
113
114
  def client
114
- @client ||= Client.new(config)
115
+ @client ||= begin
116
+ c = Client.new(config)
117
+ register_at_exit_hook!
118
+ c
119
+ end
120
+ end
121
+
122
+ def register_at_exit_hook!
123
+ return if @at_exit_registered
124
+ @at_exit_registered = true
125
+ at_exit { OpenTrace.shutdown(timeout: 2) }
115
126
  end
116
127
 
117
128
  def reset_client!
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opentrace
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenTrace