errorgap 0.5.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +7 -0
- data/lib/errorgap/rack_middleware.rb +24 -12
- data/lib/errorgap/transaction.rb +2 -1
- data/lib/errorgap/version.rb +1 -1
- data/lib/errorgap.rb +60 -28
- 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: 75cf1e4a725d5e1c7f484a577814d71403fbdc4e6c6cd4bcec3d0eca30f00478
|
|
4
|
+
data.tar.gz: 98d336466f4eceedc7ad6fc03fff038704d10872e30c6b7f5376528e424dd73b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 44e3afd205bce67d34e40e6b71a5c67b43615dc04acc299f7188f0e01d514844e7b836523a0dcae0e95388b7b38875c76f2ffc012d3a83b316450182b5a2fb36
|
|
7
|
+
data.tar.gz: b8b2d440abe90925b5750cf6526b34449d00d913d5bd47a8d74339ce8ba9c5622abe55949b66465ad1f1f0b0972ee26446c258086a98280c94dcdff36b66a229
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.6.0] - 2026-10-03
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Errors link to their request.** Every APM transaction (Rack middleware,
|
|
13
|
+
`track_transaction`, `track_job`) now has an id, sent as the transaction's
|
|
14
|
+
`id`, and errors reported while it runs carry it as
|
|
15
|
+
`context.transaction_id`. Errorgap shows the error a request actually raised
|
|
16
|
+
on its trace instead of matching by route and time, and links each
|
|
17
|
+
occurrence to its trace. The id is fiber-local, so concurrent requests never
|
|
18
|
+
share one, and is sent even when APM sampling drops the transaction.
|
|
19
|
+
`Errorgap.current_transaction_id` and `Errorgap.with_transaction_id` expose
|
|
20
|
+
it for custom instrumentation.
|
|
21
|
+
|
|
8
22
|
## [0.5.0] - 2026-07-20
|
|
9
23
|
|
|
10
24
|
### Added
|
data/README.md
CHANGED
|
@@ -90,6 +90,13 @@ end
|
|
|
90
90
|
Enable with `config.apm_enabled = true` (and optionally `config.apm_sample_rate`).
|
|
91
91
|
Call `Errorgap.flush` before process exit to drain async deliveries.
|
|
92
92
|
|
|
93
|
+
Every transaction gets an id, and errors reported while it runs — from the
|
|
94
|
+
middleware, `Errorgap.notify`, or inside `track_transaction`/`track_job` —
|
|
95
|
+
carry it as `context.transaction_id`. Errorgap then shows the error a request
|
|
96
|
+
actually raised on its trace, and links each occurrence to its request.
|
|
97
|
+
`Errorgap.current_transaction_id` returns the id in effect (fiber-local), and
|
|
98
|
+
`Errorgap.with_transaction_id { |id| ... }` scopes one around custom work.
|
|
99
|
+
|
|
93
100
|
## Rack
|
|
94
101
|
|
|
95
102
|
```ruby
|
|
@@ -7,30 +7,42 @@ module Errorgap
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def call(env)
|
|
10
|
-
|
|
11
|
-
SpanCollector.start if apm_enabled?
|
|
10
|
+
return call_without_transaction(env) unless apm_enabled?
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
# Errors raised while this request runs carry its transaction id.
|
|
13
|
+
Errorgap.with_transaction_id do |transaction_id|
|
|
14
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
15
|
+
SpanCollector.start
|
|
16
|
+
begin
|
|
17
|
+
status, headers, body = @app.call(env)
|
|
18
|
+
[status, headers, body]
|
|
19
|
+
rescue Exception => exception # rubocop:disable Lint/RescueException
|
|
20
|
+
notify_once(env, exception)
|
|
21
|
+
raise
|
|
22
|
+
ensure
|
|
23
|
+
elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000.0
|
|
24
|
+
record_transaction(env, status || 500, elapsed_ms, transaction_id)
|
|
25
|
+
end
|
|
22
26
|
end
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
private
|
|
26
30
|
|
|
31
|
+
def call_without_transaction(env)
|
|
32
|
+
@app.call(env)
|
|
33
|
+
rescue Exception => exception # rubocop:disable Lint/RescueException
|
|
34
|
+
notify_once(env, exception)
|
|
35
|
+
raise
|
|
36
|
+
end
|
|
37
|
+
|
|
27
38
|
def apm_enabled?
|
|
28
39
|
Errorgap.configuration.apm_enabled
|
|
29
40
|
end
|
|
30
41
|
|
|
31
|
-
def record_transaction(env, status_code, elapsed_ms)
|
|
42
|
+
def record_transaction(env, status_code, elapsed_ms, transaction_id)
|
|
32
43
|
spans = SpanCollector.flush
|
|
33
44
|
txn = Transaction.new(
|
|
45
|
+
id: transaction_id,
|
|
34
46
|
kind: "web",
|
|
35
47
|
method: env["REQUEST_METHOD"],
|
|
36
48
|
path: route_pattern(env),
|
data/lib/errorgap/transaction.rb
CHANGED
|
@@ -17,13 +17,14 @@ module Errorgap
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
Transaction = Struct.new(
|
|
20
|
-
:kind, :method, :path, :path_raw, :status_code,
|
|
20
|
+
:id, :kind, :method, :path, :path_raw, :status_code,
|
|
21
21
|
:duration_ms, :environment, :occurred_at, :spans,
|
|
22
22
|
:job_class, :queue,
|
|
23
23
|
keyword_init: true
|
|
24
24
|
) do
|
|
25
25
|
def to_h
|
|
26
26
|
{
|
|
27
|
+
id: id,
|
|
27
28
|
kind: kind,
|
|
28
29
|
method: method,
|
|
29
30
|
path: path,
|
data/lib/errorgap/version.rb
CHANGED
data/lib/errorgap.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
3
5
|
require_relative "errorgap/configuration"
|
|
4
6
|
require_relative "errorgap/breadcrumbs"
|
|
5
7
|
require_relative "errorgap/notifier"
|
|
@@ -43,6 +45,10 @@ module Errorgap
|
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
def notify(error, context: {}, environment: {}, session: {}, params: {}, sync: false)
|
|
48
|
+
# The request or job this error was raised in, so errorgap links the two.
|
|
49
|
+
if (transaction_id = current_transaction_id) && !context.key?(:transaction_id) && !context.key?("transaction_id")
|
|
50
|
+
context = context.merge(transaction_id: transaction_id)
|
|
51
|
+
end
|
|
46
52
|
notifier.notify(
|
|
47
53
|
error,
|
|
48
54
|
context: context,
|
|
@@ -75,6 +81,26 @@ module Errorgap
|
|
|
75
81
|
)
|
|
76
82
|
end
|
|
77
83
|
|
|
84
|
+
TRANSACTION_ID_KEY = :errorgap_transaction_id
|
|
85
|
+
|
|
86
|
+
# The id of the APM transaction running on this fiber, if any. Notices
|
|
87
|
+
# reported while it is set carry it as `context.transaction_id`.
|
|
88
|
+
def current_transaction_id
|
|
89
|
+
Thread.current[TRANSACTION_ID_KEY]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Run the block as one transaction: a new id is current for its duration
|
|
93
|
+
# (fiber-local, so concurrent requests never share one) and the previous
|
|
94
|
+
# one is restored after. Yields the id.
|
|
95
|
+
def with_transaction_id
|
|
96
|
+
previous = Thread.current[TRANSACTION_ID_KEY]
|
|
97
|
+
id = SecureRandom.uuid
|
|
98
|
+
Thread.current[TRANSACTION_ID_KEY] = id
|
|
99
|
+
yield id
|
|
100
|
+
ensure
|
|
101
|
+
Thread.current[TRANSACTION_ID_KEY] = previous
|
|
102
|
+
end
|
|
103
|
+
|
|
78
104
|
# Deliver a prebuilt APM transaction, honoring apm_enabled and sampling.
|
|
79
105
|
def notify_transaction(transaction, sync: false)
|
|
80
106
|
return unless configuration.apm_enabled
|
|
@@ -92,39 +118,45 @@ module Errorgap
|
|
|
92
118
|
# receives a SpanRecorder for manual DB/HTTP spans; any automatic
|
|
93
119
|
# `sql.active_record` spans recorded during the block are merged in.
|
|
94
120
|
def track_transaction(method: nil, path: nil, path_raw: nil, status_code: nil, kind: "web", environment: nil, sync: false)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
121
|
+
with_transaction_id do |transaction_id|
|
|
122
|
+
SpanCollector.start
|
|
123
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
124
|
+
occurred = Time.now
|
|
125
|
+
begin
|
|
126
|
+
yield(SpanRecorder.new) if block_given?
|
|
127
|
+
ensure
|
|
128
|
+
elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000.0
|
|
129
|
+
transaction = Transaction.new(
|
|
130
|
+
id: transaction_id,
|
|
131
|
+
kind: kind, method: method, path: path, path_raw: path_raw,
|
|
132
|
+
status_code: status_code, duration_ms: elapsed_ms.round(2),
|
|
133
|
+
environment: environment || configuration.environment,
|
|
134
|
+
occurred_at: occurred, spans: SpanCollector.flush
|
|
135
|
+
)
|
|
136
|
+
notify_transaction(transaction, sync: sync)
|
|
137
|
+
end
|
|
109
138
|
end
|
|
110
139
|
end
|
|
111
140
|
|
|
112
141
|
# Time a background job and deliver it as a `job` transaction.
|
|
113
142
|
def track_job(job_class, queue: "default", environment: nil, sync: false)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
143
|
+
with_transaction_id do |transaction_id|
|
|
144
|
+
SpanCollector.start
|
|
145
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
146
|
+
occurred = Time.now
|
|
147
|
+
begin
|
|
148
|
+
yield(SpanRecorder.new) if block_given?
|
|
149
|
+
ensure
|
|
150
|
+
elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000.0
|
|
151
|
+
transaction = Transaction.new(
|
|
152
|
+
id: transaction_id,
|
|
153
|
+
kind: "job", job_class: job_class, queue: queue,
|
|
154
|
+
duration_ms: elapsed_ms.round(2),
|
|
155
|
+
environment: environment || configuration.environment,
|
|
156
|
+
occurred_at: occurred, spans: SpanCollector.flush
|
|
157
|
+
)
|
|
158
|
+
notify_transaction(transaction, sync: sync)
|
|
159
|
+
end
|
|
128
160
|
end
|
|
129
161
|
end
|
|
130
162
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: errorgap
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Errorgap
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|