opentrace 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +175 -0
- data/lib/opentrace/client.rb +168 -0
- data/lib/opentrace/config.rb +43 -0
- data/lib/opentrace/log_forwarder.rb +53 -0
- data/lib/opentrace/logger.rb +118 -0
- data/lib/opentrace/middleware.rb +18 -0
- data/lib/opentrace/rails.rb +233 -0
- data/lib/opentrace/version.rb +5 -0
- data/lib/opentrace.rb +150 -0
- metadata +65 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ba760f415dab6abc4ce324ec9f63aaaa961d4cd115555fce4b4f11fa718c1327
|
|
4
|
+
data.tar.gz: 2d40538689afa5fafa0ab678d5b81f64a2ee2cccbf8b69143f1f0786422b71fa
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c4e5a1c1f97d166b2905f139c9035876a627f0affd604092fead4a64a8795913ab8e4dec1b86c7c2b337ce1fc070c776105e758fb16624fb28e5342bbcf0eb5c
|
|
7
|
+
data.tar.gz: 7f3a034f7fbc791d73201e57a0ac25fe4307a92436873fa0256517887e19a689d11e1d5fd29c9f203894e3847557ebb417f8c9e8d5fff897a1fac7f15b62d4bd
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 OpenTrace
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# OpenTrace Ruby
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "opentrace"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then run:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
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)
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If any required field (`endpoint`, `api_key`, `service`) is missing or empty, the gem **disables itself automatically**. No errors, no logs sent.
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### Direct logging
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
OpenTrace.log("INFO", "User signed in", { user_id: 42, ip: "1.2.3.4" })
|
|
42
|
+
|
|
43
|
+
OpenTrace.log("ERROR", "Payment failed", {
|
|
44
|
+
trace_id: "abc-123",
|
|
45
|
+
user_id: 99,
|
|
46
|
+
exception: {
|
|
47
|
+
class: "Stripe::CardError",
|
|
48
|
+
message: "Your card was declined"
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Pass `trace_id` inside metadata and it will be promoted to a top-level field automatically.
|
|
54
|
+
|
|
55
|
+
### Logger wrapper
|
|
56
|
+
|
|
57
|
+
Wrap any Ruby `Logger` to forward all log output to OpenTrace while keeping the original logger working exactly as before:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
require "logger"
|
|
61
|
+
|
|
62
|
+
logger = Logger.new($stdout)
|
|
63
|
+
logger = OpenTrace::Logger.new(logger)
|
|
64
|
+
|
|
65
|
+
logger.info("This goes to STDOUT and to OpenTrace")
|
|
66
|
+
logger.error("So does this")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
You can attach default metadata to every log from this logger:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
logger = OpenTrace::Logger.new(original_logger, metadata: { component: "worker" })
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Rails
|
|
76
|
+
|
|
77
|
+
In a Rails app, add an initializer:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
# config/initializers/opentrace.rb
|
|
81
|
+
OpenTrace.configure do |c|
|
|
82
|
+
c.endpoint = ENV["OPENTRACE_ENDPOINT"]
|
|
83
|
+
c.api_key = ENV["OPENTRACE_API_KEY"]
|
|
84
|
+
c.service = "my-rails-app"
|
|
85
|
+
c.environment = Rails.env
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The gem auto-detects Rails and will:
|
|
90
|
+
|
|
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`)
|
|
97
|
+
|
|
98
|
+
Requests that return 5xx status codes are logged as `ERROR`, everything else as `INFO`.
|
|
99
|
+
|
|
100
|
+
### TaggedLogging
|
|
101
|
+
|
|
102
|
+
If your wrapped logger uses `ActiveSupport::TaggedLogging`, tags are preserved and injected into the metadata:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
Rails.logger.tagged("RequestID-123") do
|
|
106
|
+
Rails.logger.info("Processing request")
|
|
107
|
+
# metadata will include: { tags: ["RequestID-123"] }
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Runtime controls
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
OpenTrace.enabled? # check if logging is active
|
|
115
|
+
OpenTrace.disable! # turn off (logs are silently dropped)
|
|
116
|
+
OpenTrace.enable! # turn back on
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Graceful shutdown
|
|
120
|
+
|
|
121
|
+
If your app needs a clean shutdown (e.g. a Sidekiq worker), drain the queue before exiting:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
OpenTrace.shutdown(timeout: 5)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
This gives the background thread up to 5 seconds to send any remaining queued logs.
|
|
128
|
+
|
|
129
|
+
## How it works
|
|
130
|
+
|
|
131
|
+
- 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`
|
|
133
|
+
- The thread is started lazily on the first log call -- no threads are created at boot
|
|
134
|
+
- If the queue exceeds 1,000 items, new logs are dropped (oldest are preserved)
|
|
135
|
+
- Payloads larger than 32 KB are dropped
|
|
136
|
+
- All network errors (timeouts, connection refused, DNS failures) are swallowed silently
|
|
137
|
+
- The HTTP timeout defaults to 1 second
|
|
138
|
+
|
|
139
|
+
## Log payload format
|
|
140
|
+
|
|
141
|
+
Each log is sent as a JSON object:
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"timestamp": "2026-02-08T12:41:00.000000Z",
|
|
146
|
+
"level": "ERROR",
|
|
147
|
+
"service": "billing-api",
|
|
148
|
+
"environment": "production",
|
|
149
|
+
"trace_id": "abc-123",
|
|
150
|
+
"message": "PG::UniqueViolation",
|
|
151
|
+
"metadata": {
|
|
152
|
+
"user_id": 42,
|
|
153
|
+
"request_id": "req-456"
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
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 |
|
|
167
|
+
|
|
168
|
+
## Requirements
|
|
169
|
+
|
|
170
|
+
- Ruby 3.0+
|
|
171
|
+
- Rails 6+ (optional, auto-detected)
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module OpenTrace
|
|
8
|
+
class Client
|
|
9
|
+
MAX_QUEUE_SIZE = 1000
|
|
10
|
+
PAYLOAD_MAX_BYTES = 32_768 # 32 KB
|
|
11
|
+
POLL_INTERVAL = 0.05 # 50ms
|
|
12
|
+
|
|
13
|
+
def initialize(config)
|
|
14
|
+
@config = config
|
|
15
|
+
@queue = Thread::Queue.new
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
@thread = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def enqueue(payload)
|
|
21
|
+
return unless @config.enabled?
|
|
22
|
+
|
|
23
|
+
# Drop newest if queue is full
|
|
24
|
+
return if @queue.size >= MAX_QUEUE_SIZE
|
|
25
|
+
|
|
26
|
+
@queue.push(payload)
|
|
27
|
+
ensure_thread_running
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def shutdown(timeout: 5)
|
|
31
|
+
@queue.close
|
|
32
|
+
@thread&.join(timeout)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def ensure_thread_running
|
|
38
|
+
return if @thread&.alive?
|
|
39
|
+
|
|
40
|
+
@mutex.synchronize do
|
|
41
|
+
return if @thread&.alive?
|
|
42
|
+
|
|
43
|
+
@thread = Thread.new { dispatch_loop }
|
|
44
|
+
@thread.abort_on_exception = false
|
|
45
|
+
@thread.report_on_exception = false
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def dispatch_loop
|
|
50
|
+
uri = URI.join(@config.endpoint.chomp("/") + "/", "api/logs")
|
|
51
|
+
|
|
52
|
+
loop do
|
|
53
|
+
batch = drain_queue
|
|
54
|
+
break if batch.nil?
|
|
55
|
+
next if batch.empty?
|
|
56
|
+
|
|
57
|
+
send_batch(uri, batch)
|
|
58
|
+
end
|
|
59
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
60
|
+
# Swallow all errors including thread kill
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def drain_queue
|
|
64
|
+
batch = []
|
|
65
|
+
deadline = Time.now + @config.flush_interval
|
|
66
|
+
|
|
67
|
+
loop do
|
|
68
|
+
if batch.empty?
|
|
69
|
+
# Block until first item arrives or timeout
|
|
70
|
+
item = pop_with_timeout(deadline - Time.now)
|
|
71
|
+
return nil if item.nil? && @queue.closed?
|
|
72
|
+
batch << item if item
|
|
73
|
+
else
|
|
74
|
+
# Non-blocking drain up to batch_size
|
|
75
|
+
while batch.size < @config.batch_size
|
|
76
|
+
begin
|
|
77
|
+
item = @queue.pop(true) # non_block = true
|
|
78
|
+
batch << item
|
|
79
|
+
rescue ThreadError
|
|
80
|
+
break # queue empty
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
break if batch.size >= @config.batch_size
|
|
86
|
+
break if Time.now >= deadline && !batch.empty?
|
|
87
|
+
break if @queue.closed?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
batch
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def pop_with_timeout(timeout)
|
|
94
|
+
deadline = Time.now + [timeout, 0].max
|
|
95
|
+
loop do
|
|
96
|
+
begin
|
|
97
|
+
return @queue.pop(true)
|
|
98
|
+
rescue ThreadError
|
|
99
|
+
return nil if Time.now >= deadline || @queue.closed?
|
|
100
|
+
sleep(POLL_INTERVAL)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
rescue ClosedQueueError
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def send_batch(uri, batch)
|
|
108
|
+
# Apply per-payload truncation
|
|
109
|
+
batch = batch.map { |p| fit_payload(p) }.compact
|
|
110
|
+
return if batch.empty?
|
|
111
|
+
|
|
112
|
+
json = JSON.generate(batch)
|
|
113
|
+
|
|
114
|
+
# If entire batch exceeds limit, split and retry
|
|
115
|
+
if json.bytesize > PAYLOAD_MAX_BYTES
|
|
116
|
+
mid = batch.size / 2
|
|
117
|
+
send_batch(uri, batch[0...mid]) if mid > 0
|
|
118
|
+
send_batch(uri, batch[mid..]) if mid < batch.size
|
|
119
|
+
return
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
http = build_http(uri)
|
|
123
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
124
|
+
request["Authorization"] = "Bearer #{@config.api_key}"
|
|
125
|
+
request["Content-Type"] = "application/json"
|
|
126
|
+
request["User-Agent"] = "opentrace-ruby/#{OpenTrace::VERSION}"
|
|
127
|
+
request.body = json
|
|
128
|
+
|
|
129
|
+
http.request(request)
|
|
130
|
+
rescue StandardError
|
|
131
|
+
# Swallow all network errors silently
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def build_http(uri)
|
|
135
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
136
|
+
http.use_ssl = (uri.scheme == "https")
|
|
137
|
+
http.open_timeout = @config.timeout
|
|
138
|
+
http.read_timeout = @config.timeout
|
|
139
|
+
http.write_timeout = @config.timeout
|
|
140
|
+
http
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def fit_payload(payload)
|
|
144
|
+
json = JSON.generate(payload)
|
|
145
|
+
if json.bytesize > PAYLOAD_MAX_BYTES
|
|
146
|
+
payload = truncate_payload(payload)
|
|
147
|
+
json = JSON.generate(payload)
|
|
148
|
+
return nil if json.bytesize > PAYLOAD_MAX_BYTES
|
|
149
|
+
end
|
|
150
|
+
payload
|
|
151
|
+
rescue StandardError
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def truncate_payload(payload)
|
|
156
|
+
meta = payload[:metadata]&.dup || {}
|
|
157
|
+
|
|
158
|
+
# Truncation priority: remove largest optional fields first
|
|
159
|
+
meta.delete(:backtrace)
|
|
160
|
+
meta.delete(:params)
|
|
161
|
+
meta.delete(:job_arguments)
|
|
162
|
+
meta[:sql] = meta[:sql][0, 200] + "..." if meta[:sql].is_a?(String) && meta[:sql].length > 200
|
|
163
|
+
meta[:exception_message] = meta[:exception_message][0, 200] + "..." if meta[:exception_message].is_a?(String) && meta[:exception_message].length > 200
|
|
164
|
+
|
|
165
|
+
payload.merge(metadata: meta)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenTrace
|
|
4
|
+
class Config
|
|
5
|
+
REQUIRED_FIELDS = %i[endpoint api_key service].freeze
|
|
6
|
+
LEVELS = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 }.freeze
|
|
7
|
+
|
|
8
|
+
attr_accessor :endpoint, :api_key, :service, :environment, :timeout, :enabled,
|
|
9
|
+
:context, :min_level, :hostname, :pid, :git_sha,
|
|
10
|
+
:batch_size, :flush_interval,
|
|
11
|
+
:sql_logging, :sql_duration_threshold_ms
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@endpoint = nil
|
|
15
|
+
@api_key = nil
|
|
16
|
+
@service = nil
|
|
17
|
+
@environment = nil
|
|
18
|
+
@timeout = 1.0
|
|
19
|
+
@enabled = true
|
|
20
|
+
@context = nil # nil | Hash | Proc
|
|
21
|
+
@min_level = :debug # send everything by default
|
|
22
|
+
@hostname = nil
|
|
23
|
+
@pid = nil
|
|
24
|
+
@git_sha = nil
|
|
25
|
+
@batch_size = 50
|
|
26
|
+
@flush_interval = 5.0
|
|
27
|
+
@sql_logging = true
|
|
28
|
+
@sql_duration_threshold_ms = 0.0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def valid?
|
|
32
|
+
REQUIRED_FIELDS.all? { |f| value = send(f); value.is_a?(String) && !value.empty? }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def enabled?
|
|
36
|
+
@enabled && valid?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def min_level_value
|
|
40
|
+
LEVELS[min_level.to_s.downcase.to_sym] || 0
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
module OpenTrace
|
|
6
|
+
# Minimal Logger-compatible class that forwards log messages to OpenTrace.
|
|
7
|
+
# Designed to be used as a broadcast target with Rails 7.1+ BroadcastLogger.
|
|
8
|
+
# Does NOT wrap another logger — its only job is to forward to OpenTrace.
|
|
9
|
+
class LogForwarder < ::Logger
|
|
10
|
+
SEVERITY_MAP = {
|
|
11
|
+
::Logger::DEBUG => "DEBUG",
|
|
12
|
+
::Logger::INFO => "INFO",
|
|
13
|
+
::Logger::WARN => "WARN",
|
|
14
|
+
::Logger::ERROR => "ERROR",
|
|
15
|
+
::Logger::FATAL => "FATAL",
|
|
16
|
+
::Logger::UNKNOWN => "UNKNOWN"
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
super(nil)
|
|
21
|
+
self.level = ::Logger::DEBUG
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def add(severity, message = nil, progname = nil, &block)
|
|
25
|
+
severity ||= ::Logger::UNKNOWN
|
|
26
|
+
return true if severity < level
|
|
27
|
+
|
|
28
|
+
msg = resolve_message(message, progname, &block)
|
|
29
|
+
return true if msg.nil? || (msg.is_a?(String) && msg.strip.empty?)
|
|
30
|
+
|
|
31
|
+
level_str = SEVERITY_MAP.fetch(severity, "UNKNOWN")
|
|
32
|
+
OpenTrace.log(level_str, msg.to_s)
|
|
33
|
+
|
|
34
|
+
true
|
|
35
|
+
rescue StandardError
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def close
|
|
40
|
+
# no-op — nothing to close
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def resolve_message(message, progname, &block)
|
|
46
|
+
if message.nil?
|
|
47
|
+
block ? block.call : progname
|
|
48
|
+
else
|
|
49
|
+
message
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
module OpenTrace
|
|
6
|
+
class Logger < ::Logger
|
|
7
|
+
SEVERITY_MAP = {
|
|
8
|
+
::Logger::DEBUG => "DEBUG",
|
|
9
|
+
::Logger::INFO => "INFO",
|
|
10
|
+
::Logger::WARN => "WARN",
|
|
11
|
+
::Logger::ERROR => "ERROR",
|
|
12
|
+
::Logger::FATAL => "FATAL",
|
|
13
|
+
::Logger::UNKNOWN => "UNKNOWN"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :wrapped_logger
|
|
17
|
+
|
|
18
|
+
def initialize(wrapped_logger, metadata: {})
|
|
19
|
+
@wrapped_logger = wrapped_logger
|
|
20
|
+
@default_metadata = metadata
|
|
21
|
+
# Initialize with nil logdev - we override #add to handle output
|
|
22
|
+
super(nil)
|
|
23
|
+
self.level = wrapped_logger.level if wrapped_logger.respond_to?(:level)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def add(severity, message = nil, progname = nil, &block)
|
|
27
|
+
# Delegate to wrapped logger first (synchronous, as required)
|
|
28
|
+
@wrapped_logger.add(severity, message, progname, &block)
|
|
29
|
+
|
|
30
|
+
# Forward to OpenTrace, never raise
|
|
31
|
+
forward_to_opentrace(severity, message, progname, &block)
|
|
32
|
+
|
|
33
|
+
true
|
|
34
|
+
rescue StandardError
|
|
35
|
+
# Never raise to the host app
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Support TaggedLogging if the wrapped logger uses it
|
|
40
|
+
def tagged(*tags, &block)
|
|
41
|
+
if @wrapped_logger.respond_to?(:tagged)
|
|
42
|
+
@wrapped_logger.tagged(*tags) do
|
|
43
|
+
@current_tags = current_tags_from_wrapped
|
|
44
|
+
block.call(self)
|
|
45
|
+
end
|
|
46
|
+
else
|
|
47
|
+
block.call(self)
|
|
48
|
+
end
|
|
49
|
+
rescue StandardError
|
|
50
|
+
block.call(self)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def flush
|
|
54
|
+
@wrapped_logger.flush if @wrapped_logger.respond_to?(:flush)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def close
|
|
58
|
+
@wrapped_logger.close if @wrapped_logger.respond_to?(:close)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Proxy formatter to wrapped logger
|
|
62
|
+
def formatter
|
|
63
|
+
if @wrapped_logger.respond_to?(:formatter)
|
|
64
|
+
@wrapped_logger.formatter
|
|
65
|
+
else
|
|
66
|
+
super
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def formatter=(f)
|
|
71
|
+
if @wrapped_logger&.respond_to?(:formatter=)
|
|
72
|
+
@wrapped_logger.formatter = f
|
|
73
|
+
else
|
|
74
|
+
super
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def forward_to_opentrace(severity, message, progname, &block)
|
|
81
|
+
return unless OpenTrace.enabled?
|
|
82
|
+
|
|
83
|
+
msg = resolve_message(message, progname, &block)
|
|
84
|
+
return if msg.nil? || (msg.is_a?(String) && msg.strip.empty?)
|
|
85
|
+
|
|
86
|
+
level_str = SEVERITY_MAP.fetch(severity || ::Logger::UNKNOWN, "UNKNOWN")
|
|
87
|
+
|
|
88
|
+
metadata = @default_metadata.dup
|
|
89
|
+
tags = current_tags_from_wrapped
|
|
90
|
+
metadata[:tags] = tags if tags && !tags.empty?
|
|
91
|
+
|
|
92
|
+
OpenTrace.log(level_str, msg.to_s, metadata)
|
|
93
|
+
rescue StandardError
|
|
94
|
+
# Swallow - never affect the host app
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def resolve_message(message, progname, &block)
|
|
98
|
+
if message.nil?
|
|
99
|
+
if block
|
|
100
|
+
block.call
|
|
101
|
+
else
|
|
102
|
+
progname
|
|
103
|
+
end
|
|
104
|
+
else
|
|
105
|
+
message
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def current_tags_from_wrapped
|
|
110
|
+
if @wrapped_logger.respond_to?(:formatter) &&
|
|
111
|
+
@wrapped_logger.formatter.respond_to?(:current_tags)
|
|
112
|
+
@wrapped_logger.formatter.current_tags.dup
|
|
113
|
+
end
|
|
114
|
+
rescue StandardError
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenTrace
|
|
4
|
+
class Middleware
|
|
5
|
+
def initialize(app)
|
|
6
|
+
@app = app
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(env)
|
|
10
|
+
request_id = env["action_dispatch.request_id"] || env["HTTP_X_REQUEST_ID"]
|
|
11
|
+
OpenTrace.current_request_id = request_id
|
|
12
|
+
|
|
13
|
+
@app.call(env)
|
|
14
|
+
ensure
|
|
15
|
+
OpenTrace.current_request_id = nil
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
if defined?(::Rails::Railtie)
|
|
4
|
+
module OpenTrace
|
|
5
|
+
class Railtie < ::Rails::Railtie
|
|
6
|
+
# Use config.after_initialize so that config/initializers/ files
|
|
7
|
+
# (where the user calls OpenTrace.configure) have already run.
|
|
8
|
+
# Register middleware early — before the stack is frozen
|
|
9
|
+
initializer "opentrace.middleware" do |app|
|
|
10
|
+
app.middleware.use OpenTrace::Middleware
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
config.after_initialize do |app|
|
|
14
|
+
next unless OpenTrace.enabled?
|
|
15
|
+
|
|
16
|
+
if Rails.logger.respond_to?(:broadcast_to)
|
|
17
|
+
# Rails 7.1+: register as a broadcast target (non-invasive)
|
|
18
|
+
Rails.logger.broadcast_to(OpenTrace::LogForwarder.new)
|
|
19
|
+
else
|
|
20
|
+
# Pre-7.1 fallback: wrap the logger directly
|
|
21
|
+
if app.config.logger
|
|
22
|
+
app.config.logger = OpenTrace::Logger.new(app.config.logger)
|
|
23
|
+
Rails.logger = app.config.logger
|
|
24
|
+
elsif Rails.logger
|
|
25
|
+
Rails.logger = OpenTrace::Logger.new(Rails.logger)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Subscribe to controller request notifications
|
|
30
|
+
ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
|
|
31
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
32
|
+
forward_request_log(event)
|
|
33
|
+
rescue StandardError
|
|
34
|
+
# Swallow - never affect the host app
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Subscribe to SQL query notifications
|
|
38
|
+
if OpenTrace.config.sql_logging
|
|
39
|
+
ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
|
|
40
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
41
|
+
forward_sql_log(event)
|
|
42
|
+
rescue StandardError
|
|
43
|
+
# Swallow
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Subscribe to ActiveJob notifications
|
|
48
|
+
ActiveSupport::Notifications.subscribe("perform.active_job") do |*args|
|
|
49
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
50
|
+
forward_job_log(event)
|
|
51
|
+
rescue StandardError
|
|
52
|
+
# Swallow
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class << self
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def forward_request_log(event)
|
|
60
|
+
return unless OpenTrace.enabled?
|
|
61
|
+
|
|
62
|
+
payload = event.payload
|
|
63
|
+
metadata = {
|
|
64
|
+
request_id: payload[:headers]&.env&.dig("action_dispatch.request_id"),
|
|
65
|
+
controller: payload[:controller],
|
|
66
|
+
action: payload[:action],
|
|
67
|
+
method: payload[:method],
|
|
68
|
+
path: payload[:path],
|
|
69
|
+
status: payload[:status],
|
|
70
|
+
duration_ms: event.duration&.round(1)
|
|
71
|
+
}.compact
|
|
72
|
+
|
|
73
|
+
# Attempt to capture current user ID if available
|
|
74
|
+
user_id = extract_user_id(payload)
|
|
75
|
+
metadata[:user_id] = user_id if user_id
|
|
76
|
+
|
|
77
|
+
# Exception auto-capture
|
|
78
|
+
if payload[:exception]
|
|
79
|
+
metadata[:exception_class] = payload[:exception][0]
|
|
80
|
+
metadata[:exception_message] = truncate(payload[:exception][1], 500)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if payload[:exception_object]&.backtrace
|
|
84
|
+
cleaned = clean_backtrace(payload[:exception_object].backtrace)
|
|
85
|
+
metadata[:backtrace] = cleaned.first(15)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Filtered request params
|
|
89
|
+
extract_params(payload, metadata)
|
|
90
|
+
|
|
91
|
+
level = if payload[:exception]
|
|
92
|
+
"ERROR"
|
|
93
|
+
elsif payload[:status].to_i >= 500
|
|
94
|
+
"ERROR"
|
|
95
|
+
elsif payload[:status].to_i >= 400
|
|
96
|
+
"WARN"
|
|
97
|
+
else
|
|
98
|
+
"INFO"
|
|
99
|
+
end
|
|
100
|
+
message = "#{payload[:method]} #{payload[:path]} #{payload[:status]} #{event.duration&.round(1)}ms"
|
|
101
|
+
|
|
102
|
+
OpenTrace.log(level, message, metadata)
|
|
103
|
+
rescue StandardError
|
|
104
|
+
# Swallow
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def forward_job_log(event)
|
|
108
|
+
return unless OpenTrace.enabled?
|
|
109
|
+
|
|
110
|
+
payload = event.payload
|
|
111
|
+
job = payload[:job]
|
|
112
|
+
|
|
113
|
+
metadata = {
|
|
114
|
+
job_class: job.class.name,
|
|
115
|
+
job_id: job.respond_to?(:job_id) ? job.job_id : nil,
|
|
116
|
+
queue_name: job.respond_to?(:queue_name) ? job.queue_name : nil,
|
|
117
|
+
executions: job.respond_to?(:executions) ? job.executions : nil,
|
|
118
|
+
duration_ms: event.duration&.round(1)
|
|
119
|
+
}.compact
|
|
120
|
+
|
|
121
|
+
# Capture arguments (truncated)
|
|
122
|
+
if job.respond_to?(:arguments)
|
|
123
|
+
args_json = JSON.generate(job.arguments)
|
|
124
|
+
metadata[:job_arguments] = if args_json.bytesize > 512
|
|
125
|
+
args_json[0, 512] + "..."
|
|
126
|
+
else
|
|
127
|
+
job.arguments
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Capture exceptions from job failures
|
|
132
|
+
if payload[:exception_object]
|
|
133
|
+
metadata[:exception_class] = payload[:exception_object].class.name
|
|
134
|
+
metadata[:exception_message] = truncate(payload[:exception_object].message, 500)
|
|
135
|
+
if payload[:exception_object].backtrace
|
|
136
|
+
metadata[:backtrace] = clean_backtrace(payload[:exception_object].backtrace).first(15)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
level = payload[:exception_object] ? "ERROR" : "INFO"
|
|
141
|
+
message = if payload[:exception_object]
|
|
142
|
+
"Job #{job.class.name} FAILED (attempt #{job.respond_to?(:executions) ? job.executions : '?'})"
|
|
143
|
+
else
|
|
144
|
+
"Job #{job.class.name} completed #{event.duration&.round(1)}ms"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
OpenTrace.log(level, message, metadata)
|
|
148
|
+
rescue StandardError
|
|
149
|
+
# Swallow
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def forward_sql_log(event)
|
|
153
|
+
return unless OpenTrace.enabled?
|
|
154
|
+
|
|
155
|
+
payload = event.payload
|
|
156
|
+
duration = event.duration&.round(2)
|
|
157
|
+
threshold = OpenTrace.config.sql_duration_threshold_ms
|
|
158
|
+
|
|
159
|
+
# Skip if below threshold
|
|
160
|
+
return if threshold > 0 && duration && duration < threshold
|
|
161
|
+
|
|
162
|
+
# Skip SCHEMA queries (migrations, structure dumps)
|
|
163
|
+
return if payload[:name] == "SCHEMA"
|
|
164
|
+
|
|
165
|
+
metadata = {
|
|
166
|
+
sql_name: payload[:name],
|
|
167
|
+
sql: truncate(payload[:sql], 1000),
|
|
168
|
+
sql_duration_ms: duration,
|
|
169
|
+
sql_cached: payload[:cached] || false
|
|
170
|
+
}.compact
|
|
171
|
+
|
|
172
|
+
# Extract table name from SQL for easier filtering
|
|
173
|
+
if payload[:sql] =~ /\b(?:FROM|INTO|UPDATE|JOIN)\s+[`"]?(\w+)[`"]?/i
|
|
174
|
+
metadata[:sql_table] = $1
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
level = (duration && duration > 1000) ? "WARN" : "DEBUG"
|
|
178
|
+
message = "SQL #{payload[:name]} #{duration}ms"
|
|
179
|
+
|
|
180
|
+
OpenTrace.log(level, message, metadata)
|
|
181
|
+
rescue StandardError
|
|
182
|
+
# Swallow
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def extract_user_id(payload)
|
|
186
|
+
controller = payload[:controller_instance]
|
|
187
|
+
return unless controller
|
|
188
|
+
|
|
189
|
+
if controller.respond_to?(:current_user, true)
|
|
190
|
+
user = controller.send(:current_user)
|
|
191
|
+
user.respond_to?(:id) ? user.id : nil
|
|
192
|
+
end
|
|
193
|
+
rescue StandardError
|
|
194
|
+
nil
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def extract_params(payload, metadata)
|
|
198
|
+
controller = payload[:controller_instance]
|
|
199
|
+
return unless controller
|
|
200
|
+
|
|
201
|
+
if controller.respond_to?(:request, true) && controller.request.respond_to?(:filtered_parameters)
|
|
202
|
+
params = controller.request.filtered_parameters
|
|
203
|
+
params = params.except("controller", "action")
|
|
204
|
+
metadata[:params] = truncate_hash(params, 2048) unless params.empty?
|
|
205
|
+
end
|
|
206
|
+
rescue StandardError
|
|
207
|
+
# Swallow
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def truncate(str, max)
|
|
211
|
+
return str if str.nil? || str.length <= max
|
|
212
|
+
str[0, max] + "..."
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def clean_backtrace(backtrace)
|
|
216
|
+
if defined?(::Rails) && ::Rails.respond_to?(:backtrace_cleaner)
|
|
217
|
+
::Rails.backtrace_cleaner.clean(backtrace)
|
|
218
|
+
else
|
|
219
|
+
backtrace.reject { |line| line.include?("/gems/") }
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def truncate_hash(hash, max_bytes)
|
|
224
|
+
json = JSON.generate(hash)
|
|
225
|
+
return hash if json.bytesize <= max_bytes
|
|
226
|
+
{ _truncated: true, _size: json.bytesize }
|
|
227
|
+
rescue StandardError
|
|
228
|
+
{ _truncated: true }
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
data/lib/opentrace.rb
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require_relative "opentrace/version"
|
|
5
|
+
require_relative "opentrace/config"
|
|
6
|
+
require_relative "opentrace/client"
|
|
7
|
+
require_relative "opentrace/logger"
|
|
8
|
+
require_relative "opentrace/log_forwarder"
|
|
9
|
+
require_relative "opentrace/middleware"
|
|
10
|
+
|
|
11
|
+
module OpenTrace
|
|
12
|
+
LEVEL_VALUES = { "DEBUG" => 0, "INFO" => 1, "WARN" => 2, "ERROR" => 3, "FATAL" => 4 }.freeze
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def configure
|
|
16
|
+
yield config
|
|
17
|
+
reset_client!
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def config
|
|
21
|
+
@config ||= Config.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def log(level, message, metadata = {})
|
|
25
|
+
return unless enabled?
|
|
26
|
+
return unless level_meets_threshold?(level)
|
|
27
|
+
|
|
28
|
+
# 1. Start with user-defined context (lowest priority)
|
|
29
|
+
meta = resolve_context
|
|
30
|
+
|
|
31
|
+
# 2. Merge caller-provided metadata (overrides context)
|
|
32
|
+
meta.merge!(metadata) if metadata.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
# 3. Static context — only fills in keys not already set
|
|
35
|
+
static_context.each { |k, v| meta[k] ||= v }
|
|
36
|
+
|
|
37
|
+
# 4. Request ID from middleware (if not already set by caller or context)
|
|
38
|
+
meta[:request_id] ||= current_request_id if current_request_id
|
|
39
|
+
|
|
40
|
+
# Extract trace_id to top level before building payload
|
|
41
|
+
trace_id = meta.delete(:trace_id)
|
|
42
|
+
|
|
43
|
+
payload = {
|
|
44
|
+
timestamp: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%6NZ"),
|
|
45
|
+
level: level.to_s.upcase,
|
|
46
|
+
service: config.service,
|
|
47
|
+
environment: config.environment,
|
|
48
|
+
message: message.to_s,
|
|
49
|
+
metadata: meta.compact
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
payload[:trace_id] = trace_id.to_s if trace_id
|
|
53
|
+
|
|
54
|
+
client.enqueue(payload)
|
|
55
|
+
rescue StandardError
|
|
56
|
+
# Never raise to the host app
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def error(exception, metadata = {})
|
|
60
|
+
return unless enabled?
|
|
61
|
+
|
|
62
|
+
meta = metadata.is_a?(Hash) ? metadata.dup : {}
|
|
63
|
+
meta[:exception_class] = exception.class.name
|
|
64
|
+
meta[:exception_message] = exception.message&.slice(0, 500)
|
|
65
|
+
|
|
66
|
+
if exception.backtrace
|
|
67
|
+
cleaned = if defined?(::Rails) && ::Rails.respond_to?(:backtrace_cleaner)
|
|
68
|
+
::Rails.backtrace_cleaner.clean(exception.backtrace)
|
|
69
|
+
else
|
|
70
|
+
exception.backtrace.reject { |l| l.include?("/gems/") }
|
|
71
|
+
end
|
|
72
|
+
meta[:backtrace] = cleaned.first(15)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
log("ERROR", exception.message.to_s, meta)
|
|
76
|
+
rescue StandardError
|
|
77
|
+
# Never raise to the host app
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def enabled?
|
|
81
|
+
config.enabled?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def disable!
|
|
85
|
+
config.enabled = false
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def enable!
|
|
89
|
+
config.enabled = true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def current_request_id
|
|
93
|
+
Thread.current[:opentrace_request_id]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def current_request_id=(id)
|
|
97
|
+
Thread.current[:opentrace_request_id] = id
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def shutdown(timeout: 5)
|
|
101
|
+
@client&.shutdown(timeout: timeout)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def reset!
|
|
105
|
+
shutdown(timeout: 1)
|
|
106
|
+
@config = nil
|
|
107
|
+
@client = nil
|
|
108
|
+
@static_context = nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def client
|
|
114
|
+
@client ||= Client.new(config)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def reset_client!
|
|
118
|
+
@client&.shutdown(timeout: 1)
|
|
119
|
+
@client = nil
|
|
120
|
+
@static_context = nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def level_meets_threshold?(level)
|
|
124
|
+
LEVEL_VALUES[level.to_s.upcase].to_i >= config.min_level_value
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def static_context
|
|
128
|
+
@static_context ||= {
|
|
129
|
+
hostname: config.hostname || Socket.gethostname,
|
|
130
|
+
pid: config.pid || Process.pid,
|
|
131
|
+
git_sha: config.git_sha || ENV["REVISION"] || ENV["GIT_SHA"] || ENV["HEROKU_SLUG_COMMIT"]
|
|
132
|
+
}.compact
|
|
133
|
+
rescue StandardError
|
|
134
|
+
{}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def resolve_context
|
|
138
|
+
ctx = case config.context
|
|
139
|
+
when Proc then config.context.call
|
|
140
|
+
when Hash then config.context
|
|
141
|
+
end
|
|
142
|
+
ctx.is_a?(Hash) ? ctx.dup : {}
|
|
143
|
+
rescue StandardError
|
|
144
|
+
{} # Broken proc? Swallow, never crash.
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Auto-load Rails integration if Rails is present
|
|
150
|
+
require_relative "opentrace/rails" if defined?(::Rails::Railtie)
|
metadata
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: opentrace
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- OpenTrace
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: logger
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
description: Forwards structured application logs to an OpenTrace server over HTTP.
|
|
27
|
+
Designed to never affect application behavior or uptime.
|
|
28
|
+
email:
|
|
29
|
+
- hello@opentrace.dev
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- LICENSE
|
|
35
|
+
- README.md
|
|
36
|
+
- lib/opentrace.rb
|
|
37
|
+
- lib/opentrace/client.rb
|
|
38
|
+
- lib/opentrace/config.rb
|
|
39
|
+
- lib/opentrace/log_forwarder.rb
|
|
40
|
+
- lib/opentrace/logger.rb
|
|
41
|
+
- lib/opentrace/middleware.rb
|
|
42
|
+
- lib/opentrace/rails.rb
|
|
43
|
+
- lib/opentrace/version.rb
|
|
44
|
+
homepage: https://github.com/adham90/opentrace-ruby
|
|
45
|
+
licenses:
|
|
46
|
+
- MIT
|
|
47
|
+
metadata: {}
|
|
48
|
+
rdoc_options: []
|
|
49
|
+
require_paths:
|
|
50
|
+
- lib
|
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: 3.0.0
|
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
requirements: []
|
|
62
|
+
rubygems_version: 4.0.3
|
|
63
|
+
specification_version: 4
|
|
64
|
+
summary: Thin, safe Ruby client for OpenTrace log ingestion
|
|
65
|
+
test_files: []
|