auditea 0.1.0.beta.2 → 0.1.0.beta.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/CHANGELOG.md +15 -1
- data/README.md +10 -2
- data/RELEASE.md +5 -5
- data/lib/auditea/instrumentation/active_job.rb +3 -0
- data/lib/auditea/instrumentation/error_subscriber.rb +93 -0
- data/lib/auditea/instrumentation/exception.rb +134 -16
- data/lib/auditea/instrumentation/structural_request.rb +136 -0
- data/lib/auditea/middleware/context.rb +2 -0
- data/lib/auditea/railtie.rb +1 -0
- data/lib/auditea/version.rb +1 -1
- data/lib/auditea.rb +21 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 587d3b57a35018f55c5e82c15a485715913e911a714971797a47fde50d788251
|
|
4
|
+
data.tar.gz: beae506f6609e933baa4d4513e9015cc7b11c919a75ed83ba912eeb546aae0e5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16e6ebde275b1f03f12028ca1c746fdedd9efb907706560ecae52b1fed188e90944f0e827c40e7a1cb909742d381fe37d26deb385ee66d82965c8185444ac99f
|
|
7
|
+
data.tar.gz: e0c7097c1f146d724befdaf2df850e30a556c670a06406f459003b46a2b52fc77687b36d3fa366d3db4662e13718ddf70ea12698480555858bf51650ee9781c8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.0.beta.
|
|
3
|
+
## 0.1.0.beta.3 (unreleased)
|
|
4
|
+
|
|
5
|
+
### Exception capture parity (AUD-ERROR-PARITY01)
|
|
6
|
+
|
|
7
|
+
- Public `Auditea.capture_exception(error, handled:, mechanism:, context:, metadata:)` → `exception.raised`
|
|
8
|
+
- Rails `ErrorReporter` subscriber via public `subscribe` API (Rails 7.2 / 8.1); graceful no-op when absent
|
|
9
|
+
- Identity-safe WeakMap dedupe of exception objects (no integer `object_id` reuse false negatives); cleared at request/job boundaries
|
|
10
|
+
- When `CurrentContext` is already cleared (post-unwind `application.action_dispatch`), derive method/path/request_id/controller/action from the ErrorReporter controller/request via public APIs only
|
|
11
|
+
- `process_action.action_controller` remains as fallback when ErrorReporter is not installed
|
|
12
|
+
- Structured evidence: type, message, bounded frames (≤40) with deterministic `in_app` when knowable, `handled`, `mechanism`, HTTP path/method (no query secrets), controller/action when available
|
|
13
|
+
- ActiveJob: ErrorReporter sources matching `active_job` are skipped when `capture_active_job` is enabled (dedicated `job.failed` path unchanged)
|
|
14
|
+
- Real Rails 7.2 / 8.1 integration coverage for unhandled/handled controller exceptions and ActiveJob no-duplicate behavior
|
|
15
|
+
- Provenance unchanged: sdk/ruby/rails versions + configured `release`
|
|
16
|
+
|
|
17
|
+
## 0.1.0.beta.2
|
|
4
18
|
|
|
5
19
|
### Runtime / dependency inventory (opt-in, default OFF)
|
|
6
20
|
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Ruby / Rails SDK for [AudiTea](https://github.com/reznis/AudiTea) event collection.
|
|
4
4
|
|
|
5
|
-
Version: **0.1.0.beta.
|
|
5
|
+
Version: **0.1.0.beta.3** (not published yet)
|
|
6
6
|
|
|
7
7
|
Captures durable technical and business evidence. AudiTea **server** owns taxonomy, dictionary, human labels, vulnerability matching, correlation, and reinterpretation.
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@ The SDK does **not** perform vulnerability scanning, OS package/CVE inventory, d
|
|
|
12
12
|
|
|
13
13
|
```ruby
|
|
14
14
|
# Gemfile
|
|
15
|
-
gem "auditea", "0.1.0.beta.
|
|
15
|
+
gem "auditea", "0.1.0.beta.3"
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
```bash
|
|
@@ -114,9 +114,17 @@ Auditea.capture(
|
|
|
114
114
|
evidence: { business: { from_status: "draft", to_status: "approved" } }
|
|
115
115
|
)
|
|
116
116
|
|
|
117
|
+
Auditea.capture_exception(
|
|
118
|
+
error,
|
|
119
|
+
handled: true,
|
|
120
|
+
mechanism: "application_rescue"
|
|
121
|
+
)
|
|
122
|
+
|
|
117
123
|
Auditea.log(:info, "Export finished", context: { export_id: export.id })
|
|
118
124
|
```
|
|
119
125
|
|
|
126
|
+
Automatic Rails exception observation uses `Rails.error` (ErrorReporter) when available, with `process_action.action_controller` as fallback. Same-exception dedupe is request/thread-local by object id so ErrorReporter and controller notifications do not double-count.
|
|
127
|
+
|
|
120
128
|
Wire format follows AudiTea **EVENT_CONTRACT_V1**:
|
|
121
129
|
|
|
122
130
|
- Bearer Source credential authentication
|
data/RELEASE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Release: auditea 0.1.0.beta.
|
|
1
|
+
# Release: auditea 0.1.0.beta.3
|
|
2
2
|
|
|
3
3
|
Authoritative runbook for controlled beta publication of the Ruby SDK.
|
|
4
4
|
|
|
@@ -153,10 +153,10 @@ PR status may say `READY FOR HUMAN PUBLICATION REVIEW` while still listing exter
|
|
|
153
153
|
```bash
|
|
154
154
|
git checkout main
|
|
155
155
|
git pull
|
|
156
|
-
# confirm VERSION and CHANGELOG still say 0.1.0.beta.
|
|
156
|
+
# confirm VERSION and CHANGELOG still say 0.1.0.beta.3
|
|
157
157
|
gem build auditea.gemspec
|
|
158
158
|
# inspect package once more
|
|
159
|
-
gem push auditea-0.1.0.beta.
|
|
159
|
+
gem push auditea-0.1.0.beta.3.gem
|
|
160
160
|
```
|
|
161
161
|
|
|
162
162
|
Then mark CHANGELOG released with the publication date in a follow-up commit.
|
|
@@ -166,9 +166,9 @@ Then mark CHANGELOG released with the publication date in a follow-up commit.
|
|
|
166
166
|
## Post-publication verification
|
|
167
167
|
|
|
168
168
|
```bash
|
|
169
|
-
gem install auditea -v 0.1.0.beta.
|
|
169
|
+
gem install auditea -v 0.1.0.beta.3 --pre
|
|
170
170
|
ruby -rauditea -e 'puts Auditea::VERSION'
|
|
171
|
-
# expect: 0.1.0.beta.
|
|
171
|
+
# expect: 0.1.0.beta.3
|
|
172
172
|
```
|
|
173
173
|
|
|
174
174
|
Prefer installing the RubyGems artifact (not a git branch) into the controlled TEST app.
|
|
@@ -23,10 +23,13 @@ module Auditea
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
::ActiveJob::Base.around_perform do |job, block|
|
|
26
|
+
Auditea::Instrumentation::Exception::Deduper.clear!
|
|
26
27
|
block.call
|
|
27
28
|
rescue StandardError => error
|
|
28
29
|
Auditea::Instrumentation::ActiveJob.report_unhandled(job, error)
|
|
29
30
|
raise
|
|
31
|
+
ensure
|
|
32
|
+
Auditea::Instrumentation::Exception::Deduper.clear!
|
|
30
33
|
end
|
|
31
34
|
end
|
|
32
35
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Auditea
|
|
4
|
+
module Instrumentation
|
|
5
|
+
# Rails / ActiveSupport::ErrorReporter subscriber (public API only).
|
|
6
|
+
# Installed when Rails.error / ActiveSupport.error_reporter#subscribe exists.
|
|
7
|
+
class ErrorSubscriber
|
|
8
|
+
ACTIVE_JOB_SOURCE = /active_job/i
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def install!
|
|
12
|
+
return if @installed
|
|
13
|
+
|
|
14
|
+
reporter = resolve_reporter
|
|
15
|
+
return unless reporter.respond_to?(:subscribe)
|
|
16
|
+
|
|
17
|
+
@subscriber = new
|
|
18
|
+
reporter.subscribe(@subscriber)
|
|
19
|
+
@installed = true
|
|
20
|
+
true
|
|
21
|
+
rescue StandardError => error
|
|
22
|
+
Auditea.handle_error(error)
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def installed?
|
|
27
|
+
!!@installed
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def reset_for_tests!
|
|
31
|
+
reporter = resolve_reporter
|
|
32
|
+
reporter.unsubscribe(@subscriber) if @subscriber && reporter.respond_to?(:unsubscribe)
|
|
33
|
+
@subscriber = nil
|
|
34
|
+
@installed = false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def resolve_reporter
|
|
38
|
+
if defined?(::Rails) && ::Rails.respond_to?(:error)
|
|
39
|
+
::Rails.error
|
|
40
|
+
elsif defined?(::ActiveSupport) && ::ActiveSupport.respond_to?(:error_reporter)
|
|
41
|
+
::ActiveSupport.error_reporter
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# ActiveSupport::ErrorReporter subscriber contract — must never raise.
|
|
47
|
+
def report(error, handled:, severity: :error, context: {}, source: nil)
|
|
48
|
+
return unless Auditea.configuration.capture_exceptions
|
|
49
|
+
return if skip_active_job?(source)
|
|
50
|
+
|
|
51
|
+
structural = StructuralRequest.from_reporter_context(context)
|
|
52
|
+
|
|
53
|
+
Auditea::Instrumentation::Exception.capture(
|
|
54
|
+
error,
|
|
55
|
+
handled: handled,
|
|
56
|
+
mechanism: mechanism_for(source),
|
|
57
|
+
severity: map_severity(severity, handled),
|
|
58
|
+
evidence: structural["evidence"],
|
|
59
|
+
context: structural["context"],
|
|
60
|
+
metadata: {
|
|
61
|
+
"sdk_event_kind" => "auto_observed"
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
rescue StandardError => error
|
|
65
|
+
Auditea.handle_error(error)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def skip_active_job?(source)
|
|
71
|
+
return false unless source.to_s.match?(ACTIVE_JOB_SOURCE)
|
|
72
|
+
return true if Auditea.configuration.capture_active_job
|
|
73
|
+
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def mechanism_for(source)
|
|
78
|
+
value = source.to_s.strip
|
|
79
|
+
value.empty? ? "rails.error_reporter" : value
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def map_severity(severity, handled)
|
|
83
|
+
case severity.to_s
|
|
84
|
+
when "warning" then "warning"
|
|
85
|
+
when "info" then "info"
|
|
86
|
+
when "error" then "error"
|
|
87
|
+
else
|
|
88
|
+
handled ? "warning" : "error"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -2,28 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
module Auditea
|
|
4
4
|
module Instrumentation
|
|
5
|
+
# Structured exception → `exception.raised` evidence.
|
|
6
|
+
#
|
|
7
|
+
# Capture paths (fail-open at the Auditea boundary):
|
|
8
|
+
# - explicit Auditea.capture_exception
|
|
9
|
+
# - Rails ErrorReporter subscriber (preferred when available)
|
|
10
|
+
# - process_action.action_controller fallback when ErrorReporter is absent
|
|
5
11
|
module Exception
|
|
12
|
+
MAX_FRAMES = 40
|
|
13
|
+
MESSAGE_MAX = 500
|
|
14
|
+
|
|
6
15
|
module_function
|
|
7
16
|
|
|
17
|
+
def capture(error, handled: false, mechanism: nil, severity: "error",
|
|
18
|
+
context: nil, metadata: nil, evidence: nil, claim: true)
|
|
19
|
+
return { ok: false, skipped: true, reason: "disabled" } unless Auditea.configuration.capture_exceptions
|
|
20
|
+
return { ok: false, skipped: true, reason: "not_an_exception" } unless error.is_a?(::Exception)
|
|
21
|
+
return { ok: false, skipped: true, reason: "duplicate" } if claim && !Deduper.claim!(error)
|
|
22
|
+
|
|
23
|
+
merged_evidence = merge_evidence(error, handled: handled, mechanism: mechanism, evidence: evidence)
|
|
24
|
+
meta = { "sdk_event_kind" => "auto_observed" }
|
|
25
|
+
meta.merge!(stringify_keys(metadata)) if metadata
|
|
26
|
+
|
|
27
|
+
Auditea.capture(
|
|
28
|
+
"exception.raised",
|
|
29
|
+
category: "error",
|
|
30
|
+
severity: severity || "error",
|
|
31
|
+
evidence: merged_evidence,
|
|
32
|
+
context: context,
|
|
33
|
+
metadata: meta
|
|
34
|
+
)
|
|
35
|
+
rescue StandardError => error
|
|
36
|
+
Auditea.handle_error(error)
|
|
37
|
+
end
|
|
38
|
+
|
|
8
39
|
def capture_from_notification(event)
|
|
40
|
+
# Prefer Rails ErrorReporter when installed so handled/source semantics stay
|
|
41
|
+
# consistent and ActionDispatch does not double-emit with process_action.
|
|
42
|
+
return if ErrorSubscriber.installed?
|
|
43
|
+
|
|
9
44
|
payload = event.payload || {}
|
|
10
45
|
exception = payload[:exception_object]
|
|
11
46
|
exception ||= build_from_array(payload[:exception])
|
|
12
47
|
return unless exception
|
|
13
48
|
|
|
14
|
-
frames = safe_frames(exception)
|
|
15
49
|
path = payload[:path].to_s.split("?", 2).first
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
severity: "error",
|
|
50
|
+
capture(
|
|
51
|
+
exception,
|
|
52
|
+
handled: false,
|
|
53
|
+
mechanism: "action_controller.process_action",
|
|
21
54
|
evidence: {
|
|
22
|
-
"exception" => {
|
|
23
|
-
"type" => exception.class.name,
|
|
24
|
-
"message" => Sanitizer.truncate_string(exception.message.to_s, 500),
|
|
25
|
-
"frames" => frames
|
|
26
|
-
}.compact,
|
|
27
55
|
"http" => {
|
|
28
56
|
"method" => payload[:method],
|
|
29
57
|
"path" => path,
|
|
@@ -33,15 +61,46 @@ module Auditea
|
|
|
33
61
|
"controller" => payload[:controller],
|
|
34
62
|
"action" => payload[:action]
|
|
35
63
|
}.compact
|
|
36
|
-
},
|
|
37
|
-
metadata: {
|
|
38
|
-
"sdk_event_kind" => "auto_observed"
|
|
39
64
|
}
|
|
40
65
|
)
|
|
41
66
|
rescue StandardError => error
|
|
42
67
|
Auditea.handle_error(error)
|
|
43
68
|
end
|
|
44
69
|
|
|
70
|
+
def merge_evidence(error, handled:, mechanism:, evidence:)
|
|
71
|
+
base = stringify_keys(evidence || {})
|
|
72
|
+
exception = base["exception"].is_a?(Hash) ? base["exception"].dup : {}
|
|
73
|
+
exception["type"] ||= error.class.name
|
|
74
|
+
exception["message"] ||= Sanitizer.truncate_string(error.message.to_s, MESSAGE_MAX)
|
|
75
|
+
exception["frames"] ||= safe_frames(error)
|
|
76
|
+
exception["handled"] = handled ? true : false unless exception.key?("handled")
|
|
77
|
+
exception["mechanism"] = mechanism.to_s if mechanism && !exception.key?("mechanism")
|
|
78
|
+
|
|
79
|
+
http = base["http"].is_a?(Hash) ? base["http"].dup : {}
|
|
80
|
+
enrich_http_from_context!(http)
|
|
81
|
+
|
|
82
|
+
rails = base["rails"].is_a?(Hash) ? base["rails"].dup : {}
|
|
83
|
+
|
|
84
|
+
{
|
|
85
|
+
"exception" => exception.compact,
|
|
86
|
+
"http" => compact_hash(http),
|
|
87
|
+
"rails" => compact_hash(rails)
|
|
88
|
+
}.compact.merge(base.except("exception", "http", "rails"))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def compact_hash(hash)
|
|
92
|
+
cleaned = hash.compact
|
|
93
|
+
cleaned.empty? ? nil : cleaned
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def enrich_http_from_context!(http)
|
|
97
|
+
ctx = CurrentContext.to_h
|
|
98
|
+
http_ctx = ctx["http"].is_a?(Hash) ? ctx["http"] : {}
|
|
99
|
+
http["method"] ||= http_ctx["method"]
|
|
100
|
+
path = http_ctx["path"].to_s.split("?", 2).first
|
|
101
|
+
http["path"] ||= path unless path.nil? || path.empty?
|
|
102
|
+
end
|
|
103
|
+
|
|
45
104
|
def build_from_array(pair)
|
|
46
105
|
return nil unless pair.is_a?(Array) && pair.size >= 2
|
|
47
106
|
|
|
@@ -56,9 +115,12 @@ module Auditea
|
|
|
56
115
|
def safe_frames(exception)
|
|
57
116
|
return [] unless exception.respond_to?(:backtrace) && exception.backtrace
|
|
58
117
|
|
|
59
|
-
exception.backtrace.first(
|
|
118
|
+
exception.backtrace.first(MAX_FRAMES).map do |line|
|
|
60
119
|
file, linenum, method_name = parse_frame(line)
|
|
61
|
-
{ "file" => file, "line" => linenum, "method" => method_name }.compact
|
|
120
|
+
frame = { "file" => file, "line" => linenum, "method" => method_name }.compact
|
|
121
|
+
in_app = classify_in_app(file)
|
|
122
|
+
frame["in_app"] = in_app unless in_app.nil?
|
|
123
|
+
frame
|
|
62
124
|
end
|
|
63
125
|
end
|
|
64
126
|
|
|
@@ -70,6 +132,62 @@ module Auditea
|
|
|
70
132
|
[line, nil, nil]
|
|
71
133
|
end
|
|
72
134
|
end
|
|
135
|
+
|
|
136
|
+
# Deterministic app-vs-framework marking. Returns nil when unknown (omit key).
|
|
137
|
+
def classify_in_app(file)
|
|
138
|
+
path = file.to_s
|
|
139
|
+
return nil if path.empty?
|
|
140
|
+
|
|
141
|
+
return true if path.start_with?("app/") || path.include?("/app/")
|
|
142
|
+
return false if path.include?("/gems/") || path.include?("/ruby/") ||
|
|
143
|
+
path.include?("/vendor/bundle/") || path.include?("/lib/ruby/")
|
|
144
|
+
|
|
145
|
+
if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
|
|
146
|
+
root = ::Rails.root.to_s
|
|
147
|
+
if path.start_with?(root)
|
|
148
|
+
return false if path.start_with?("#{root}/vendor")
|
|
149
|
+
|
|
150
|
+
return true
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def stringify_keys(value)
|
|
158
|
+
EventBuilder.stringify_keys(value)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Identity-safe claim set: WeakMap keys are the exception objects themselves.
|
|
162
|
+
# GC removes entries when exceptions are collected, so recycled object_ids cannot
|
|
163
|
+
# falsely suppress a distinct raise. Still cleared at request/job boundaries.
|
|
164
|
+
module Deduper
|
|
165
|
+
class << self
|
|
166
|
+
def claim!(error)
|
|
167
|
+
map = store
|
|
168
|
+
return false if map.key?(error)
|
|
169
|
+
|
|
170
|
+
map[error] = true
|
|
171
|
+
true
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def claimed?(error)
|
|
175
|
+
store.key?(error)
|
|
176
|
+
rescue StandardError
|
|
177
|
+
false
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def clear!
|
|
181
|
+
Thread.current[:auditea_exception_claims] = ObjectSpace::WeakMap.new
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
def store
|
|
187
|
+
Thread.current[:auditea_exception_claims] ||= ObjectSpace::WeakMap.new
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
73
191
|
end
|
|
74
192
|
end
|
|
75
193
|
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Auditea
|
|
4
|
+
module Instrumentation
|
|
5
|
+
# Derive scalar request/controller fields from Rails ErrorReporter context.
|
|
6
|
+
# Never retains controller/request objects; never reads params/body/cookies/session.
|
|
7
|
+
module StructuralRequest
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def from_reporter_context(context)
|
|
11
|
+
hash = context.is_a?(Hash) ? context : {}
|
|
12
|
+
controller = hash[:controller] || hash["controller"]
|
|
13
|
+
|
|
14
|
+
rails = {}
|
|
15
|
+
http = {}
|
|
16
|
+
event_context = {}
|
|
17
|
+
|
|
18
|
+
if controller && !controller.is_a?(String) && controller.respond_to?(:class)
|
|
19
|
+
rails["controller"] = controller.class.name
|
|
20
|
+
if controller.respond_to?(:action_name)
|
|
21
|
+
action = controller.action_name
|
|
22
|
+
rails["action"] = action.to_s if present?(action)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
request = safe_request(controller)
|
|
26
|
+
if request
|
|
27
|
+
method = safe_request_method(request)
|
|
28
|
+
http["method"] = method if present?(method)
|
|
29
|
+
|
|
30
|
+
path = safe_path(request)
|
|
31
|
+
http["path"] = path if present?(path)
|
|
32
|
+
|
|
33
|
+
request_id = safe_request_id(request)
|
|
34
|
+
event_context["request_id"] = request_id if present?(request_id)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
status = safe_status(controller)
|
|
38
|
+
http["status"] = status unless status.nil?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
"evidence" => {
|
|
43
|
+
"rails" => compact_hash(rails),
|
|
44
|
+
"http" => compact_hash(http)
|
|
45
|
+
}.compact,
|
|
46
|
+
"context" => compact_hash(event_context)
|
|
47
|
+
}
|
|
48
|
+
rescue StandardError
|
|
49
|
+
{ "evidence" => {}, "context" => {} }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def safe_request(controller)
|
|
53
|
+
return nil unless controller.respond_to?(:request)
|
|
54
|
+
|
|
55
|
+
request = controller.request
|
|
56
|
+
return nil if request.nil?
|
|
57
|
+
return nil unless request.respond_to?(:request_method) || request.respond_to?(:path)
|
|
58
|
+
|
|
59
|
+
request
|
|
60
|
+
rescue StandardError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def safe_request_method(request)
|
|
65
|
+
return request.request_method.to_s if request.respond_to?(:request_method)
|
|
66
|
+
|
|
67
|
+
nil
|
|
68
|
+
rescue StandardError
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def safe_path(request)
|
|
73
|
+
raw =
|
|
74
|
+
if request.respond_to?(:path)
|
|
75
|
+
request.path
|
|
76
|
+
elsif request.respond_to?(:fullpath)
|
|
77
|
+
request.fullpath
|
|
78
|
+
end
|
|
79
|
+
return nil unless present?(raw)
|
|
80
|
+
|
|
81
|
+
raw.to_s.split("?", 2).first
|
|
82
|
+
rescue StandardError
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def safe_request_id(request)
|
|
87
|
+
if request.respond_to?(:request_id)
|
|
88
|
+
value = request.request_id
|
|
89
|
+
return value.to_s if present?(value)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if request.respond_to?(:env)
|
|
93
|
+
env = request.env
|
|
94
|
+
if env.is_a?(Hash)
|
|
95
|
+
value = env["action_dispatch.request_id"] || env["HTTP_X_REQUEST_ID"]
|
|
96
|
+
return value.to_s if present?(value)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
nil
|
|
101
|
+
rescue StandardError
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def safe_status(controller)
|
|
106
|
+
return nil unless controller.respond_to?(:response)
|
|
107
|
+
|
|
108
|
+
response = controller.response
|
|
109
|
+
return nil unless response
|
|
110
|
+
return nil unless response.respond_to?(:status)
|
|
111
|
+
|
|
112
|
+
status = response.status
|
|
113
|
+
return nil unless status.is_a?(Integer)
|
|
114
|
+
return nil unless status.between?(100, 599)
|
|
115
|
+
|
|
116
|
+
# Unhandled failures often still have the default 200 before rendering;
|
|
117
|
+
# only keep status when the response looks finalized or already erroneous.
|
|
118
|
+
committed = response.respond_to?(:committed?) && response.committed?
|
|
119
|
+
return status if committed || status >= 400
|
|
120
|
+
|
|
121
|
+
nil
|
|
122
|
+
rescue StandardError
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def compact_hash(hash)
|
|
127
|
+
cleaned = hash.compact
|
|
128
|
+
cleaned.empty? ? nil : cleaned
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def present?(value)
|
|
132
|
+
!(value.nil? || (value.respond_to?(:empty?) && value.empty?))
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -12,6 +12,7 @@ module Auditea
|
|
|
12
12
|
|
|
13
13
|
def call(env)
|
|
14
14
|
CurrentContext.clear!
|
|
15
|
+
Instrumentation::Exception::Deduper.clear!
|
|
15
16
|
request = ::Rack::Request.new(env)
|
|
16
17
|
CurrentContext.set(
|
|
17
18
|
"request_id" => env["action_dispatch.request_id"] || env["HTTP_X_REQUEST_ID"],
|
|
@@ -24,6 +25,7 @@ module Auditea
|
|
|
24
25
|
ensure
|
|
25
26
|
opportunistically_refresh_inventory
|
|
26
27
|
CurrentContext.clear!
|
|
28
|
+
Instrumentation::Exception::Deduper.clear!
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
private
|
data/lib/auditea/railtie.rb
CHANGED
data/lib/auditea/version.rb
CHANGED
data/lib/auditea.rb
CHANGED
|
@@ -32,6 +32,8 @@ require_relative "auditea/inventory"
|
|
|
32
32
|
require_relative "auditea/host_inventory"
|
|
33
33
|
require_relative "auditea/instrumentation/request"
|
|
34
34
|
require_relative "auditea/instrumentation/exception"
|
|
35
|
+
require_relative "auditea/instrumentation/structural_request"
|
|
36
|
+
require_relative "auditea/instrumentation/error_subscriber"
|
|
35
37
|
require_relative "auditea/instrumentation/logger_adapter"
|
|
36
38
|
require_relative "auditea/instrumentation/active_job"
|
|
37
39
|
require_relative "auditea/middleware/context"
|
|
@@ -94,6 +96,25 @@ module Auditea
|
|
|
94
96
|
# Alias kept for legacy familiarity (KEEP CONCEPT).
|
|
95
97
|
alias track capture
|
|
96
98
|
|
|
99
|
+
# First-class exception capture → action `exception.raised` / category `error`.
|
|
100
|
+
# Rebuilds structured exception evidence (type, message, frames, handled, mechanism).
|
|
101
|
+
# Uses CurrentContext when present; sanitizes like normal events; fail-open by default.
|
|
102
|
+
def capture_exception(error, handled: false, mechanism: nil, severity: "error",
|
|
103
|
+
context: nil, metadata: nil, evidence: nil)
|
|
104
|
+
Instrumentation::Exception.capture(
|
|
105
|
+
error,
|
|
106
|
+
handled: handled,
|
|
107
|
+
mechanism: mechanism || "auditea.capture_exception",
|
|
108
|
+
severity: severity,
|
|
109
|
+
context: context,
|
|
110
|
+
metadata: (metadata || {}).merge("sdk_event_kind" => "explicit"),
|
|
111
|
+
evidence: evidence,
|
|
112
|
+
claim: true
|
|
113
|
+
)
|
|
114
|
+
rescue StandardError => error
|
|
115
|
+
handle_error(error)
|
|
116
|
+
end
|
|
117
|
+
|
|
97
118
|
# Structured application log as a first-class event (not Rails.logger auto-forward).
|
|
98
119
|
def log(level, message, context: {}, metadata: {})
|
|
99
120
|
Log.record(level, message, context: context, metadata: metadata)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: auditea
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.beta.
|
|
4
|
+
version: 0.1.0.beta.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- AudiTea
|
|
@@ -35,9 +35,11 @@ files:
|
|
|
35
35
|
- lib/auditea/host_inventory/collector.rb
|
|
36
36
|
- lib/auditea/host_inventory/emitter.rb
|
|
37
37
|
- lib/auditea/instrumentation/active_job.rb
|
|
38
|
+
- lib/auditea/instrumentation/error_subscriber.rb
|
|
38
39
|
- lib/auditea/instrumentation/exception.rb
|
|
39
40
|
- lib/auditea/instrumentation/logger_adapter.rb
|
|
40
41
|
- lib/auditea/instrumentation/request.rb
|
|
42
|
+
- lib/auditea/instrumentation/structural_request.rb
|
|
41
43
|
- lib/auditea/inventory.rb
|
|
42
44
|
- lib/auditea/inventory/background_refresh.rb
|
|
43
45
|
- lib/auditea/inventory/collector.rb
|