@descryy/runtime-adapter-python 0.3.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 ADDED
@@ -0,0 +1,6 @@
1
+ Copyright (c) Descry
2
+
3
+ All rights reserved. This software is proprietary and confidential.
4
+ No license, express or implied, to reproduce, distribute, modify, or
5
+ create derivative works is granted except as explicitly agreed in
6
+ writing.
@@ -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 itself is Python (`../python-preload/`), so it cannot live in
6
- * `src/` and be compiled — it is loaded by CPython, not by tsc. It sits in a
7
- * sibling directory of `src/` and `dist/` and is resolved by walking out of
8
- * whichever of the two this module is running from, the identical arrangement
9
- * `jvm-agent/build.ts` already uses for `Agent.java` and for the identical
10
- * reason: an `import.meta.url`-relative path that stays *inside* `src` or
11
- * `dist` resolves to a directory that only exists in one of them.
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
- * The hop count is one, not `jvm-agent/build.ts`'s two, because that file
14
- * sits a directory deeper than this one. Getting it wrong produces a path
15
- * that exists nowhere and a child process that starts perfectly and observes
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
- * `httpx` calls to be observed.
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;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,4JAA4J;AAC5J,eAAO,MAAM,gCAAgC,QAA+D,CAAC;AAE7G;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG;IAAE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAOlG"}
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 itself is Python (`../python-preload/`), so it cannot live in
6
- * `src/` and be compiled — it is loaded by CPython, not by tsc. It sits in a
7
- * sibling directory of `src/` and `dist/` and is resolved by walking out of
8
- * whichever of the two this module is running from, the identical arrangement
9
- * `jvm-agent/build.ts` already uses for `Agent.java` and for the identical
10
- * reason: an `import.meta.url`-relative path that stays *inside* `src` or
11
- * `dist` resolves to a directory that only exists in one of them.
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
- * The hop count is one, not `jvm-agent/build.ts`'s two, because that file
14
- * sits a directory deeper than this one. Getting it wrong produces a path
15
- * that exists nowhere and a child process that starts perfectly and observes
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
- * `httpx` calls to be observed.
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;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;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;;;;;;;GAOG;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
+ {"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 — the `LineClassifier` for
3
- * `ExceptionBlockDetector` plus the raw-text-to-frame-parts extraction that
4
- * feeds `SourceLocationResolver`.
2
+ * CPython traceback line shapes — LineClassifier for ExceptionBlockDetector
3
+ * plus raw-text-to-frame-parts extraction feeding SourceLocationResolver.
5
4
  *
6
- * CPython's format differs from V8's in three ways that matter, and each one
7
- * is a place a naive port of the V8 classifier would fail quietly rather
8
- * than loudly:
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
- * 1. **The opening line names no exception.** `Traceback (most recent call
11
- * last):` is a fixed banner; the type and message arrive *after* every
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 *between* chained tracebacks. They are
42
- * deliberately NOT trailers: each chained traceback is its own complete
43
- * exception block, and swallowing the connective line into the first block
44
- * would attach the wrong text to it. They fall through as plain log lines,
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
- * A stateful `LineClassifier`, and the statefulness is the point.
60
- *
61
- * The obvious stateless rule — "a frame is a `File` line **or** anything
62
- * indented" — is wrong, and the cross-language control in this package's
63
- * tests is what caught it. `ExceptionBlockDetector` opens a block the
64
- * moment `isFrame` returns true from idle, so a stateless indent rule turns
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
- * This depends on lines arriving in stream order, which `ExceptionBlockDetector`
79
- * provides by construction — it is a one-line-at-a-time streaming detector
80
- * and `isFrame` is consulted for every line in every state. A fresh
81
- * classifier per collector keeps two processes' output from sharing state.
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;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;AAyDD,0FAA0F;AAC1F,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9D;AA6ED;;;;;;;;GAQG;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;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;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,CA+BA;AAED,yGAAyG;AACzG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAOpE"}
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 — the `LineClassifier` for
3
- * `ExceptionBlockDetector` plus the raw-text-to-frame-parts extraction that
4
- * feeds `SourceLocationResolver`.
5
- *
6
- * CPython's format differs from V8's in three ways that matter, and each one
7
- * is a place a naive port of the V8 classifier would fail quietly rather
8
- * than loudly:
9
- *
10
- * 1. **The opening line names no exception.** `Traceback (most recent call
11
- * last):` is a fixed banner; the type and message arrive *after* every
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.
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
- * sees a line.
35
- *
36
- * ## The measurement
37
- *
38
- * `asyncio.TaskGroup` (stdlib since 3.11) and every `ExceptionGroup` print
39
- * their tracebacks inside a drawn tree:
40
- *
41
- * ```
42
- * + Exception Group Traceback (most recent call last):
43
- * | File "/app/x.py", line 13, in <module>
44
- * | asyncio.run(main())
45
- * | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
46
- * +-+---------------- 1 ----------------
47
- * | Traceback (most recent call last):
48
- * | File "/app/x.py", line 4, in read_customer_name
49
- * | TypeError: 'NoneType' object is not subscriptable
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: it is
90
- * absent for a frame compiled from a bare string, and `line N` is absent in
91
- * a handful of exotic cases, which is why the line number is required by
92
- * `parsePythonFrame` but not by the classifier — recognising a line as
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, which is what keeps a
101
- * zero-indent trailer (`ValueError: ...`) from being read as one.
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
- * `myapp.errors.ConfigError: ...`, or a bare `SystemExit` with no message
107
- * at all. Anchored at zero indent — every frame line in a CPython traceback
108
- * is indented, so the indent is what separates "this closes the block" from
109
- * "this is more of the block".
110
- *
111
- * ## The lookahead, and the fabrication it removes (RT-106)
112
- *
113
- * Without `(?=[\w.]*[a-z])` this pattern was `^[A-Za-z_][\w.]*(?::[ \t].*)?$`
114
- * — **any** `WORD: text` line at zero indent, which is `logging`'s default
115
- * format and uvicorn's entire access-log format. A CPython traceback is
116
- * written one `write()` per line, so a second thread logging to the same fd
117
- * interleaves *between* them. Measured, on real threaded CPython 3.12
118
- * output, three consecutive runs of 25 failures each:
119
- *
120
- * | | run 1 | run 2 | run 3 |
121
- * | --- | --- | --- | --- |
122
- * | failures reported with an access-log line as their identity | 7 | 8 | 7 |
123
- *
124
- * And the damage is symmetric, which the first reading of this defect
125
- * missed. An `INFO:` line landing after the `Traceback` banner closed the
126
- * block **immediately**, so the emitted EXCEPTION was two lines — the
127
- * banner and `INFO: 127.0.0.1:50002 - "GET /health HTTP/1.1" 200 OK`, zero
128
- * frames. *A successful request reported as the failure.* The real frames
129
- * and the real `TypeError:` then arrived with no header and were emitted as
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 *between* chained tracebacks. They are
162
- * deliberately NOT trailers: each chained traceback is its own complete
163
- * exception block, and swallowing the connective line into the first block
164
- * would attach the wrong text to it. They fall through as plain log lines,
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
- * A stateful `LineClassifier`, and the statefulness is the point.
193
- *
194
- * The obvious stateless rule — "a frame is a `File` line **or** anything
195
- * indented" — is wrong, and the cross-language control in this package's
196
- * tests is what caught it. `ExceptionBlockDetector` opens a block the
197
- * moment `isFrame` returns true from idle, so a stateless indent rule turns
198
- * **every indented log line in the application's output** into the start of
199
- * a spurious stack-trace block: a pretty-printed JSON payload, a wrapped
200
- * message, an indented SQL statement. It also accepts another runtime's
201
- * frames verbatim (` at deleteInvoice (file:///app/db.js:17:22)` is just
202
- * an indented line), so a mis-wired adapter would half-work rather than
203
- * fail.
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 in it — a bare
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":"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"}
1
+ {"version":3,"file":"python-frame-parsing.js","sourceRoot":"","sources":["../src/python-frame-parsing.ts"],"names":[],"mappings":"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"}