@descryy/runtime-adapter-ruby 0.0.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.
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/rails-frame-parsing.d.ts +105 -0
- package/dist/rails-frame-parsing.d.ts.map +1 -0
- package/dist/rails-frame-parsing.js +183 -0
- package/dist/rails-frame-parsing.js.map +1 -0
- package/dist/rails-stack-trace-parser.d.ts +31 -0
- package/dist/rails-stack-trace-parser.d.ts.map +1 -0
- package/dist/rails-stack-trace-parser.js +63 -0
- package/dist/rails-stack-trace-parser.js.map +1 -0
- package/dist/ruby-frame-parsing.d.ts +78 -0
- package/dist/ruby-frame-parsing.d.ts.map +1 -0
- package/dist/ruby-frame-parsing.js +108 -0
- package/dist/ruby-frame-parsing.js.map +1 -0
- package/dist/ruby-log-envelopes.d.ts +101 -0
- package/dist/ruby-log-envelopes.d.ts.map +1 -0
- package/dist/ruby-log-envelopes.js +214 -0
- package/dist/ruby-log-envelopes.js.map +1 -0
- package/dist/ruby-runtime-adapter.d.ts +51 -0
- package/dist/ruby-runtime-adapter.d.ts.map +1 -0
- package/dist/ruby-runtime-adapter.js +56 -0
- package/dist/ruby-runtime-adapter.js.map +1 -0
- package/dist/ruby-source-location-resolver.d.ts +34 -0
- package/dist/ruby-source-location-resolver.d.ts.map +1 -0
- package/dist/ruby-source-location-resolver.js +61 -0
- package/dist/ruby-source-location-resolver.js.map +1 -0
- package/dist/ruby-stack-trace-parser.d.ts +24 -0
- package/dist/ruby-stack-trace-parser.d.ts.map +1 -0
- package/dist/ruby-stack-trace-parser.js +71 -0
- package/dist/ruby-stack-trace-parser.js.map +1 -0
- package/dist/sinatra-frame-parsing.d.ts +92 -0
- package/dist/sinatra-frame-parsing.d.ts.map +1 -0
- package/dist/sinatra-frame-parsing.js +142 -0
- package/dist/sinatra-frame-parsing.js.map +1 -0
- package/dist/sinatra-stack-trace-parser.d.ts +18 -0
- package/dist/sinatra-stack-trace-parser.d.ts.map +1 -0
- package/dist/sinatra-stack-trace-parser.js +50 -0
- package/dist/sinatra-stack-trace-parser.js.map +1 -0
- package/package.json +30 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two `LogEnvelope`s Ruby actually ships with: the stdlib `Logger`'s
|
|
3
|
+
* default formatter, and Rails' `ActiveSupport::TaggedLogging`.
|
|
4
|
+
*
|
|
5
|
+
* ## Why Ruby needs these when Python does not
|
|
6
|
+
*
|
|
7
|
+
* RT-090's leading measurement. A real Ruby backtrace logged through a real
|
|
8
|
+
* `Logger` detects **correctly** — right frame count, right function, right
|
|
9
|
+
* line — and reports
|
|
10
|
+
*
|
|
11
|
+
* ```
|
|
12
|
+
* file: "E, [2026-08-19T04:55:54.126478 #763304] ERROR -- : logenv.rb"
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Confident evidence, every number a reviewer would check correct, and the
|
|
16
|
+
* one field that has to resolve to a graph node resolving to nothing.
|
|
17
|
+
* Ruby's header pattern is deliberately unanchored — its own comment
|
|
18
|
+
* anticipates a leveled logger prefixing a timestamp — so the prefix does
|
|
19
|
+
* not stop it matching; the greedy `(?<file>.+)` before `:(\d+):in` simply
|
|
20
|
+
* absorbs the prefix into the path. There is no shape that separates a log
|
|
21
|
+
* prefix from a path containing spaces and a colon, so the wrapper has to
|
|
22
|
+
* come off first. That is this file.
|
|
23
|
+
*
|
|
24
|
+
* By contrast Python needs no **unwrapping**: measured across real Flask
|
|
25
|
+
* 3.1.3, Django 6.1 and FastAPI 0.141.1, all three print
|
|
26
|
+
* `Traceback (most recent call last):` at column 0 with CPython's own
|
|
27
|
+
* two-space frame indent, because `logging` appends `exc_info` unindented.
|
|
28
|
+
* PHP escapes for a different reason again — its header anchors the location
|
|
29
|
+
* at the *end* of the line, so a prefix falls outside the capture.
|
|
30
|
+
*
|
|
31
|
+
* **This paragraph used to end "and never will", and that was wrong**
|
|
32
|
+
* (corrected in RT-106). It is right about unwrapping and wrong about the
|
|
33
|
+
* other half of what this interface does: `startsRecord` is the only way a
|
|
34
|
+
* collector can learn that a line *ended the previous record*, and Python
|
|
35
|
+
* is where that matters most, because a traceback is written one `write()`
|
|
36
|
+
* per line and any concurrent logger interleaves between them.
|
|
37
|
+
* `createUvicornLogEnvelope` in `packages/adapter-python` is the result —
|
|
38
|
+
* an envelope whose `unwrap` is the identity function for every line
|
|
39
|
+
* carrying evidence, and whose entire contribution is the boundary. The
|
|
40
|
+
* general lesson is that "does this framework decorate its lines" and "does
|
|
41
|
+
* this framework tell us where a record starts" are separate questions, and
|
|
42
|
+
* the first one's answer does not settle the second.
|
|
43
|
+
*
|
|
44
|
+
* ## Both formats were measured, and the guess was wrong
|
|
45
|
+
*
|
|
46
|
+
* Written against real output from ruby 3.4.2 and activesupport 8.1.3.1, not
|
|
47
|
+
* from documentation. That mattered: the first version of this work assumed
|
|
48
|
+
* Rails tags looked like `[req-abc] [ERROR] message`, by analogy with every
|
|
49
|
+
* other framework surveyed. Real `TaggedLogging` emits **no level token at
|
|
50
|
+
* all** — it installs its own formatter deriving from `SimpleFormatter`,
|
|
51
|
+
* which discards the severity, pid and timestamp the stdlib formatter would
|
|
52
|
+
* have written. The two formats are therefore disjoint rather than nested,
|
|
53
|
+
* which is why they are two envelopes and not one with a branch.
|
|
54
|
+
*
|
|
55
|
+
* ## What Rails does NOT give, stated because ASP.NET does give it
|
|
56
|
+
*
|
|
57
|
+
* RT-089 found Kestrel writing `Request id "0HNNTFUBAD72K:00000001"` — §16's
|
|
58
|
+
* framework-correlation row arriving for free. **Rails has no equivalent.**
|
|
59
|
+
* Its tags are positional and untyped: `[req-ABC123] [user-42]` is two
|
|
60
|
+
* strings in the order `config.log_tags` listed them, with nothing in the
|
|
61
|
+
* stream saying which is a request id. The near-universal default is
|
|
62
|
+
* `[:request_id]` first, and treating the first tag as `requestId` on that
|
|
63
|
+
* basis would be inferring from a convention rather than reading a
|
|
64
|
+
* declaration — the exact distinction `fields()` exists to keep. So tags go
|
|
65
|
+
* to `attrs` verbatim and `requestId` stays null.
|
|
66
|
+
*
|
|
67
|
+
* ## The disclosed ambiguity in the tagged format
|
|
68
|
+
*
|
|
69
|
+
* `[anything] rest` is not a format Rails owns. An application logging
|
|
70
|
+
* `[fixture] GET /api/invoices/3 failed` matches it, and this envelope will
|
|
71
|
+
* unwrap that line to `GET /api/invoices/3 failed`. Bounded, and stated
|
|
72
|
+
* rather than discovered:
|
|
73
|
+
*
|
|
74
|
+
* - The line is a plain log either way, so no exception is created or lost.
|
|
75
|
+
* - `payload.envelopedRaw` keeps the printed bytes, so nothing is destroyed.
|
|
76
|
+
* - `startsRecord` firing on it is *correct* for the case that matters — a
|
|
77
|
+
* foreign line interleaved into a traceback genuinely does end the record,
|
|
78
|
+
* which is the defect `startsRecord` exists to close.
|
|
79
|
+
*
|
|
80
|
+
* **That third bullet was wrong, and booting a real Rails application is
|
|
81
|
+
* what showed it** (RT-111). `TaggedLogging` tags every line of a multi-line
|
|
82
|
+
* message, so a backtrace arrives as forty lines each looking like a record
|
|
83
|
+
* header; a boundary on all of them makes a multi-line block impossible to
|
|
84
|
+
* assemble. `startsRecord` is `false` for this envelope now — see the
|
|
85
|
+
* comment at the return. The stdlib `Logger` envelope above keeps `true`,
|
|
86
|
+
* because its prefix genuinely appears once per record.
|
|
87
|
+
*
|
|
88
|
+
* The one genuine false positive is a Ruby source line beginning with `[` at
|
|
89
|
+
* column zero, since `error_highlight` prints the offending source line
|
|
90
|
+
* unindented — `[1, 2].each do |x|` would be unwrapped. The anchor requires
|
|
91
|
+
* the bracket at column zero with no leading whitespace, which excludes
|
|
92
|
+
* every indented source line and every `\tfrom` frame; a top-level array
|
|
93
|
+
* literal on the raising line is what remains, and it costs a prefix on a
|
|
94
|
+
* snippet line rather than any part of a frame.
|
|
95
|
+
*/
|
|
96
|
+
import type { LogEnvelope } from "@descryy/runtime-backend-observation";
|
|
97
|
+
/** The stdlib `Logger` default formatter. */
|
|
98
|
+
export declare function createRubyLoggerEnvelope(): LogEnvelope;
|
|
99
|
+
/** Rails' `ActiveSupport::TaggedLogging`. */
|
|
100
|
+
export declare function createRailsTaggedLogEnvelope(): LogEnvelope;
|
|
101
|
+
//# sourceMappingURL=ruby-log-envelopes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ruby-log-envelopes.d.ts","sourceRoot":"","sources":["../src/ruby-log-envelopes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8FG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAiC,MAAM,sCAAsC,CAAC;AA0CvG,6CAA6C;AAC7C,wBAAgB,wBAAwB,IAAI,WAAW,CAgCtD;AAED,6CAA6C;AAC7C,wBAAgB,4BAA4B,IAAI,WAAW,CAoD1D"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two `LogEnvelope`s Ruby actually ships with: the stdlib `Logger`'s
|
|
3
|
+
* default formatter, and Rails' `ActiveSupport::TaggedLogging`.
|
|
4
|
+
*
|
|
5
|
+
* ## Why Ruby needs these when Python does not
|
|
6
|
+
*
|
|
7
|
+
* RT-090's leading measurement. A real Ruby backtrace logged through a real
|
|
8
|
+
* `Logger` detects **correctly** — right frame count, right function, right
|
|
9
|
+
* line — and reports
|
|
10
|
+
*
|
|
11
|
+
* ```
|
|
12
|
+
* file: "E, [2026-08-19T04:55:54.126478 #763304] ERROR -- : logenv.rb"
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Confident evidence, every number a reviewer would check correct, and the
|
|
16
|
+
* one field that has to resolve to a graph node resolving to nothing.
|
|
17
|
+
* Ruby's header pattern is deliberately unanchored — its own comment
|
|
18
|
+
* anticipates a leveled logger prefixing a timestamp — so the prefix does
|
|
19
|
+
* not stop it matching; the greedy `(?<file>.+)` before `:(\d+):in` simply
|
|
20
|
+
* absorbs the prefix into the path. There is no shape that separates a log
|
|
21
|
+
* prefix from a path containing spaces and a colon, so the wrapper has to
|
|
22
|
+
* come off first. That is this file.
|
|
23
|
+
*
|
|
24
|
+
* By contrast Python needs no **unwrapping**: measured across real Flask
|
|
25
|
+
* 3.1.3, Django 6.1 and FastAPI 0.141.1, all three print
|
|
26
|
+
* `Traceback (most recent call last):` at column 0 with CPython's own
|
|
27
|
+
* two-space frame indent, because `logging` appends `exc_info` unindented.
|
|
28
|
+
* PHP escapes for a different reason again — its header anchors the location
|
|
29
|
+
* at the *end* of the line, so a prefix falls outside the capture.
|
|
30
|
+
*
|
|
31
|
+
* **This paragraph used to end "and never will", and that was wrong**
|
|
32
|
+
* (corrected in RT-106). It is right about unwrapping and wrong about the
|
|
33
|
+
* other half of what this interface does: `startsRecord` is the only way a
|
|
34
|
+
* collector can learn that a line *ended the previous record*, and Python
|
|
35
|
+
* is where that matters most, because a traceback is written one `write()`
|
|
36
|
+
* per line and any concurrent logger interleaves between them.
|
|
37
|
+
* `createUvicornLogEnvelope` in `packages/adapter-python` is the result —
|
|
38
|
+
* an envelope whose `unwrap` is the identity function for every line
|
|
39
|
+
* carrying evidence, and whose entire contribution is the boundary. The
|
|
40
|
+
* general lesson is that "does this framework decorate its lines" and "does
|
|
41
|
+
* this framework tell us where a record starts" are separate questions, and
|
|
42
|
+
* the first one's answer does not settle the second.
|
|
43
|
+
*
|
|
44
|
+
* ## Both formats were measured, and the guess was wrong
|
|
45
|
+
*
|
|
46
|
+
* Written against real output from ruby 3.4.2 and activesupport 8.1.3.1, not
|
|
47
|
+
* from documentation. That mattered: the first version of this work assumed
|
|
48
|
+
* Rails tags looked like `[req-abc] [ERROR] message`, by analogy with every
|
|
49
|
+
* other framework surveyed. Real `TaggedLogging` emits **no level token at
|
|
50
|
+
* all** — it installs its own formatter deriving from `SimpleFormatter`,
|
|
51
|
+
* which discards the severity, pid and timestamp the stdlib formatter would
|
|
52
|
+
* have written. The two formats are therefore disjoint rather than nested,
|
|
53
|
+
* which is why they are two envelopes and not one with a branch.
|
|
54
|
+
*
|
|
55
|
+
* ## What Rails does NOT give, stated because ASP.NET does give it
|
|
56
|
+
*
|
|
57
|
+
* RT-089 found Kestrel writing `Request id "0HNNTFUBAD72K:00000001"` — §16's
|
|
58
|
+
* framework-correlation row arriving for free. **Rails has no equivalent.**
|
|
59
|
+
* Its tags are positional and untyped: `[req-ABC123] [user-42]` is two
|
|
60
|
+
* strings in the order `config.log_tags` listed them, with nothing in the
|
|
61
|
+
* stream saying which is a request id. The near-universal default is
|
|
62
|
+
* `[:request_id]` first, and treating the first tag as `requestId` on that
|
|
63
|
+
* basis would be inferring from a convention rather than reading a
|
|
64
|
+
* declaration — the exact distinction `fields()` exists to keep. So tags go
|
|
65
|
+
* to `attrs` verbatim and `requestId` stays null.
|
|
66
|
+
*
|
|
67
|
+
* ## The disclosed ambiguity in the tagged format
|
|
68
|
+
*
|
|
69
|
+
* `[anything] rest` is not a format Rails owns. An application logging
|
|
70
|
+
* `[fixture] GET /api/invoices/3 failed` matches it, and this envelope will
|
|
71
|
+
* unwrap that line to `GET /api/invoices/3 failed`. Bounded, and stated
|
|
72
|
+
* rather than discovered:
|
|
73
|
+
*
|
|
74
|
+
* - The line is a plain log either way, so no exception is created or lost.
|
|
75
|
+
* - `payload.envelopedRaw` keeps the printed bytes, so nothing is destroyed.
|
|
76
|
+
* - `startsRecord` firing on it is *correct* for the case that matters — a
|
|
77
|
+
* foreign line interleaved into a traceback genuinely does end the record,
|
|
78
|
+
* which is the defect `startsRecord` exists to close.
|
|
79
|
+
*
|
|
80
|
+
* **That third bullet was wrong, and booting a real Rails application is
|
|
81
|
+
* what showed it** (RT-111). `TaggedLogging` tags every line of a multi-line
|
|
82
|
+
* message, so a backtrace arrives as forty lines each looking like a record
|
|
83
|
+
* header; a boundary on all of them makes a multi-line block impossible to
|
|
84
|
+
* assemble. `startsRecord` is `false` for this envelope now — see the
|
|
85
|
+
* comment at the return. The stdlib `Logger` envelope above keeps `true`,
|
|
86
|
+
* because its prefix genuinely appears once per record.
|
|
87
|
+
*
|
|
88
|
+
* The one genuine false positive is a Ruby source line beginning with `[` at
|
|
89
|
+
* column zero, since `error_highlight` prints the offending source line
|
|
90
|
+
* unindented — `[1, 2].each do |x|` would be unwrapped. The anchor requires
|
|
91
|
+
* the bracket at column zero with no leading whitespace, which excludes
|
|
92
|
+
* every indented source line and every `\tfrom` frame; a top-level array
|
|
93
|
+
* literal on the raising line is what remains, and it costs a prefix on a
|
|
94
|
+
* snippet line rather than any part of a frame.
|
|
95
|
+
*/
|
|
96
|
+
/**
|
|
97
|
+
* `E, [2026-08-19T04:55:36.409952 #762887] ERROR -- : message`
|
|
98
|
+
*
|
|
99
|
+
* `Logger::Formatter::Format` is `"%s, [%s #%d] %5s -- %s: %s\n"` — severity
|
|
100
|
+
* letter, timestamp, pid, right-padded severity label, progname, message.
|
|
101
|
+
* Anchored at the start and requiring every field, because a partial match
|
|
102
|
+
* is evidence the line is not a header rather than a reason to guess at one.
|
|
103
|
+
* `progname` is `[^:]*` and routinely empty, which is what the bare `-- : `
|
|
104
|
+
* in the measured output is.
|
|
105
|
+
*/
|
|
106
|
+
const STDLIB_HEADER = /^([A-Z]), \[([^\]]*) #(\d+)\] +(\w+) -- ([^:]*): /;
|
|
107
|
+
/**
|
|
108
|
+
* One or more `[tag] ` groups at column zero.
|
|
109
|
+
*
|
|
110
|
+
* `ActiveSupport::TaggedLogging::Formatter#tagged` writes `"[#{tag}] "` per
|
|
111
|
+
* tag and then the message, with no other decoration — verified against
|
|
112
|
+
* activesupport 8.1.3.1 for both a single tag and two.
|
|
113
|
+
*/
|
|
114
|
+
const TAGGED_HEADER = /^((?:\[[^\]]*\] )+)/;
|
|
115
|
+
/** Every `[tag]` inside a matched tag run. */
|
|
116
|
+
const TAG = /\[([^\]]*)\]/g;
|
|
117
|
+
/**
|
|
118
|
+
* A continuation line, for both envelopes.
|
|
119
|
+
*
|
|
120
|
+
* Neither Ruby formatter wraps anything after the record's first line — the
|
|
121
|
+
* `full_message` body (blank line, `error_highlight` snippet, caret line,
|
|
122
|
+
* `\tfrom` frames) is written through untouched. Measured for both. So
|
|
123
|
+
* "unwrap a continuation" is the identity function here, and the only thing
|
|
124
|
+
* an envelope has to get right is not mistaking a continuation for a header.
|
|
125
|
+
*
|
|
126
|
+
* This is the property that makes Ruby cheap and .NET expensive: .NET
|
|
127
|
+
* indents every line of the record and so must strip on every line.
|
|
128
|
+
*/
|
|
129
|
+
function continuation(line) {
|
|
130
|
+
return { content: line, startsRecord: false };
|
|
131
|
+
}
|
|
132
|
+
/** The stdlib `Logger` default formatter. */
|
|
133
|
+
export function createRubyLoggerEnvelope() {
|
|
134
|
+
let fields = null;
|
|
135
|
+
return {
|
|
136
|
+
frameworkName: "ruby-stdlib-logger",
|
|
137
|
+
unwrap(line) {
|
|
138
|
+
const match = STDLIB_HEADER.exec(line);
|
|
139
|
+
if (match === null)
|
|
140
|
+
return continuation(line);
|
|
141
|
+
const [, severityChar, timestamp, pid, level, progname] = match;
|
|
142
|
+
fields = {
|
|
143
|
+
level: level ?? null,
|
|
144
|
+
// `progname` is Logger's own word for it and is usually empty. Mapped
|
|
145
|
+
// to `category` because that is the field's meaning, not because
|
|
146
|
+
// Ruby uses the word.
|
|
147
|
+
category: progname !== undefined && progname !== "" ? progname : null,
|
|
148
|
+
// Ruby's stdlib logger has no concept of a request. Null rather than
|
|
149
|
+
// scavenged from the message text — that is `extractCorrelationIds`'
|
|
150
|
+
// job and it still runs on the unwrapped content.
|
|
151
|
+
requestId: null,
|
|
152
|
+
attrs: {
|
|
153
|
+
severityChar: severityChar ?? "",
|
|
154
|
+
timestamp: timestamp ?? "",
|
|
155
|
+
pid: pid ?? "",
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
return { content: line.slice(match[0].length), startsRecord: true };
|
|
159
|
+
},
|
|
160
|
+
fields: () => fields,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** Rails' `ActiveSupport::TaggedLogging`. */
|
|
164
|
+
export function createRailsTaggedLogEnvelope() {
|
|
165
|
+
let fields = null;
|
|
166
|
+
return {
|
|
167
|
+
frameworkName: "rails-tagged-logging",
|
|
168
|
+
unwrap(line) {
|
|
169
|
+
const match = TAGGED_HEADER.exec(line);
|
|
170
|
+
if (match === null)
|
|
171
|
+
return continuation(line);
|
|
172
|
+
const run = match[1] ?? "";
|
|
173
|
+
const tags = [...run.matchAll(TAG)].map((m) => m[1] ?? "");
|
|
174
|
+
fields = {
|
|
175
|
+
// TaggedLogging's formatter derives from SimpleFormatter, which
|
|
176
|
+
// writes no severity. Measured: the level is genuinely absent from
|
|
177
|
+
// the stream, not merely unparsed.
|
|
178
|
+
level: null,
|
|
179
|
+
category: null,
|
|
180
|
+
// Positional and untyped — see this file's header. Rails does not
|
|
181
|
+
// say which tag is a request id, so neither does this.
|
|
182
|
+
requestId: null,
|
|
183
|
+
attrs: Object.fromEntries(tags.map((t, i) => [`tag${i}`, t])),
|
|
184
|
+
};
|
|
185
|
+
// **`false`, and this is the one place in this repository where a
|
|
186
|
+
// recognised header does NOT declare a record boundary** (RT-111).
|
|
187
|
+
//
|
|
188
|
+
// `ActiveSupport::TaggedLogging::Formatter` tags **every line** of a
|
|
189
|
+
// multi-line message, not the first. So a Rails backtrace arrives as
|
|
190
|
+
// forty consecutive lines each carrying the same `[request-id] ` run,
|
|
191
|
+
// and each of them looks exactly like a record header. Claiming a
|
|
192
|
+
// boundary on all of them makes a multi-line exception block
|
|
193
|
+
// impossible to assemble: the collector flushes the detector before
|
|
194
|
+
// every line, so every frame becomes its own one-line STACK_TRACE.
|
|
195
|
+
//
|
|
196
|
+
// Measured against a live Rails 8.0.4 failure: **80 evidence records,
|
|
197
|
+
// 60 of them single-frame STACK_TRACEs, zero EXCEPTIONs**, from a
|
|
198
|
+
// grammar that recognised every line correctly.
|
|
199
|
+
//
|
|
200
|
+
// RT-103 wrote `true` here with the argument that a foreign line
|
|
201
|
+
// interleaved into a backtrace genuinely does end the record. That
|
|
202
|
+
// argument is sound for a format where only the record's first line
|
|
203
|
+
// carries the prefix — Kestrel's, and Ruby's own stdlib `Logger`
|
|
204
|
+
// above, both of which keep `true`. It is false for a format where
|
|
205
|
+
// the prefix is per *line*, and Rails' is. The distinction is not
|
|
206
|
+
// "does this envelope recognise the line" but "does this framework's
|
|
207
|
+
// marker mean a record began", and only the second one may be
|
|
208
|
+
// reported here.
|
|
209
|
+
return { content: line.slice(run.length), startsRecord: false };
|
|
210
|
+
},
|
|
211
|
+
fields: () => fields,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=ruby-log-envelopes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ruby-log-envelopes.js","sourceRoot":"","sources":["../src/ruby-log-envelopes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The concrete `RuntimeAdapter` for Ruby.
|
|
3
|
+
*
|
|
4
|
+
* Ruby's grammar needed no seam changes: unlike CPython (which forced
|
|
5
|
+
* `isTrailer` and the most-recent-first contract into existence — RT-035),
|
|
6
|
+
* Ruby's header-embeds-the-innermost-frame shape and native
|
|
7
|
+
* most-recent-first order both fit the existing `LineClassifier` /
|
|
8
|
+
* `StackTrace.frames` contracts without modification. See
|
|
9
|
+
* `ruby-frame-parsing.ts` for why, and for the one disclosed gap (a
|
|
10
|
+
* top-level raise with zero caller frames) that shape leaves behind.
|
|
11
|
+
*/
|
|
12
|
+
import type { RuntimeAdapter } from "@descryy/runtime-backend-observation";
|
|
13
|
+
export interface RubyRuntimeAdapterOptions {
|
|
14
|
+
/** Attributed on every emitted Evidence's `service` field (plan §16.7). Omit when not applicable. */
|
|
15
|
+
readonly service?: string;
|
|
16
|
+
/**
|
|
17
|
+
* The framework whose exception reporting this process uses (§16, RT-111).
|
|
18
|
+
*
|
|
19
|
+
* **Declared, never sniffed**, the same rule ASP.NET's option follows.
|
|
20
|
+
* Rails does not log `Exception#full_message` — `ActionDispatch::DebugExceptions`
|
|
21
|
+
* composes `Class (message):` followed by bare `file:line:in 'name'` lines
|
|
22
|
+
* with no `from` prefix, which matches neither of Ruby's two patterns.
|
|
23
|
+
* Measured against a live Rails 8.0.4 request pipeline: **77 lines in,
|
|
24
|
+
* zero exceptions out.**
|
|
25
|
+
*
|
|
26
|
+
* Selecting this swaps in a different classifier *and* a different stack
|
|
27
|
+
* trace parser, because the two grammars disagree about where the raise
|
|
28
|
+
* site is: Ruby's header is the innermost frame, Rails' header has no
|
|
29
|
+
* location at all.
|
|
30
|
+
*
|
|
31
|
+
* Detecting it from a line that looks like Rails output would apply this
|
|
32
|
+
* grammar to a plain Ruby process that happened to print one, and Rails'
|
|
33
|
+
* frame shape is loose enough that the cost would be silent.
|
|
34
|
+
*
|
|
35
|
+
* `"sinatra"` is the same declared-not-sniffed answer for Sinatra
|
|
36
|
+
* **running in production mode** (`set :environment, :production`) —
|
|
37
|
+
* measured (plan 1C) against a real Sinatra 4.1.1 process:
|
|
38
|
+
* `Sinatra::Base#dump_errors!` composes `TIMESTAMP - Class - message:`
|
|
39
|
+
* followed by raw, `from`-less `Exception#backtrace` lines, matching
|
|
40
|
+
* neither of Ruby's own two patterns. See `sinatra-frame-parsing.ts`.
|
|
41
|
+
*
|
|
42
|
+
* Bare Rack — no framework at all — needs **no** option here: its own
|
|
43
|
+
* dispatch frames (`Rackup::Handler::WEBrick#service`, …) print in
|
|
44
|
+
* Ruby's ordinary `file:line:in 'name'` / `from …` shape and the plain
|
|
45
|
+
* classifier below reads them unchanged. Measured, not assumed — see
|
|
46
|
+
* `rack-framework-lifecycle.test.ts`.
|
|
47
|
+
*/
|
|
48
|
+
readonly framework?: "rails" | "sinatra";
|
|
49
|
+
}
|
|
50
|
+
export declare function createRubyRuntimeAdapter(options?: RubyRuntimeAdapterOptions): RuntimeAdapter;
|
|
51
|
+
//# sourceMappingURL=ruby-runtime-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ruby-runtime-adapter.d.ts","sourceRoot":"","sources":["../src/ruby-runtime-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAuB,MAAM,sCAAsC,CAAC;AAahG,MAAM,WAAW,yBAAyB;IACxC,qGAAqG;IACrG,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC1C;AAED,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,yBAA8B,GAAG,cAAc,CAsChG"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The concrete `RuntimeAdapter` for Ruby.
|
|
3
|
+
*
|
|
4
|
+
* Ruby's grammar needed no seam changes: unlike CPython (which forced
|
|
5
|
+
* `isTrailer` and the most-recent-first contract into existence — RT-035),
|
|
6
|
+
* Ruby's header-embeds-the-innermost-frame shape and native
|
|
7
|
+
* most-recent-first order both fit the existing `LineClassifier` /
|
|
8
|
+
* `StackTrace.frames` contracts without modification. See
|
|
9
|
+
* `ruby-frame-parsing.ts` for why, and for the one disclosed gap (a
|
|
10
|
+
* top-level raise with zero caller frames) that shape leaves behind.
|
|
11
|
+
*/
|
|
12
|
+
import { createLogCollector } from "@descryy/runtime-backend-observation";
|
|
13
|
+
import { createRailsLineClassifier } from "./rails-frame-parsing.js";
|
|
14
|
+
import { createRailsStackTraceParser } from "./rails-stack-trace-parser.js";
|
|
15
|
+
import { createRubyLineClassifier } from "./ruby-frame-parsing.js";
|
|
16
|
+
import { createRailsTaggedLogEnvelope } from "./ruby-log-envelopes.js";
|
|
17
|
+
import { createRubySourceLocationResolver } from "./ruby-source-location-resolver.js";
|
|
18
|
+
import { createRubyStackTraceParser } from "./ruby-stack-trace-parser.js";
|
|
19
|
+
import { createSinatraLineClassifier } from "./sinatra-frame-parsing.js";
|
|
20
|
+
import { createSinatraStackTraceParser } from "./sinatra-stack-trace-parser.js";
|
|
21
|
+
export function createRubyRuntimeAdapter(options = {}) {
|
|
22
|
+
const sourceLocationResolver = createRubySourceLocationResolver();
|
|
23
|
+
const isRails = options.framework === "rails";
|
|
24
|
+
const isSinatra = options.framework === "sinatra";
|
|
25
|
+
const stackTraceParser = isRails
|
|
26
|
+
? createRailsStackTraceParser(sourceLocationResolver)
|
|
27
|
+
: isSinatra
|
|
28
|
+
? createSinatraStackTraceParser(sourceLocationResolver)
|
|
29
|
+
: createRubyStackTraceParser(sourceLocationResolver);
|
|
30
|
+
function createBackendCollectors(sources) {
|
|
31
|
+
return sources.map((source) => createLogCollector({
|
|
32
|
+
source,
|
|
33
|
+
// Ruby's classifier is stateless (its markers — the "from " prefix
|
|
34
|
+
// and the trailing ": message" — are unambiguous per-line), but a
|
|
35
|
+
// fresh instance per collector costs nothing and keeps the
|
|
36
|
+
// construction pattern identical across every language adapter.
|
|
37
|
+
classifier: isRails ? createRailsLineClassifier() : isSinatra ? createSinatraLineClassifier() : createRubyLineClassifier(),
|
|
38
|
+
stackTraceParser,
|
|
39
|
+
// Rails logs through `ActiveSupport::TaggedLogging` by default, and
|
|
40
|
+
// the tag run corrupts the file path of every frame (RT-103). The
|
|
41
|
+
// envelope comes with the framework rather than as a separate
|
|
42
|
+
// option, because a caller that knows it launched Rails knows both.
|
|
43
|
+
// Sinatra's `dump_errors!` writes straight to `rack.errors` with no
|
|
44
|
+
// tag run, so it needs no envelope.
|
|
45
|
+
...(isRails ? { envelope: createRailsTaggedLogEnvelope() } : {}),
|
|
46
|
+
...(options.service !== undefined ? { service: options.service } : {}),
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
language: "ruby",
|
|
51
|
+
stackTraceParser,
|
|
52
|
+
sourceLocationResolver,
|
|
53
|
+
createBackendCollectors,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=ruby-runtime-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ruby-runtime-adapter.js","sourceRoot":"","sources":["../src/ruby-runtime-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAE1E,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAC5E,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAAE,gCAAgC,EAAE,MAAM,oCAAoC,CAAC;AACtF,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AAwChF,MAAM,UAAU,wBAAwB,CAAC,UAAqC,EAAE;IAC9E,MAAM,sBAAsB,GAAG,gCAAgC,EAAE,CAAC;IAClE,MAAM,OAAO,GAAG,OAAO,CAAC,SAAS,KAAK,OAAO,CAAC;IAC9C,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC;IAClD,MAAM,gBAAgB,GAAG,OAAO;QAC9B,CAAC,CAAC,2BAA2B,CAAC,sBAAsB,CAAC;QACrD,CAAC,CAAC,SAAS;YACT,CAAC,CAAC,6BAA6B,CAAC,sBAAsB,CAAC;YACvD,CAAC,CAAC,0BAA0B,CAAC,sBAAsB,CAAC,CAAC;IAEzD,SAAS,uBAAuB,CAAC,OAAuC;QACtE,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAC5B,kBAAkB,CAAC;YACjB,MAAM;YACN,mEAAmE;YACnE,kEAAkE;YAClE,2DAA2D;YAC3D,gEAAgE;YAChE,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,yBAAyB,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,2BAA2B,EAAE,CAAC,CAAC,CAAC,wBAAwB,EAAE;YAC1H,gBAAgB;YAChB,oEAAoE;YACpE,kEAAkE;YAClE,8DAA8D;YAC9D,oEAAoE;YACpE,oEAAoE;YACpE,oCAAoC;YACpC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,4BAA4B,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAChE,GAAG,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACvE,CAAC,CACH,CAAC;IACJ,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,MAAM;QAChB,gBAAgB;QAChB,sBAAsB;QACtB,uBAAuB;KACxB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruby `SourceLocationResolver`.
|
|
3
|
+
*
|
|
4
|
+
* **Almost every location is `self-contained`, and that is a positive claim
|
|
5
|
+
* rather than a fallback** — the same category RT-003 already assigns Ruby
|
|
6
|
+
* backtraces alongside Python tracebacks and PHP traces. A Ruby backtrace
|
|
7
|
+
* line names the `.rb` file and line the interpreter was actually
|
|
8
|
+
* executing; there is no compilation step and no separate map that can
|
|
9
|
+
* drift or go missing. The frame IS the answer.
|
|
10
|
+
*
|
|
11
|
+
* **What this deliberately does not do:** check that the file exists on
|
|
12
|
+
* disk, or that its contents still match what ran. `self-contained` is a
|
|
13
|
+
* statement about the *mechanism* (nothing was lost in translation), not
|
|
14
|
+
* about whether the working tree has since been edited.
|
|
15
|
+
*
|
|
16
|
+
* **The one exception: `instance_eval`/`eval` frames.** Ruby names their
|
|
17
|
+
* location `(eval at file.rb:12):1` — `file.rb:12` is where `eval` was
|
|
18
|
+
* called from, but `1` counts lines *within the evaluated string*, not
|
|
19
|
+
* within `file.rb`. Opening `file.rb` at line 1 would show unrelated code,
|
|
20
|
+
* so calling this `self-contained` would be false: something *was* lost —
|
|
21
|
+
* the line no longer indexes the named file. `isEvalSyntheticPath` detects
|
|
22
|
+
* Ruby's own synthetic-path shape (verbatim, not reparsed here — the
|
|
23
|
+
* parser already hands it through unchanged; see `ruby-frame-parsing.ts`)
|
|
24
|
+
* and this resolver reports `self-contained-eval-line` instead. The path
|
|
25
|
+
* itself is still reported verbatim, never rewritten to a guessed
|
|
26
|
+
* `file.rb:12` — a guess could be wrong and nothing downstream could tell.
|
|
27
|
+
*
|
|
28
|
+
* `column` is always null. Ruby backtraces never print one — there is
|
|
29
|
+
* nothing to derive it from, unlike CPython's 3.11+ caret line, so this
|
|
30
|
+
* isn't even a "could infer but won't" case.
|
|
31
|
+
*/
|
|
32
|
+
import type { SourceLocationResolver } from "@descryy/runtime-backend-observation";
|
|
33
|
+
export declare function createRubySourceLocationResolver(): SourceLocationResolver;
|
|
34
|
+
//# sourceMappingURL=ruby-source-location-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ruby-source-location-resolver.d.ts","sourceRoot":"","sources":["../src/ruby-source-location-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAiBrG,wBAAgB,gCAAgC,IAAI,sBAAsB,CAezE"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruby `SourceLocationResolver`.
|
|
3
|
+
*
|
|
4
|
+
* **Almost every location is `self-contained`, and that is a positive claim
|
|
5
|
+
* rather than a fallback** — the same category RT-003 already assigns Ruby
|
|
6
|
+
* backtraces alongside Python tracebacks and PHP traces. A Ruby backtrace
|
|
7
|
+
* line names the `.rb` file and line the interpreter was actually
|
|
8
|
+
* executing; there is no compilation step and no separate map that can
|
|
9
|
+
* drift or go missing. The frame IS the answer.
|
|
10
|
+
*
|
|
11
|
+
* **What this deliberately does not do:** check that the file exists on
|
|
12
|
+
* disk, or that its contents still match what ran. `self-contained` is a
|
|
13
|
+
* statement about the *mechanism* (nothing was lost in translation), not
|
|
14
|
+
* about whether the working tree has since been edited.
|
|
15
|
+
*
|
|
16
|
+
* **The one exception: `instance_eval`/`eval` frames.** Ruby names their
|
|
17
|
+
* location `(eval at file.rb:12):1` — `file.rb:12` is where `eval` was
|
|
18
|
+
* called from, but `1` counts lines *within the evaluated string*, not
|
|
19
|
+
* within `file.rb`. Opening `file.rb` at line 1 would show unrelated code,
|
|
20
|
+
* so calling this `self-contained` would be false: something *was* lost —
|
|
21
|
+
* the line no longer indexes the named file. `isEvalSyntheticPath` detects
|
|
22
|
+
* Ruby's own synthetic-path shape (verbatim, not reparsed here — the
|
|
23
|
+
* parser already hands it through unchanged; see `ruby-frame-parsing.ts`)
|
|
24
|
+
* and this resolver reports `self-contained-eval-line` instead. The path
|
|
25
|
+
* itself is still reported verbatim, never rewritten to a guessed
|
|
26
|
+
* `file.rb:12` — a guess could be wrong and nothing downstream could tell.
|
|
27
|
+
*
|
|
28
|
+
* `column` is always null. Ruby backtraces never print one — there is
|
|
29
|
+
* nothing to derive it from, unlike CPython's 3.11+ caret line, so this
|
|
30
|
+
* isn't even a "could infer but won't" case.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Ruby's synthetic path for a frame inside `eval`/`instance_eval`:
|
|
34
|
+
* `(eval)` on <3.4, `(eval at file.rb:12)` on 3.4+ (Feature #19117), and
|
|
35
|
+
* `(eval at (eval at file.rb:12):3)` when an eval nests inside another
|
|
36
|
+
* eval — all confirmed against a real `ruby` binary (see
|
|
37
|
+
* `ruby-call-shapes.test.ts`). `.+` is greedy so a nested case still
|
|
38
|
+
* matches as a whole; there is nothing to extract from inside it, since
|
|
39
|
+
* the value is reported verbatim either way.
|
|
40
|
+
*/
|
|
41
|
+
const EVAL_SYNTHETIC_PATH_PATTERN = /^\(eval(?: at .+)?\)$/;
|
|
42
|
+
function isEvalSyntheticPath(file) {
|
|
43
|
+
return file !== null && EVAL_SYNTHETIC_PATH_PATTERN.test(file);
|
|
44
|
+
}
|
|
45
|
+
export function createRubySourceLocationResolver() {
|
|
46
|
+
return {
|
|
47
|
+
// Async to satisfy the interface; Ruby needs no I/O to answer, so this
|
|
48
|
+
// never awaits — same as the Python resolver, for the same reason.
|
|
49
|
+
resolve(frame) {
|
|
50
|
+
return Promise.resolve({
|
|
51
|
+
file: frame.file,
|
|
52
|
+
line: frame.line,
|
|
53
|
+
column: frame.column,
|
|
54
|
+
functionName: frame.functionName,
|
|
55
|
+
reliability: isEvalSyntheticPath(frame.file) ? "self-contained-eval-line" : "self-contained",
|
|
56
|
+
resolvedVia: null,
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=ruby-source-location-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ruby-source-location-resolver.js","sourceRoot":"","sources":["../src/ruby-source-location-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAKH;;;;;;;;GAQG;AACH,MAAM,2BAA2B,GAAG,uBAAuB,CAAC;AAE5D,SAAS,mBAAmB,CAAC,IAAmB;IAC9C,OAAO,IAAI,KAAK,IAAI,IAAI,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,gCAAgC;IAC9C,OAAO;QACL,uEAAuE;QACvE,mEAAmE;QACnE,OAAO,CAAC,KAAuB;YAC7B,OAAO,OAAO,CAAC,OAAO,CAAC;gBACrB,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,YAAY,EAAE,KAAK,CAAC,YAAY;gBAChC,WAAW,EAAE,mBAAmB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC,gBAAgB;gBAC5F,WAAW,EAAE,IAAI;aAClB,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composes the pure Ruby line-recognition functions
|
|
3
|
+
* (`ruby-frame-parsing.ts`) with a `SourceLocationResolver` to implement the
|
|
4
|
+
* full async `StackTraceParser` contract.
|
|
5
|
+
*
|
|
6
|
+
* **No reversal.** Unlike CPython, Ruby prints most-recent-call first
|
|
7
|
+
* already — the header line IS the innermost frame, and each `from` line
|
|
8
|
+
* walks outward toward `<main>`. `StackTrace.frames` is contractually
|
|
9
|
+
* most-recent-first, so this parser emits frames in the order it reads
|
|
10
|
+
* them. (Of the languages implemented against this seam so far, CPython is
|
|
11
|
+
* the outlier that reverses; V8, and now Ruby, both print native
|
|
12
|
+
* most-recent-first order.)
|
|
13
|
+
*
|
|
14
|
+
* `fidelity` is always reported `"synchronous"`. A Ruby backtrace is built
|
|
15
|
+
* by walking the actual call frames of the raising thread/fiber — every
|
|
16
|
+
* frame shown is a genuine, unbroken link in the call chain that was
|
|
17
|
+
* executing. What it cannot show is a logical caller across a `Thread.new`
|
|
18
|
+
* hand-off or a Fiber resumption boundary, the same class of limit V8 and
|
|
19
|
+
* CPython stacks share; detecting that would need scheduler-level task
|
|
20
|
+
* correlation, not text parsing, so it is not claimed here.
|
|
21
|
+
*/
|
|
22
|
+
import type { SourceLocationResolver, StackTraceParser } from "@descryy/runtime-backend-observation";
|
|
23
|
+
export declare function createRubyStackTraceParser(resolver: SourceLocationResolver): StackTraceParser;
|
|
24
|
+
//# sourceMappingURL=ruby-stack-trace-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ruby-stack-trace-parser.d.ts","sourceRoot":"","sources":["../src/ruby-stack-trace-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAIrG,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,sBAAsB,GAAG,gBAAgB,CAkD7F"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composes the pure Ruby line-recognition functions
|
|
3
|
+
* (`ruby-frame-parsing.ts`) with a `SourceLocationResolver` to implement the
|
|
4
|
+
* full async `StackTraceParser` contract.
|
|
5
|
+
*
|
|
6
|
+
* **No reversal.** Unlike CPython, Ruby prints most-recent-call first
|
|
7
|
+
* already — the header line IS the innermost frame, and each `from` line
|
|
8
|
+
* walks outward toward `<main>`. `StackTrace.frames` is contractually
|
|
9
|
+
* most-recent-first, so this parser emits frames in the order it reads
|
|
10
|
+
* them. (Of the languages implemented against this seam so far, CPython is
|
|
11
|
+
* the outlier that reverses; V8, and now Ruby, both print native
|
|
12
|
+
* most-recent-first order.)
|
|
13
|
+
*
|
|
14
|
+
* `fidelity` is always reported `"synchronous"`. A Ruby backtrace is built
|
|
15
|
+
* by walking the actual call frames of the raising thread/fiber — every
|
|
16
|
+
* frame shown is a genuine, unbroken link in the call chain that was
|
|
17
|
+
* executing. What it cannot show is a logical caller across a `Thread.new`
|
|
18
|
+
* hand-off or a Fiber resumption boundary, the same class of limit V8 and
|
|
19
|
+
* CPython stacks share; detecting that would need scheduler-level task
|
|
20
|
+
* correlation, not text parsing, so it is not claimed here.
|
|
21
|
+
*/
|
|
22
|
+
import { parseRubyHeaderFrame, parseRubyFromFrame } from "./ruby-frame-parsing.js";
|
|
23
|
+
export function createRubyStackTraceParser(resolver) {
|
|
24
|
+
return {
|
|
25
|
+
async parse(rawText) {
|
|
26
|
+
const lines = rawText.split(/\r\n|\r|\n/);
|
|
27
|
+
const frames = [];
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
// A header line's trailing `: message` makes it more specific than
|
|
30
|
+
// a bare `from` line, so it is tried first; a genuine `from` line
|
|
31
|
+
// never matches the header pattern (see FROM_FRAME_PATTERN's
|
|
32
|
+
// required prefix), so trying both in either order is safe.
|
|
33
|
+
const raw = parseRubyHeaderFrame(line) ?? parseRubyFromFrame(line);
|
|
34
|
+
if (raw === null)
|
|
35
|
+
continue;
|
|
36
|
+
const location = await resolver.resolve({
|
|
37
|
+
file: raw.file,
|
|
38
|
+
line: raw.line,
|
|
39
|
+
column: null,
|
|
40
|
+
functionName: raw.functionName,
|
|
41
|
+
});
|
|
42
|
+
frames.push({ location, raw: line });
|
|
43
|
+
}
|
|
44
|
+
if (frames.length === 0) {
|
|
45
|
+
// Nothing frame-shaped at all — not recognisable as a Ruby
|
|
46
|
+
// backtrace, per this interface's contract: null, never an
|
|
47
|
+
// empty-but-structured StackTrace standing in for "couldn't parse".
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
// `primaryFrameIndex` is required so that every adapter states an
|
|
51
|
+
// answer rather than inheriting one (RT-083). Ruby's is 0, and the
|
|
52
|
+
// reason is structural rather than incidental: **Ruby's header line
|
|
53
|
+
// IS the raise site** (`file:line:in 'method': message (Class)`), it
|
|
54
|
+
// is parsed as a frame, and it is always the first line of the block.
|
|
55
|
+
// Ruby prints no unwinding frames above it — the `from` lines walk
|
|
56
|
+
// strictly outward toward `<main>`.
|
|
57
|
+
//
|
|
58
|
+
// **It is not necessarily application code, and that is fine.**
|
|
59
|
+
// Measured: `JSON.parse("{not json}")` from a two-deep call chain
|
|
60
|
+
// gives a header of
|
|
61
|
+
// `.../3.4.0/json/common.rb:221:in 'JSON::Ext::Parser.parse': ...`,
|
|
62
|
+
// a stdlib frame, with the application's own frames below it. Index 0
|
|
63
|
+
// still names it, because this field marks the failure site rather
|
|
64
|
+
// than the nearest owned code — see the contract's own note that
|
|
65
|
+
// nothing here denylists a stdlib path, since that "would break on
|
|
66
|
+
// any application that legitimately fails inside a library frame."
|
|
67
|
+
return { fidelity: "synchronous", frames, primaryFrameIndex: 0 };
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=ruby-stack-trace-parser.js.map
|