@descryy/runtime-adapter-python 0.1.0 → 0.4.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/LICENSE +6 -0
- package/dist/outbound-http-instrumentation.d.ts +13 -26
- package/dist/outbound-http-instrumentation.d.ts.map +1 -1
- package/dist/outbound-http-instrumentation.js +13 -26
- package/dist/outbound-http-instrumentation.js.map +1 -1
- package/dist/python-frame-parsing.d.ts +28 -55
- package/dist/python-frame-parsing.d.ts.map +1 -1
- package/dist/python-frame-parsing.js +81 -170
- package/dist/python-frame-parsing.js.map +1 -1
- package/dist/python-log-envelopes.d.ts +35 -84
- package/dist/python-log-envelopes.d.ts.map +1 -1
- package/dist/python-log-envelopes.js +41 -101
- package/dist/python-log-envelopes.js.map +1 -1
- package/dist/python-runtime-adapter.d.ts +25 -50
- package/dist/python-runtime-adapter.d.ts.map +1 -1
- package/dist/python-runtime-adapter.js +15 -38
- package/dist/python-runtime-adapter.js.map +1 -1
- package/dist/python-source-location-resolver.d.ts +12 -25
- package/dist/python-source-location-resolver.d.ts.map +1 -1
- package/dist/python-source-location-resolver.js +13 -29
- package/dist/python-source-location-resolver.js.map +1 -1
- package/dist/python-stack-trace-parser.d.ts +10 -20
- package/dist/python-stack-trace-parser.d.ts.map +1 -1
- package/dist/python-stack-trace-parser.js +18 -45
- package/dist/python-stack-trace-parser.js.map +1 -1
- package/package.json +8 -3
package/LICENSE
ADDED
|
@@ -2,38 +2,25 @@
|
|
|
2
2
|
* Where Python's outbound-HTTP instrumentation lives, and how a caller
|
|
3
3
|
* launches a process with it.
|
|
4
4
|
*
|
|
5
|
-
* The artifact
|
|
6
|
-
* `src/` and be compiled
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* The artifact is Python (`../python-preload/`), loaded by CPython not tsc,
|
|
6
|
+
* so it can't live in `src/` and be compiled. Sits as a sibling of `src/`
|
|
7
|
+
* and `dist/`, resolved by walking out of whichever this module runs from —
|
|
8
|
+
* same arrangement as jvm-agent/build.ts's Agent.java, one hop not two
|
|
9
|
+
* (this file sits one directory shallower). Getting the hop count wrong
|
|
10
|
+
* produces a path that exists nowhere and a child process that starts fine
|
|
11
|
+
* and observes nothing — measured, hence this module's test checks the
|
|
12
|
+
* directory is really on disk.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* nothing — measured, not hypothetical, and the reason this module's test
|
|
17
|
-
* asserts the directory is really on disk rather than only that the two
|
|
18
|
-
* spellings of the constant agree with each other.
|
|
19
|
-
*
|
|
20
|
-
* **Env, not a rewritten command.** CPython has no `--import`, and rewriting
|
|
21
|
-
* a caller's command string to inject a flag would be this runtime editing
|
|
22
|
-
* how the application boots. `site` imports a module named `sitecustomize` at
|
|
23
|
-
* interpreter startup, so prepending this directory to `PYTHONPATH` runs the
|
|
24
|
-
* instrumentation before the application's own code through a hook CPython
|
|
25
|
-
* already provides. See `sitecustomize.py` for the one real cost of that
|
|
26
|
-
* mechanism, stated there rather than discovered later.
|
|
14
|
+
* Env, not a rewritten command: CPython has no `--import`. `site` imports
|
|
15
|
+
* `sitecustomize` at startup, so prepending this dir to PYTHONPATH runs
|
|
16
|
+
* instrumentation before the app's own code via a hook CPython already provides.
|
|
27
17
|
*/
|
|
28
18
|
/** Absolute path to the directory holding `sitecustomize.py` and the instrumentation it installs. Prepend it to `PYTHONPATH`; never import it from Node. */
|
|
29
19
|
export declare const PYTHON_OUTBOUND_HTTP_PRELOAD_DIR: string;
|
|
30
20
|
/**
|
|
31
21
|
* The environment a Python process must be launched with for its outbound
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* `existingPythonPath` is **prepended to, never replaced**: a target whose own
|
|
35
|
-
* `PYTHONPATH` matters would otherwise stop finding its own modules, which is
|
|
36
|
-
* a way of breaking the application in order to watch it.
|
|
22
|
+
* httpx calls to be observed. `existingPythonPath` is prepended to, never
|
|
23
|
+
* replaced — a target's own PYTHONPATH must still resolve its own modules.
|
|
37
24
|
*/
|
|
38
25
|
export declare function pythonOutboundHttpEnv(existingPythonPath?: string): {
|
|
39
26
|
readonly PYTHONPATH: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"outbound-http-instrumentation.d.ts","sourceRoot":"","sources":["../src/outbound-http-instrumentation.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"outbound-http-instrumentation.d.ts","sourceRoot":"","sources":["../src/outbound-http-instrumentation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,4JAA4J;AAC5J,eAAO,MAAM,gCAAgC,QAA+D,CAAC;AAE7G;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG;IAAE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAOlG"}
|
|
@@ -2,28 +2,18 @@
|
|
|
2
2
|
* Where Python's outbound-HTTP instrumentation lives, and how a caller
|
|
3
3
|
* launches a process with it.
|
|
4
4
|
*
|
|
5
|
-
* The artifact
|
|
6
|
-
* `src/` and be compiled
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* The artifact is Python (`../python-preload/`), loaded by CPython not tsc,
|
|
6
|
+
* so it can't live in `src/` and be compiled. Sits as a sibling of `src/`
|
|
7
|
+
* and `dist/`, resolved by walking out of whichever this module runs from —
|
|
8
|
+
* same arrangement as jvm-agent/build.ts's Agent.java, one hop not two
|
|
9
|
+
* (this file sits one directory shallower). Getting the hop count wrong
|
|
10
|
+
* produces a path that exists nowhere and a child process that starts fine
|
|
11
|
+
* and observes nothing — measured, hence this module's test checks the
|
|
12
|
+
* directory is really on disk.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* nothing — measured, not hypothetical, and the reason this module's test
|
|
17
|
-
* asserts the directory is really on disk rather than only that the two
|
|
18
|
-
* spellings of the constant agree with each other.
|
|
19
|
-
*
|
|
20
|
-
* **Env, not a rewritten command.** CPython has no `--import`, and rewriting
|
|
21
|
-
* a caller's command string to inject a flag would be this runtime editing
|
|
22
|
-
* how the application boots. `site` imports a module named `sitecustomize` at
|
|
23
|
-
* interpreter startup, so prepending this directory to `PYTHONPATH` runs the
|
|
24
|
-
* instrumentation before the application's own code through a hook CPython
|
|
25
|
-
* already provides. See `sitecustomize.py` for the one real cost of that
|
|
26
|
-
* mechanism, stated there rather than discovered later.
|
|
14
|
+
* Env, not a rewritten command: CPython has no `--import`. `site` imports
|
|
15
|
+
* `sitecustomize` at startup, so prepending this dir to PYTHONPATH runs
|
|
16
|
+
* instrumentation before the app's own code via a hook CPython already provides.
|
|
27
17
|
*/
|
|
28
18
|
import { delimiter } from "node:path";
|
|
29
19
|
import { fileURLToPath } from "node:url";
|
|
@@ -31,11 +21,8 @@ import { fileURLToPath } from "node:url";
|
|
|
31
21
|
export const PYTHON_OUTBOUND_HTTP_PRELOAD_DIR = fileURLToPath(new URL("../python-preload", import.meta.url));
|
|
32
22
|
/**
|
|
33
23
|
* The environment a Python process must be launched with for its outbound
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* `existingPythonPath` is **prepended to, never replaced**: a target whose own
|
|
37
|
-
* `PYTHONPATH` matters would otherwise stop finding its own modules, which is
|
|
38
|
-
* a way of breaking the application in order to watch it.
|
|
24
|
+
* httpx calls to be observed. `existingPythonPath` is prepended to, never
|
|
25
|
+
* replaced — a target's own PYTHONPATH must still resolve its own modules.
|
|
39
26
|
*/
|
|
40
27
|
export function pythonOutboundHttpEnv(existingPythonPath) {
|
|
41
28
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"outbound-http-instrumentation.js","sourceRoot":"","sources":["../src/outbound-http-instrumentation.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"outbound-http-instrumentation.js","sourceRoot":"","sources":["../src/outbound-http-instrumentation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,4JAA4J;AAC5J,MAAM,CAAC,MAAM,gCAAgC,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,mBAAmB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE7G;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,kBAA2B;IAC/D,OAAO;QACL,UAAU,EACR,kBAAkB,KAAK,SAAS,IAAI,kBAAkB,KAAK,EAAE;YAC3D,CAAC,CAAC,gCAAgC;YAClC,CAAC,CAAC,GAAG,gCAAgC,GAAG,SAAS,GAAG,kBAAkB,EAAE;KAC7E,CAAC;AACJ,CAAC"}
|
|
@@ -1,32 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CPython traceback line shapes —
|
|
3
|
-
*
|
|
4
|
-
* feeds `SourceLocationResolver`.
|
|
2
|
+
* CPython traceback line shapes — LineClassifier for ExceptionBlockDetector
|
|
3
|
+
* plus raw-text-to-frame-parts extraction feeding SourceLocationResolver.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Differs from V8 in three ways a naive port would miss:
|
|
6
|
+
* 1. Opening line names no exception — `Traceback (most recent call last):`
|
|
7
|
+
* is a fixed banner; type/message arrive after every frame (isPythonTrailerLine).
|
|
8
|
+
* 2. Frames print oldest-call-first (failing frame last) — reversed in the
|
|
9
|
+
* parser, not here, since StackTrace.frames is contractually most-recent-first.
|
|
10
|
+
* 3. A frame is more than one line: the `File "...", line N, in f` line is
|
|
11
|
+
* followed by source text and (3.11+) a caret line — continuation lines
|
|
12
|
+
* with no location of their own that isPythonFrameLine must still accept,
|
|
13
|
+
* or the first source line would truncate every traceback to one frame.
|
|
9
14
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* frame. That is what `isPythonTrailerLine` exists for — see
|
|
13
|
-
* `LineClassifier.isTrailer`.
|
|
14
|
-
* 2. **Frames print oldest-call-first.** The failing frame is last, the
|
|
15
|
-
* opposite of V8. The reversal is done in the parser, not here, because
|
|
16
|
-
* `StackTrace.frames` is contractually most-recent-first.
|
|
17
|
-
* 3. **A frame is more than one line.** The `File "...", line N, in f`
|
|
18
|
-
* line is followed by the source text, and on 3.11+ by a caret line
|
|
19
|
-
* (` ^^^^^^`) marking the failing expression. Those continuation
|
|
20
|
-
* lines carry no location of their own but belong to the block, so
|
|
21
|
-
* `isPythonFrameLine` accepts them — otherwise the first source line
|
|
22
|
-
* would terminate the block and truncate every traceback to one frame.
|
|
23
|
-
*
|
|
24
|
-
* The banner test is deliberately not anchored to the start of the line, for
|
|
25
|
-
* the same reason the V8 header test isn't: real applications log through
|
|
26
|
-
* formatters that prefix timestamps and levels. The block detector's own
|
|
27
|
-
* "banner immediately followed by a frame" rule is what keeps that safe — a
|
|
28
|
-
* log line merely mentioning the word with nothing frame-shaped after it
|
|
29
|
-
* never becomes an exception.
|
|
15
|
+
* Banner test isn't anchored to line start (loggers prefix timestamps/levels),
|
|
16
|
+
* safe because the block detector requires a frame immediately after.
|
|
30
17
|
*/
|
|
31
18
|
export interface RawPythonFrame {
|
|
32
19
|
/** `<module>`, `<lambda>` and `<listcomp>` are real CPython function names and are kept verbatim, not nulled. */
|
|
@@ -38,13 +25,10 @@ export interface RawPythonFrame {
|
|
|
38
25
|
/** Removes PEP 654's tree gutter, if present. Identity for an ordinary traceback line. */
|
|
39
26
|
export declare function stripExceptionGroupGutter(line: string): string;
|
|
40
27
|
/**
|
|
41
|
-
* Two connective lines CPython prints
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* and the second `Traceback` banner opens a second block — which reports
|
|
46
|
-
* chained exceptions as two honest blocks plus the sentence linking them,
|
|
47
|
-
* rather than one block silently missing half its content.
|
|
28
|
+
* Two connective lines CPython prints between chained tracebacks. Deliberately
|
|
29
|
+
* NOT trailers: each chained traceback is its own complete block, so these
|
|
30
|
+
* fall through as plain log lines and the second banner opens a second block —
|
|
31
|
+
* two honest blocks plus the linking sentence, not one block missing half its content.
|
|
48
32
|
*/
|
|
49
33
|
export declare const CHAINED_EXCEPTION_CONNECTIVES: readonly ["During handling of the above exception, another exception occurred:", "The above exception was the direct cause of the following exception:"];
|
|
50
34
|
export declare function isPythonBannerLine(line: string): boolean;
|
|
@@ -56,29 +40,18 @@ export declare function isPythonLocationLine(line: string): boolean;
|
|
|
56
40
|
export declare function looksLikeContinuation(line: string): boolean;
|
|
57
41
|
export declare function isPythonTrailerLine(line: string): boolean;
|
|
58
42
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* **every indented log line in the application's output** into the start of
|
|
66
|
-
* a spurious stack-trace block: a pretty-printed JSON payload, a wrapped
|
|
67
|
-
* message, an indented SQL statement. It also accepts another runtime's
|
|
68
|
-
* frames verbatim (` at deleteInvoice (file:///app/db.js:17:22)` is just
|
|
69
|
-
* an indented line), so a mis-wired adapter would half-work rather than
|
|
70
|
-
* fail.
|
|
71
|
-
*
|
|
72
|
-
* A continuation is therefore only recognised **after a `File` line has
|
|
73
|
-
* been seen** — which is the actual rule CPython's format obeys, since
|
|
74
|
-
* source text and caret markers only ever appear beneath a location line.
|
|
75
|
-
* Any line that is neither a location nor a continuation clears that state,
|
|
76
|
-
* so a block that ends without a trailer cannot leave the classifier primed.
|
|
43
|
+
* Stateful, and the statefulness is the point. The obvious stateless rule —
|
|
44
|
+
* "a frame is a File line OR anything indented" — is wrong: ExceptionBlockDetector
|
|
45
|
+
* opens a block the moment isFrame returns true from idle, so a stateless
|
|
46
|
+
* indent rule turns every indented log line (pretty-printed JSON, a wrapped
|
|
47
|
+
* message, indented SQL) into a spurious block, and would accept another
|
|
48
|
+
* runtime's frames verbatim too.
|
|
77
49
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
50
|
+
* A continuation is only recognised after a File line has been seen —
|
|
51
|
+
* the actual rule CPython's format obeys. Any other line clears the state,
|
|
52
|
+
* so a block without a trailer can't leave the classifier primed. Relies on
|
|
53
|
+
* stream order (guaranteed by ExceptionBlockDetector); a fresh classifier
|
|
54
|
+
* per collector keeps two processes' output from sharing state.
|
|
82
55
|
*/
|
|
83
56
|
export declare function createPythonLineClassifier(): {
|
|
84
57
|
isHeader(line: string): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"python-frame-parsing.d.ts","sourceRoot":"","sources":["../src/python-frame-parsing.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"python-frame-parsing.d.ts","sourceRoot":"","sources":["../src/python-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,cAAc;IAC7B,iHAAiH;IACjH,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,0GAA0G;IAC1G,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AA0BD,0FAA0F;AAC1F,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9D;AA+CD;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,0JAGhC,CAAC;AAEX,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGzD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,IAAI;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CAClC,CA8BA;AAED,yGAAyG;AACzG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAOpE"}
|
|
@@ -1,84 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CPython traceback line shapes —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* 3. **A frame is more than one line.** The `File "...", line N, in f`
|
|
18
|
-
* line is followed by the source text, and on 3.11+ by a caret line
|
|
19
|
-
* (` ^^^^^^`) marking the failing expression. Those continuation
|
|
20
|
-
* lines carry no location of their own but belong to the block, so
|
|
21
|
-
* `isPythonFrameLine` accepts them — otherwise the first source line
|
|
22
|
-
* would terminate the block and truncate every traceback to one frame.
|
|
23
|
-
*
|
|
24
|
-
* The banner test is deliberately not anchored to the start of the line, for
|
|
25
|
-
* the same reason the V8 header test isn't: real applications log through
|
|
26
|
-
* formatters that prefix timestamps and levels. The block detector's own
|
|
27
|
-
* "banner immediately followed by a frame" rule is what keeps that safe — a
|
|
28
|
-
* log line merely mentioning the word with nothing frame-shaped after it
|
|
29
|
-
* never becomes an exception.
|
|
2
|
+
* CPython traceback line shapes — LineClassifier for ExceptionBlockDetector
|
|
3
|
+
* plus raw-text-to-frame-parts extraction feeding SourceLocationResolver.
|
|
4
|
+
*
|
|
5
|
+
* Differs from V8 in three ways a naive port would miss:
|
|
6
|
+
* 1. Opening line names no exception — `Traceback (most recent call last):`
|
|
7
|
+
* is a fixed banner; type/message arrive after every frame (isPythonTrailerLine).
|
|
8
|
+
* 2. Frames print oldest-call-first (failing frame last) — reversed in the
|
|
9
|
+
* parser, not here, since StackTrace.frames is contractually most-recent-first.
|
|
10
|
+
* 3. A frame is more than one line: the `File "...", line N, in f` line is
|
|
11
|
+
* followed by source text and (3.11+) a caret line — continuation lines
|
|
12
|
+
* with no location of their own that isPythonFrameLine must still accept,
|
|
13
|
+
* or the first source line would truncate every traceback to one frame.
|
|
14
|
+
*
|
|
15
|
+
* Banner test isn't anchored to line start (loggers prefix timestamps/levels),
|
|
16
|
+
* safe because the block detector requires a frame immediately after.
|
|
30
17
|
*/
|
|
31
18
|
const BANNER_PATTERN = /Traceback \(most recent call last\):/;
|
|
32
19
|
/**
|
|
33
|
-
* PEP 654's tree gutter, stripped before every other pattern in this file
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* ```
|
|
52
|
-
*
|
|
53
|
-
* Every location line is ` | File "..."`, and `FILE_LINE_LOOSE_PATTERN`
|
|
54
|
-
* anchors `^\s*File`. So **not one frame line matched**. Measured through
|
|
55
|
-
* the real detector on real output: 28 lines in, **28 plain logs out** —
|
|
56
|
-
* no EXCEPTION, no STACK_TRACE, no frames, and no degradation reported.
|
|
57
|
-
*
|
|
58
|
-
* Reachable through the front door, not a corner: a real FastAPI service
|
|
59
|
-
* with an async endpoint failing inside a `TaskGroup` produced two genuine
|
|
60
|
-
* `TypeError`s and the pipeline reported **one**. Half the failures silently
|
|
61
|
-
* gone, from a healthy-looking collector.
|
|
62
|
-
*
|
|
63
|
-
* ## Why it was invisible
|
|
64
|
-
*
|
|
65
|
-
* Every Python fixture in this repository raises from a plain synchronous
|
|
66
|
-
* call chain, so nothing was ever grouped. Lane A hit the identical shape on
|
|
67
|
-
* the JVM (RT-094): `isFrame` rejected JPMS module-qualified frames, every
|
|
68
|
-
* stack truncated at its first reflective call, and it was invisible because
|
|
69
|
-
* every JVM fixture threw directly from `main`. **A fixture that exercises
|
|
70
|
-
* only the simple call shape certifies only the simple call shape** — that
|
|
71
|
-
* is a property of the fixture, not of the language, and it has now cost two
|
|
72
|
-
* languages the same way.
|
|
73
|
-
*
|
|
74
|
-
* ## Why stripping is safe here
|
|
75
|
-
*
|
|
76
|
-
* The pattern is deliberately tighter than "any leading pipe": exactly one
|
|
77
|
-
* `|` or `+` followed by exactly one space, repeated for nesting. The
|
|
78
|
-
* separator rules (`+-+------- 1 -------`, `+---------`) have `-` after the
|
|
79
|
-
* `+` and so are never stripped — they stay unrecognised, which is correct,
|
|
80
|
-
* because they are decoration and belong to no frame. A traceback that is
|
|
81
|
-
* not in a group has no gutter and is untouched.
|
|
20
|
+
* PEP 654's tree gutter, stripped before every other pattern in this file sees a line.
|
|
21
|
+
*
|
|
22
|
+
* asyncio.TaskGroup/ExceptionGroup print tracebacks inside a drawn tree
|
|
23
|
+
* (` | File "..."` prefixed with `|`/`+`). FILE_LINE_LOOSE_PATTERN anchors
|
|
24
|
+
* `^\s*File`, so without stripping, not one frame line matched: measured
|
|
25
|
+
* through the real detector, 28 lines in -> 28 plain logs out, no EXCEPTION,
|
|
26
|
+
* no STACK_TRACE, no degradation reported. On a real FastAPI service with an
|
|
27
|
+
* async TaskGroup failure, two genuine TypeErrors were reported as one.
|
|
28
|
+
*
|
|
29
|
+
* Invisible because every fixture here raises from a plain synchronous call
|
|
30
|
+
* chain — same shape as RT-094's JVM defect (isFrame rejecting JPMS frames,
|
|
31
|
+
* invisible because every JVM fixture threw from main). A fixture that only
|
|
32
|
+
* exercises the simple call shape certifies only that shape.
|
|
33
|
+
*
|
|
34
|
+
* Stripping is safe: the pattern requires exactly one `|`/`+` plus one space,
|
|
35
|
+
* repeated for nesting — separator rules like `+-+------- 1 -------` have `-`
|
|
36
|
+
* right after `+` and are never stripped, staying unrecognised decoration.
|
|
37
|
+
* An ungrouped traceback has no gutter and is untouched.
|
|
82
38
|
*/
|
|
83
39
|
const GROUP_GUTTER = /^(?:[ \t]*[|+] )+/;
|
|
84
40
|
/** Removes PEP 654's tree gutter, if present. Identity for an ordinary traceback line. */
|
|
@@ -86,85 +42,52 @@ export function stripExceptionGroupGutter(line) {
|
|
|
86
42
|
return line.replace(GROUP_GUTTER, "");
|
|
87
43
|
}
|
|
88
44
|
/**
|
|
89
|
-
* The two-space-indented location line. `, in <name>` is optional
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* belonging to the block is a weaker claim than extracting a location from
|
|
94
|
-
* it, and conflating the two loses frames rather than merely their detail.
|
|
45
|
+
* The two-space-indented location line. `, in <name>` is optional (absent for
|
|
46
|
+
* a frame compiled from a bare string). Line number is required by
|
|
47
|
+
* parsePythonFrame but not the classifier — recognising a line as belonging
|
|
48
|
+
* to the block is a weaker claim than extracting a location from it.
|
|
95
49
|
*/
|
|
96
50
|
const FILE_LINE_PATTERN = /^\s*File "(?<file>.*)", line (?<line>\d+)(?:, in (?<fn>.*))?\s*$/;
|
|
97
51
|
const FILE_LINE_LOOSE_PATTERN = /^\s*File "(?<file>.*)"/;
|
|
98
52
|
/**
|
|
99
53
|
* A frame's continuation: source text, or a 3.11+ caret marker. Indented
|
|
100
|
-
* strictly deeper than the `File` line's two spaces,
|
|
101
|
-
*
|
|
54
|
+
* strictly deeper than the `File` line's two spaces, so a zero-indent
|
|
55
|
+
* trailer (`ValueError: ...`) is never read as one.
|
|
102
56
|
*/
|
|
103
57
|
const CONTINUATION_PATTERN = /^\s{3,}\S/;
|
|
104
58
|
/**
|
|
105
|
-
* The closing line: `ValueError: bad input`, a dotted
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* a bare `stack-trace` event, so the genuine failure produced **no
|
|
131
|
-
* EXCEPTION at all.** Roughly 30% of failures, both halves wrong.
|
|
132
|
-
*
|
|
133
|
-
* ## Why a lowercase letter, and what it costs
|
|
134
|
-
*
|
|
135
|
-
* A Python exception class is CapWords by PEP 8 and a log level is
|
|
136
|
-
* upper-case by universal convention, so "the class token contains at least
|
|
137
|
-
* one lowercase letter" separates them. That is a naming convention rather
|
|
138
|
-
* than a grammar, so the cost was **measured before it was accepted**: 199
|
|
139
|
-
* importable stdlib modules were walked and every `BaseException` subclass
|
|
140
|
-
* in them collected — **228 distinct exception class names, 0 of which
|
|
141
|
-
* contain no lowercase letter.** A dotted `myapp.errors.ConfigError`
|
|
142
|
-
* passes on its module path alone.
|
|
143
|
-
*
|
|
144
|
-
* **The disclosed recall cost:** an exception class named entirely in
|
|
145
|
-
* upper-case (`FOO`) no longer closes a block. Its frames still become a
|
|
146
|
-
* `stack-trace` event; what is lost is the exception identity, not the
|
|
147
|
-
* evidence. That is the deliberate direction of the trade — this defect was
|
|
148
|
-
* shipping *wrong* evidence, and a gap is disclosable in a way a
|
|
149
|
-
* confidently-wrong identity is not.
|
|
150
|
-
*
|
|
151
|
-
* **What it does not fix:** a log line whose first token happens to be
|
|
152
|
-
* CapWords — `Error: connection refused` from an application's own `print`
|
|
153
|
-
* — is still trailer-shaped and still interleaves. The structural answer to
|
|
154
|
-
* that is a declared `LogEnvelope`, whose `startsRecord` closes a block at
|
|
155
|
-
* a boundary the framework *states* rather than one this pattern guesses;
|
|
156
|
-
* `createUvicornLogEnvelope` is the first of those. This lookahead is what
|
|
157
|
-
* an **un-enveloped** stream gets, and it is a heuristic, said plainly.
|
|
59
|
+
* The closing line: `ValueError: bad input`, a dotted `myapp.errors.ConfigError:
|
|
60
|
+
* ...`, or a bare `SystemExit`. Anchored at zero indent — every frame line is indented.
|
|
61
|
+
*
|
|
62
|
+
* The lookahead `(?=[\w.]*[a-z])` (RT-106): without it, this pattern matched
|
|
63
|
+
* ANY `WORD: text` at zero indent — logging's default format and uvicorn's
|
|
64
|
+
* whole access-log format. A traceback is written one write() per line, so a
|
|
65
|
+
* second thread's logging interleaves between them. Measured on real
|
|
66
|
+
* threaded CPython 3.12, three runs of 25 failures each: 7, 8, 7 reported
|
|
67
|
+
* with an access-log line as their identity. Damage was symmetric: an INFO:
|
|
68
|
+
* line right after the banner closed the block immediately (a successful
|
|
69
|
+
* request reported as the failure), and the real frames then arrived
|
|
70
|
+
* headerless as a bare stack-trace event — the genuine failure produced no
|
|
71
|
+
* EXCEPTION at all. Roughly 30% of failures, both halves wrong.
|
|
72
|
+
*
|
|
73
|
+
* Why a lowercase letter: exception classes are CapWords (PEP 8), log levels
|
|
74
|
+
* are upper-case by convention. Measured before accepting the heuristic: 199
|
|
75
|
+
* stdlib modules walked, 228 distinct BaseException subclasses collected, 0
|
|
76
|
+
* with no lowercase letter.
|
|
77
|
+
*
|
|
78
|
+
* Disclosed recall cost: an all-upper-case exception class (`FOO`) no longer
|
|
79
|
+
* closes a block — its frames still become a stack-trace event, only the
|
|
80
|
+
* identity is lost, not the evidence. Does not fix a log line whose first
|
|
81
|
+
* token happens to be CapWords (`Error: connection refused` from a print) —
|
|
82
|
+
* the structural fix is a declared LogEnvelope (createUvicornLogEnvelope is
|
|
83
|
+
* the first); this lookahead is the heuristic an un-enveloped stream gets.
|
|
158
84
|
*/
|
|
159
85
|
const TRAILER_PATTERN = /^(?=[\w.]*[a-z])[A-Za-z_][\w.]*(?::[ \t].*)?$/;
|
|
160
86
|
/**
|
|
161
|
-
* Two connective lines CPython prints
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* and the second `Traceback` banner opens a second block — which reports
|
|
166
|
-
* chained exceptions as two honest blocks plus the sentence linking them,
|
|
167
|
-
* rather than one block silently missing half its content.
|
|
87
|
+
* Two connective lines CPython prints between chained tracebacks. Deliberately
|
|
88
|
+
* NOT trailers: each chained traceback is its own complete block, so these
|
|
89
|
+
* fall through as plain log lines and the second banner opens a second block —
|
|
90
|
+
* two honest blocks plus the linking sentence, not one block missing half its content.
|
|
168
91
|
*/
|
|
169
92
|
export const CHAINED_EXCEPTION_CONNECTIVES = [
|
|
170
93
|
"During handling of the above exception, another exception occurred:",
|
|
@@ -189,29 +112,18 @@ export function isPythonTrailerLine(line) {
|
|
|
189
112
|
return TRAILER_PATTERN.test(stripExceptionGroupGutter(line));
|
|
190
113
|
}
|
|
191
114
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* A continuation is therefore only recognised **after a `File` line has
|
|
206
|
-
* been seen** — which is the actual rule CPython's format obeys, since
|
|
207
|
-
* source text and caret markers only ever appear beneath a location line.
|
|
208
|
-
* Any line that is neither a location nor a continuation clears that state,
|
|
209
|
-
* so a block that ends without a trailer cannot leave the classifier primed.
|
|
210
|
-
*
|
|
211
|
-
* This depends on lines arriving in stream order, which `ExceptionBlockDetector`
|
|
212
|
-
* provides by construction — it is a one-line-at-a-time streaming detector
|
|
213
|
-
* and `isFrame` is consulted for every line in every state. A fresh
|
|
214
|
-
* classifier per collector keeps two processes' output from sharing state.
|
|
115
|
+
* Stateful, and the statefulness is the point. The obvious stateless rule —
|
|
116
|
+
* "a frame is a File line OR anything indented" — is wrong: ExceptionBlockDetector
|
|
117
|
+
* opens a block the moment isFrame returns true from idle, so a stateless
|
|
118
|
+
* indent rule turns every indented log line (pretty-printed JSON, a wrapped
|
|
119
|
+
* message, indented SQL) into a spurious block, and would accept another
|
|
120
|
+
* runtime's frames verbatim too.
|
|
121
|
+
*
|
|
122
|
+
* A continuation is only recognised after a File line has been seen —
|
|
123
|
+
* the actual rule CPython's format obeys. Any other line clears the state,
|
|
124
|
+
* so a block without a trailer can't leave the classifier primed. Relies on
|
|
125
|
+
* stream order (guaranteed by ExceptionBlockDetector); a fresh classifier
|
|
126
|
+
* per collector keeps two processes' output from sharing state.
|
|
215
127
|
*/
|
|
216
128
|
export function createPythonLineClassifier() {
|
|
217
129
|
let sawLocationLine = false;
|
|
@@ -234,8 +146,7 @@ export function createPythonLineClassifier() {
|
|
|
234
146
|
return false;
|
|
235
147
|
},
|
|
236
148
|
isTrailer(line) {
|
|
237
|
-
// Only closes a block that actually had frames
|
|
238
|
-
// identifier-shaped log line cannot close a block that never opened.
|
|
149
|
+
// Only closes a block that actually had frames — a bare identifier-shaped log line can't close one that never opened.
|
|
239
150
|
if (!sawLocationLine && !isPythonTrailerLine(line))
|
|
240
151
|
return false;
|
|
241
152
|
if (!isPythonTrailerLine(line))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"python-frame-parsing.js","sourceRoot":"","sources":["../src/python-frame-parsing.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"python-frame-parsing.js","sourceRoot":"","sources":["../src/python-frame-parsing.ts"],"names":[],"mappings":"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"}
|