@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
|
@@ -1,97 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Python's
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* A traceback is written one `write()` per line, so any other thread
|
|
24
|
-
* logging to the same stream interleaves between them. Measured on real
|
|
25
|
-
* threaded CPython 3.12, three runs of 25 failures: 7, 8 and 7 of them were
|
|
26
|
-
* reported as an EXCEPTION whose entire identity was an access-log line for
|
|
27
|
-
* a **successful** request — see `TRAILER_PATTERN` in
|
|
28
|
-
* `python-frame-parsing.ts` for the full measurement and for the heuristic
|
|
29
|
-
* that an un-enveloped stream falls back on.
|
|
30
|
-
*
|
|
31
|
-
* This envelope makes that case structural rather than guessed. uvicorn
|
|
32
|
-
* *states* where a record begins; the trailer pattern can only infer it.
|
|
33
|
-
*
|
|
34
|
-
* ## Nothing is stripped from a traceback, and that is the whole design
|
|
35
|
-
*
|
|
36
|
-
* uvicorn formats with `"%(levelprefix)s %(message)s"`, where `levelprefix`
|
|
37
|
-
* is `f"{levelname}:".ljust(9)`. That prefix goes on the record's **first**
|
|
38
|
-
* line only. The `exc_info` traceback `logging` appends afterwards is
|
|
39
|
-
* written through untouched, at column 0, with CPython's own indentation.
|
|
40
|
-
*
|
|
41
|
-
* Measured against real `uvicorn 0.41.0` / `fastapi 0.141.1` output
|
|
42
|
-
* (`test/fixtures/uvicorn-asgi-failure.txt`): `ERROR: Exception in ASGI
|
|
43
|
-
* application` is the header, and all 28 traceback lines beneath it are
|
|
44
|
-
* continuations returned byte-identical. So `unwrap` is the identity
|
|
45
|
-
* function for every line that carries evidence, and the envelope's entire
|
|
46
|
-
* contribution is `startsRecord` — the opposite balance to Ruby's, where
|
|
2
|
+
* Python's LogEnvelopes. Currently one: uvicorn (RT-106).
|
|
3
|
+
*
|
|
4
|
+
* RT-103 measured Flask/Django/FastAPI all printing tracebacks at column 0
|
|
5
|
+
* with CPython's own indent (logging appends exc_info unindented) and
|
|
6
|
+
* concluded Python needs no envelope. Right about unwrapping, wrong about
|
|
7
|
+
* record boundaries — different jobs behind one interface.
|
|
8
|
+
* UnwrappedLine.startsRecord is the only way a collector knows a line ends
|
|
9
|
+
* the previous record, and interleaved logging threads make that matter: a
|
|
10
|
+
* traceback is written one write() per line, so another thread's logging
|
|
11
|
+
* can land between them. Measured on real threaded CPython 3.12, three runs
|
|
12
|
+
* of 25 failures: 7, 8, 7 reported as an EXCEPTION whose entire identity was
|
|
13
|
+
* an access-log line for a successful request (see TRAILER_PATTERN in
|
|
14
|
+
* python-frame-parsing.ts for the full measurement). This envelope makes
|
|
15
|
+
* uvicorn's record boundary structural instead of inferred.
|
|
16
|
+
*
|
|
17
|
+
* Nothing is stripped from a traceback: uvicorn's `levelprefix` only prefixes
|
|
18
|
+
* the record's first line; the exc_info traceback logging appends afterwards
|
|
19
|
+
* passes through untouched. Measured against real uvicorn 0.41.0/fastapi
|
|
20
|
+
* 0.141.1 output: all 28 traceback lines are continuations returned
|
|
21
|
+
* byte-identical, so unwrap is identity and the envelope's whole
|
|
22
|
+
* contribution is startsRecord — opposite of Ruby's envelope, where
|
|
47
23
|
* unwrapping is the load-bearing half.
|
|
48
24
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* read as a record boundary, closing a block one line before its identity
|
|
53
|
-
* arrives. That is the same class of over-matching this envelope exists to
|
|
54
|
-
* fix, introduced from the other side. So the set is exactly the six level
|
|
55
|
-
* names `logging` and uvicorn can produce, and a seventh would be a
|
|
56
|
-
* measured addition rather than a widened pattern.
|
|
57
|
-
*
|
|
58
|
-
* ## Two disclosed limits
|
|
25
|
+
* Level set is closed, not `\w+`: `^(\w+): +` would match `TypeError: ...`,
|
|
26
|
+
* a real trailer wrongly read as a record boundary. Set is exactly the six
|
|
27
|
+
* level names logging/uvicorn can produce.
|
|
59
28
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* **Application loggers are not covered.** uvicorn configures only its own
|
|
67
|
-
* `uvicorn`/`uvicorn.error`/`uvicorn.access` loggers. An application's own
|
|
68
|
-
* `logging.getLogger(__name__).info(...)` goes through the root config and
|
|
69
|
-
* usually carries no prefix at all, so it comes back from `unwrap`
|
|
70
|
-
* untouched with `startsRecord: false` — correct, because this envelope has
|
|
71
|
-
* no idea whether that line began a record, and saying so would be the
|
|
72
|
-
* inference `fields()` exists to refuse.
|
|
29
|
+
* Two disclosed limits: uvicorn wraps levelprefix in ANSI on a TTY (Descry
|
|
30
|
+
* pipes, so unmeasured, unmatched); and application loggers via
|
|
31
|
+
* `logging.getLogger(__name__)` carry no prefix, so they come back
|
|
32
|
+
* untouched with startsRecord: false — correct, since this envelope has no
|
|
33
|
+
* way to know whether such a line began a record.
|
|
73
34
|
*/
|
|
74
35
|
import { type LogEnvelope, type LogPattern, type LogPatternRegistry } from "@descryy/runtime-backend-observation";
|
|
75
36
|
/**
|
|
76
|
-
* uvicorn's wrapper,
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* The regexes and the fields are unchanged — this is the same measurement
|
|
80
|
-
* against the same real `uvicorn 0.41.0` output, moved to where a caller can
|
|
81
|
-
* name it and compose it with another format. What changed is that
|
|
82
|
-
* `PythonRuntimeAdapterOptions.server` is no longer the entire Python
|
|
83
|
-
* vocabulary: a process that prints uvicorn's access lines AND formatted
|
|
84
|
-
* console output can now be given both, which is what UAT phase 2's Phase 9
|
|
85
|
-
* asked for and what a closed union of framework names structurally cannot do.
|
|
37
|
+
* uvicorn's wrapper, as a registry entry rather than a private constant, so a
|
|
38
|
+
* process printing uvicorn's access lines AND formatted console output can
|
|
39
|
+
* be given both — a closed union of framework names couldn't do that.
|
|
86
40
|
*/
|
|
87
41
|
export declare const UVICORN_LOG_PATTERN: LogPattern;
|
|
88
42
|
/**
|
|
89
|
-
* The patterns a Python process can be declared to use:
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* A fresh registry per call, so a caller registering its own pattern cannot
|
|
94
|
-
* change what another caller resolves a name to.
|
|
43
|
+
* The patterns a Python process can be declared to use: bracket-tag consoles
|
|
44
|
+
* (rich, `[TAG] message`) plus uvicorn's own. Fresh registry per call, so
|
|
45
|
+
* one caller's registered pattern can't change another's resolution.
|
|
95
46
|
*/
|
|
96
47
|
export declare function pythonLogPatternRegistry(): LogPatternRegistry;
|
|
97
48
|
/** Unchanged in behaviour and in name: exactly `pythonLogPatternRegistry().envelope(["uvicorn"])`, kept because it is the spelling every existing caller and test uses. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"python-log-envelopes.d.ts","sourceRoot":"","sources":["../src/python-log-envelopes.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"python-log-envelopes.d.ts","sourceRoot":"","sources":["../src/python-log-envelopes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAIL,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,kBAAkB,EACxB,MAAM,sCAAsC,CAAC;AAiB9C;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,UAqBjC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,wBAAwB,IAAI,kBAAkB,CAI7D;AAED,2KAA2K;AAC3K,wBAAgB,wBAAwB,IAAI,WAAW,CAEtD"}
|
|
@@ -1,86 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Python's
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* A traceback is written one `write()` per line, so any other thread
|
|
24
|
-
* logging to the same stream interleaves between them. Measured on real
|
|
25
|
-
* threaded CPython 3.12, three runs of 25 failures: 7, 8 and 7 of them were
|
|
26
|
-
* reported as an EXCEPTION whose entire identity was an access-log line for
|
|
27
|
-
* a **successful** request — see `TRAILER_PATTERN` in
|
|
28
|
-
* `python-frame-parsing.ts` for the full measurement and for the heuristic
|
|
29
|
-
* that an un-enveloped stream falls back on.
|
|
30
|
-
*
|
|
31
|
-
* This envelope makes that case structural rather than guessed. uvicorn
|
|
32
|
-
* *states* where a record begins; the trailer pattern can only infer it.
|
|
33
|
-
*
|
|
34
|
-
* ## Nothing is stripped from a traceback, and that is the whole design
|
|
35
|
-
*
|
|
36
|
-
* uvicorn formats with `"%(levelprefix)s %(message)s"`, where `levelprefix`
|
|
37
|
-
* is `f"{levelname}:".ljust(9)`. That prefix goes on the record's **first**
|
|
38
|
-
* line only. The `exc_info` traceback `logging` appends afterwards is
|
|
39
|
-
* written through untouched, at column 0, with CPython's own indentation.
|
|
40
|
-
*
|
|
41
|
-
* Measured against real `uvicorn 0.41.0` / `fastapi 0.141.1` output
|
|
42
|
-
* (`test/fixtures/uvicorn-asgi-failure.txt`): `ERROR: Exception in ASGI
|
|
43
|
-
* application` is the header, and all 28 traceback lines beneath it are
|
|
44
|
-
* continuations returned byte-identical. So `unwrap` is the identity
|
|
45
|
-
* function for every line that carries evidence, and the envelope's entire
|
|
46
|
-
* contribution is `startsRecord` — the opposite balance to Ruby's, where
|
|
2
|
+
* Python's LogEnvelopes. Currently one: uvicorn (RT-106).
|
|
3
|
+
*
|
|
4
|
+
* RT-103 measured Flask/Django/FastAPI all printing tracebacks at column 0
|
|
5
|
+
* with CPython's own indent (logging appends exc_info unindented) and
|
|
6
|
+
* concluded Python needs no envelope. Right about unwrapping, wrong about
|
|
7
|
+
* record boundaries — different jobs behind one interface.
|
|
8
|
+
* UnwrappedLine.startsRecord is the only way a collector knows a line ends
|
|
9
|
+
* the previous record, and interleaved logging threads make that matter: a
|
|
10
|
+
* traceback is written one write() per line, so another thread's logging
|
|
11
|
+
* can land between them. Measured on real threaded CPython 3.12, three runs
|
|
12
|
+
* of 25 failures: 7, 8, 7 reported as an EXCEPTION whose entire identity was
|
|
13
|
+
* an access-log line for a successful request (see TRAILER_PATTERN in
|
|
14
|
+
* python-frame-parsing.ts for the full measurement). This envelope makes
|
|
15
|
+
* uvicorn's record boundary structural instead of inferred.
|
|
16
|
+
*
|
|
17
|
+
* Nothing is stripped from a traceback: uvicorn's `levelprefix` only prefixes
|
|
18
|
+
* the record's first line; the exc_info traceback logging appends afterwards
|
|
19
|
+
* passes through untouched. Measured against real uvicorn 0.41.0/fastapi
|
|
20
|
+
* 0.141.1 output: all 28 traceback lines are continuations returned
|
|
21
|
+
* byte-identical, so unwrap is identity and the envelope's whole
|
|
22
|
+
* contribution is startsRecord — opposite of Ruby's envelope, where
|
|
47
23
|
* unwrapping is the load-bearing half.
|
|
48
24
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* read as a record boundary, closing a block one line before its identity
|
|
53
|
-
* arrives. That is the same class of over-matching this envelope exists to
|
|
54
|
-
* fix, introduced from the other side. So the set is exactly the six level
|
|
55
|
-
* names `logging` and uvicorn can produce, and a seventh would be a
|
|
56
|
-
* measured addition rather than a widened pattern.
|
|
57
|
-
*
|
|
58
|
-
* ## Two disclosed limits
|
|
59
|
-
*
|
|
60
|
-
* **Colour.** uvicorn wraps `levelprefix` in ANSI when its output is a TTY.
|
|
61
|
-
* Descry pipes, so the measured stream has none, and this pattern would not
|
|
62
|
-
* match a colourised one. Stated rather than handled: adding an optional
|
|
63
|
-
* escape-sequence prefix on an unmeasured shape is the guess this file's
|
|
64
|
-
* own argument rejects.
|
|
25
|
+
* Level set is closed, not `\w+`: `^(\w+): +` would match `TypeError: ...`,
|
|
26
|
+
* a real trailer wrongly read as a record boundary. Set is exactly the six
|
|
27
|
+
* level names logging/uvicorn can produce.
|
|
65
28
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* `logging.getLogger(__name__)
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* no idea whether that line began a record, and saying so would be the
|
|
72
|
-
* inference `fields()` exists to refuse.
|
|
29
|
+
* Two disclosed limits: uvicorn wraps levelprefix in ANSI on a TTY (Descry
|
|
30
|
+
* pipes, so unmeasured, unmatched); and application loggers via
|
|
31
|
+
* `logging.getLogger(__name__)` carry no prefix, so they come back
|
|
32
|
+
* untouched with startsRecord: false — correct, since this envelope has no
|
|
33
|
+
* way to know whether such a line began a record.
|
|
73
34
|
*/
|
|
74
35
|
import { createLogPatternRegistry, createPatternLogEnvelope, } from "@descryy/runtime-backend-observation";
|
|
75
36
|
/**
|
|
76
37
|
* `INFO: 127.0.0.1:39334 - "GET /health HTTP/1.1" 200 OK`
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* so the separator is between 1 and 5 spaces depending on the level's own
|
|
81
|
-
* length — `CRITICAL:` needs no padding, `INFO:` needs four. Anchored at
|
|
82
|
-
* column zero: a traceback's frame lines are indented and its trailer is
|
|
83
|
-
* not level-shaped, so neither can reach this.
|
|
38
|
+
* uvicorn's formatters use `levelprefix = f"{levelname}:".ljust(9)`, so 1-5
|
|
39
|
+
* spaces depending on level length. Anchored at column zero: traceback frame
|
|
40
|
+
* lines are indented, its trailer isn't level-shaped.
|
|
84
41
|
*/
|
|
85
42
|
const UVICORN_HEADER = /^(CRITICAL|ERROR|WARNING|INFO|DEBUG|TRACE): +/;
|
|
86
43
|
/**
|
|
@@ -90,16 +47,9 @@ const UVICORN_HEADER = /^(CRITICAL|ERROR|WARNING|INFO|DEBUG|TRACE): +/;
|
|
|
90
47
|
*/
|
|
91
48
|
const ACCESS_MESSAGE = /^(?<client>\S+) - "(?<method>[A-Z]+) (?<path>\S+) (?<protocol>\S+)" (?<status>\d{3})\b/;
|
|
92
49
|
/**
|
|
93
|
-
* uvicorn's wrapper,
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* The regexes and the fields are unchanged — this is the same measurement
|
|
97
|
-
* against the same real `uvicorn 0.41.0` output, moved to where a caller can
|
|
98
|
-
* name it and compose it with another format. What changed is that
|
|
99
|
-
* `PythonRuntimeAdapterOptions.server` is no longer the entire Python
|
|
100
|
-
* vocabulary: a process that prints uvicorn's access lines AND formatted
|
|
101
|
-
* console output can now be given both, which is what UAT phase 2's Phase 9
|
|
102
|
-
* asked for and what a closed union of framework names structurally cannot do.
|
|
50
|
+
* uvicorn's wrapper, as a registry entry rather than a private constant, so a
|
|
51
|
+
* process printing uvicorn's access lines AND formatted console output can
|
|
52
|
+
* be given both — a closed union of framework names couldn't do that.
|
|
103
53
|
*/
|
|
104
54
|
export const UVICORN_LOG_PATTERN = {
|
|
105
55
|
name: "uvicorn",
|
|
@@ -108,16 +58,9 @@ export const UVICORN_LOG_PATTERN = {
|
|
|
108
58
|
fields(match) {
|
|
109
59
|
const access = ACCESS_MESSAGE.exec(match.input.slice(match[0].length));
|
|
110
60
|
return {
|
|
111
|
-
// uvicorn's own word,
|
|
112
|
-
|
|
113
|
-
// uvicorn
|
|
114
|
-
// and `uvicorn.error` are indistinguishable in the printed stream.
|
|
115
|
-
// Null because it is genuinely absent, not merely unparsed.
|
|
116
|
-
category: null,
|
|
117
|
-
// uvicorn has no request-id concept and prints none. Unlike
|
|
118
|
-
// Kestrel's `Request id "..."`, there is nothing here to read, and
|
|
119
|
-
// deriving one from the client port would be an invention.
|
|
120
|
-
requestId: null,
|
|
61
|
+
level: match[1] ?? null, // uvicorn's own word, not normalised
|
|
62
|
+
category: null, // uvicorn's formatter discards the logger name; genuinely absent, not unparsed
|
|
63
|
+
requestId: null, // uvicorn has no request-id concept; deriving one would be invention
|
|
121
64
|
attrs: access?.groups === undefined
|
|
122
65
|
? {}
|
|
123
66
|
: {
|
|
@@ -130,12 +73,9 @@ export const UVICORN_LOG_PATTERN = {
|
|
|
130
73
|
},
|
|
131
74
|
};
|
|
132
75
|
/**
|
|
133
|
-
* The patterns a Python process can be declared to use:
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* A fresh registry per call, so a caller registering its own pattern cannot
|
|
138
|
-
* change what another caller resolves a name to.
|
|
76
|
+
* The patterns a Python process can be declared to use: bracket-tag consoles
|
|
77
|
+
* (rich, `[TAG] message`) plus uvicorn's own. Fresh registry per call, so
|
|
78
|
+
* one caller's registered pattern can't change another's resolution.
|
|
139
79
|
*/
|
|
140
80
|
export function pythonLogPatternRegistry() {
|
|
141
81
|
const registry = createLogPatternRegistry();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"python-log-envelopes.js","sourceRoot":"","sources":["../src/python-log-envelopes.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"python-log-envelopes.js","sourceRoot":"","sources":["../src/python-log-envelopes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EACL,wBAAwB,EACxB,wBAAwB,GAKzB,MAAM,sCAAsC,CAAC;AAE9C;;;;;GAKG;AACH,MAAM,cAAc,GAAG,+CAA+C,CAAC;AAEvE;;;;GAIG;AACH,MAAM,cAAc,GAAG,wFAAwF,CAAC;AAEhH;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAe;IAC7C,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,cAAc;IACtB,kBAAkB,EAAE,IAAI;IACxB,MAAM,CAAC,KAAsB;QAC3B,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QACvE,OAAO;YACL,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,qCAAqC;YAC9D,QAAQ,EAAE,IAAI,EAAE,+EAA+E;YAC/F,SAAS,EAAE,IAAI,EAAE,qEAAqE;YACtF,KAAK,EACH,MAAM,EAAE,MAAM,KAAK,SAAS;gBAC1B,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC;oBACE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE;oBACrC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE;oBACrC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE;oBACjC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE;iBACtC;SACR,CAAC;IACJ,CAAC;CACF,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,wBAAwB;IACtC,MAAM,QAAQ,GAAG,wBAAwB,EAAE,CAAC;IAC5C,QAAQ,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC;IACvC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,2KAA2K;AAC3K,MAAM,UAAU,wBAAwB;IACtC,OAAO,wBAAwB,CAAC,SAAS,EAAE,CAAC,mBAAmB,CAAC,CAAC,CAAC;AACpE,CAAC"}
|
|
@@ -1,66 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The concrete
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* to it, because a V8 header names its exception up front. A CPython
|
|
11
|
-
* traceback names it last. `isTrailer` is that concept, added
|
|
12
|
-
* language-neutrally.
|
|
13
|
-
* - `StackTrace.frames` had no documented order, because with one language
|
|
14
|
-
* nothing ever had to convert. Python prints the opposite of V8. The
|
|
15
|
-
* contract now states most-recent-first and this adapter reverses.
|
|
16
|
-
*
|
|
17
|
-
* Both are recorded because they are the general lesson of adding a second
|
|
18
|
-
* anything: a seam with one implementation is a seam in name only, and the
|
|
19
|
-
* defects it hides are silent ones — a truncated traceback and a
|
|
20
|
-
* backwards frame list both look like well-formed output.
|
|
2
|
+
* The concrete RuntimeAdapter for Python — the second language, first to
|
|
3
|
+
* test whether @descryy/runtime-backend-observation's seam is genuinely
|
|
4
|
+
* language-neutral or merely V8-shaped. It wasn't, in two places, fixed in
|
|
5
|
+
* the seam rather than worked around here:
|
|
6
|
+
* - LineClassifier had no `isTrailer` concept (a V8 header names its
|
|
7
|
+
* exception up front; CPython names it last) — added language-neutrally.
|
|
8
|
+
* - StackTrace.frames had no documented order (Python prints the opposite
|
|
9
|
+
* of V8) — contract now states most-recent-first, this adapter reverses.
|
|
21
10
|
*/
|
|
22
11
|
import type { RuntimeAdapter } from "@descryy/runtime-backend-observation";
|
|
23
12
|
export interface PythonRuntimeAdapterOptions {
|
|
24
13
|
/** Attributed on every emitted Evidence's `service` field (plan §16.7). Omit when not applicable. */
|
|
25
14
|
readonly service?: string;
|
|
26
15
|
/**
|
|
27
|
-
* The server whose logging layer wraps this process's output
|
|
16
|
+
* The server whose logging layer wraps this process's output.
|
|
17
|
+
* Declared, never sniffed — uvicorn's `LEVEL:` format is `logging`'s own
|
|
18
|
+
* default shape, so sniffing it would enable record-boundary flushing for
|
|
19
|
+
* any process that ever printed `INFO: ...`, and a wrong boundary
|
|
20
|
+
* truncates an exception block. Only uvicorn, because only it was
|
|
21
|
+
* measured (RT-106) — Gunicorn's and Django runserver's formats differ
|
|
22
|
+
* and get no envelope until measured.
|
|
28
23
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* padding, which is `logging`'s own default shape; detecting it from a
|
|
32
|
-
* line that looks like one would enable record-boundary flushing for any
|
|
33
|
-
* Python process that ever printed `INFO: ...`, and the cost of a wrong
|
|
34
|
-
* boundary is a truncated exception block. A caller that knows it
|
|
35
|
-
* launched uvicorn says so.
|
|
36
|
-
*
|
|
37
|
-
* Only uvicorn, and only because it was measured (RT-106). Gunicorn's
|
|
38
|
-
* default `[%(asctime)s] [%(process)d] [%(levelname)s]` and Django's
|
|
39
|
-
* `runserver` output are different formats and get no envelope until one
|
|
40
|
-
* of them is run and read.
|
|
41
|
-
*
|
|
42
|
-
* **Superseded in reach, not in rule, by `logPatterns`.** This option is
|
|
43
|
-
* exactly `logPatterns: ["uvicorn"]` and is kept because it is the spelling
|
|
44
|
-
* every existing caller uses. Declaring both is refused rather than merged.
|
|
24
|
+
* Superseded in reach, not in rule, by `logPatterns` (exactly
|
|
25
|
+
* `logPatterns: ["uvicorn"]`); kept for existing callers. Declaring both is refused, not merged.
|
|
45
26
|
*/
|
|
46
27
|
readonly server?: "uvicorn";
|
|
47
28
|
/**
|
|
48
|
-
* Log patterns this process
|
|
49
|
-
*
|
|
50
|
-
* given, first match per line winning.
|
|
29
|
+
* Log patterns this process declares, by name from pythonLogPatternRegistry(),
|
|
30
|
+
* composed into one envelope, first match per line winning.
|
|
51
31
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* pool`, through `rich.console.Console().print()` — had no envelope that
|
|
56
|
-
* could see it, and those were the most operationally interesting lines of
|
|
57
|
-
* the run that found this. `["uvicorn", "bracket-tag-console"]` covers both
|
|
58
|
-
* halves of exactly that process.
|
|
32
|
+
* `server` named only one framework, so a process also printing formatted
|
|
33
|
+
* console output (rich.console.Console().print()) had no envelope for it —
|
|
34
|
+
* `["uvicorn", "bracket-tag-console"]` covers both.
|
|
59
35
|
*
|
|
60
|
-
* Still declared, never sniffed
|
|
61
|
-
* `
|
|
62
|
-
*
|
|
63
|
-
* decide which applies. An unknown name is refused, not skipped.
|
|
36
|
+
* Still declared, never sniffed: `bracket-tag-console` and Ruby's
|
|
37
|
+
* `rails-tagged-logging` are the same regex with opposite record
|
|
38
|
+
* semantics, so no line can decide which applies. Unknown name is refused, not skipped.
|
|
64
39
|
*/
|
|
65
40
|
readonly logPatterns?: readonly string[];
|
|
66
41
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"python-runtime-adapter.d.ts","sourceRoot":"","sources":["../src/python-runtime-adapter.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"python-runtime-adapter.d.ts","sourceRoot":"","sources":["../src/python-runtime-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,cAAc,EAA2C,MAAM,sCAAsC,CAAC;AAUpH,MAAM,WAAW,2BAA2B;IAC1C,qGAAqG;IACrG,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;IAC5B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,2BAAgC,GAAG,cAAc,CAiDpG"}
|
|
@@ -1,23 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The concrete
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* to it, because a V8 header names its exception up front. A CPython
|
|
11
|
-
* traceback names it last. `isTrailer` is that concept, added
|
|
12
|
-
* language-neutrally.
|
|
13
|
-
* - `StackTrace.frames` had no documented order, because with one language
|
|
14
|
-
* nothing ever had to convert. Python prints the opposite of V8. The
|
|
15
|
-
* contract now states most-recent-first and this adapter reverses.
|
|
16
|
-
*
|
|
17
|
-
* Both are recorded because they are the general lesson of adding a second
|
|
18
|
-
* anything: a seam with one implementation is a seam in name only, and the
|
|
19
|
-
* defects it hides are silent ones — a truncated traceback and a
|
|
20
|
-
* backwards frame list both look like well-formed output.
|
|
2
|
+
* The concrete RuntimeAdapter for Python — the second language, first to
|
|
3
|
+
* test whether @descryy/runtime-backend-observation's seam is genuinely
|
|
4
|
+
* language-neutral or merely V8-shaped. It wasn't, in two places, fixed in
|
|
5
|
+
* the seam rather than worked around here:
|
|
6
|
+
* - LineClassifier had no `isTrailer` concept (a V8 header names its
|
|
7
|
+
* exception up front; CPython names it last) — added language-neutrally.
|
|
8
|
+
* - StackTrace.frames had no documented order (Python prints the opposite
|
|
9
|
+
* of V8) — contract now states most-recent-first, this adapter reverses.
|
|
21
10
|
*/
|
|
22
11
|
import { createLogCollector } from "@descryy/runtime-backend-observation";
|
|
23
12
|
import { createPythonLineClassifier } from "./python-frame-parsing.js";
|
|
@@ -34,37 +23,25 @@ export function createPythonRuntimeAdapter(options = {}) {
|
|
|
34
23
|
}
|
|
35
24
|
const patternNames = options.logPatterns ?? (options.server === undefined ? null : [options.server]);
|
|
36
25
|
const registry = patternNames === null ? null : pythonLogPatternRegistry();
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// only threw once a process was already spawned, or -- worse -- that
|
|
40
|
-
// resolved to no envelope at all, would be silence exactly where this
|
|
41
|
-
// option exists to remove it. The envelope itself is still built per
|
|
42
|
-
// collector below, because it is stateful.
|
|
26
|
+
// Checked HERE, not inside createBackendCollectors: an unknown pattern name
|
|
27
|
+
// must fail where the caller declared it, not silently once a process is spawned.
|
|
43
28
|
if (registry !== null && patternNames !== null)
|
|
44
29
|
registry.envelope(patternNames);
|
|
45
30
|
function createBackendCollectors(sources) {
|
|
46
31
|
return sources.map((source) => createLogCollector({
|
|
47
32
|
source,
|
|
48
|
-
// One classifier per collector
|
|
49
|
-
// state, and two processes' output must not prime each other.
|
|
33
|
+
// One classifier per collector: carries per-stream state, must not be shared.
|
|
50
34
|
classifier: createPythonLineClassifier(),
|
|
51
35
|
stackTraceParser,
|
|
52
|
-
// One envelope per collector
|
|
53
|
-
// record's fields, and two processes' output must not attribute
|
|
54
|
-
// each other's. For uvicorn its whole contribution is `startsRecord`
|
|
55
|
-
// — nothing is stripped from a Python traceback (RT-106); for the
|
|
56
|
-
// console patterns it also strips the tag run.
|
|
36
|
+
// One envelope per collector: holds current record's fields, must not be shared.
|
|
57
37
|
...(registry === null || patternNames === null ? {} : { envelope: registry.envelope(patternNames) }),
|
|
58
38
|
...(options.service !== undefined ? { service: options.service } : {}),
|
|
59
39
|
}));
|
|
60
40
|
}
|
|
61
41
|
/**
|
|
62
|
-
* CPython has no `--import
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* *prepended*, never replacing: a target whose own `PYTHONPATH` matters
|
|
66
|
-
* would otherwise stop finding its own modules, which is breaking the
|
|
67
|
-
* application in order to watch it.
|
|
42
|
+
* CPython has no `--import`; it runs `sitecustomize.py` from anywhere on
|
|
43
|
+
* PYTHONPATH at startup, the earliest hook available. Prepended, never
|
|
44
|
+
* replacing — a target's own PYTHONPATH must still resolve its own modules.
|
|
68
45
|
*/
|
|
69
46
|
function outboundHttpLaunch() {
|
|
70
47
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"python-runtime-adapter.js","sourceRoot":"","sources":["../src/python-runtime-adapter.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"python-runtime-adapter.js","sourceRoot":"","sources":["../src/python-runtime-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAE1E,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAAE,kCAAkC,EAAE,MAAM,sCAAsC,CAAC;AAC1F,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAC9E,OAAO,EAAE,gCAAgC,EAAE,MAAM,oCAAoC,CAAC;AAiCtF,MAAM,UAAU,0BAA0B,CAAC,UAAuC,EAAE;IAClF,MAAM,sBAAsB,GAAG,kCAAkC,EAAE,CAAC;IACpE,MAAM,gBAAgB,GAAG,4BAA4B,CAAC,sBAAsB,CAAC,CAAC;IAE9E,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACtE,MAAM,IAAI,KAAK,CACb,+GAA+G;YAC7G,gGAAgG,CACnG,CAAC;IACJ,CAAC;IACD,MAAM,YAAY,GAAG,OAAO,CAAC,WAAW,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IACrG,MAAM,QAAQ,GAAG,YAAY,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,wBAAwB,EAAE,CAAC;IAC3E,4EAA4E;IAC5E,kFAAkF;IAClF,IAAI,QAAQ,KAAK,IAAI,IAAI,YAAY,KAAK,IAAI;QAAE,QAAQ,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IAEhF,SAAS,uBAAuB,CAAC,OAAuC;QACtE,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAC5B,kBAAkB,CAAC;YACjB,MAAM;YACN,8EAA8E;YAC9E,UAAU,EAAE,0BAA0B,EAAE;YACxC,gBAAgB;YAChB,iFAAiF;YACjF,GAAG,CAAC,QAAQ,KAAK,IAAI,IAAI,YAAY,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;YACpG,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;;;;OAIG;IACH,SAAS,kBAAkB;QACzB,OAAO;YACL,GAAG,EAAE,EAAE,UAAU,EAAE,gCAAgC,EAAE;YACrD,iBAAiB,EAAE,CAAC,YAAY,CAAC;SAClC,CAAC;IACJ,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,QAAQ;QAClB,gBAAgB;QAChB,sBAAsB;QACtB,uBAAuB;QACvB,kBAAkB;KACnB,CAAC;AACJ,CAAC"}
|
|
@@ -1,32 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Python
|
|
2
|
+
* Python SourceLocationResolver.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Contrast the Node resolver, which must decide between four values because
|
|
9
|
-
* a source map is a second file that can be absent, stale, or wrong.
|
|
4
|
+
* Every location is `self-contained` (a positive claim, not a fallback):
|
|
5
|
+
* CPython captures the path/line of the .py file it's actually executing,
|
|
6
|
+
* no compile step or map to drift. Contrast the Node resolver, which must
|
|
7
|
+
* choose between four values because a source map can be absent/stale/wrong.
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* Deliberately does not check the file exists on disk or still matches what
|
|
10
|
+
* ran — `self-contained` is a statement about mechanism, not the working
|
|
11
|
+
* tree, and a stat call per frame buys only latency. Pseudo-files
|
|
12
|
+
* (`<string>`, `<stdin>`, `<frozen importlib._bootstrap>`) pass through verbatim.
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* *mechanism*, not about whether the working tree has since been edited —
|
|
19
|
-
* and a stat call per frame would buy nothing but latency. CPython's
|
|
20
|
-
* pseudo-files (`<string>`, `<stdin>`, `<frozen importlib._bootstrap>`) are
|
|
21
|
-
* passed through verbatim for the same reason: they are the honest location,
|
|
22
|
-
* and rewriting them to null would discard real information.
|
|
23
|
-
*
|
|
24
|
-
* `column` is always null. CPython does not print columns; 3.11+ prints a
|
|
25
|
-
* caret line under the failing expression, from which a column *could* be
|
|
26
|
-
* inferred by counting characters. That inference is not made here — it
|
|
27
|
-
* would be a derived guess presented in a field callers read as captured
|
|
28
|
-
* fact, and the caret line is preserved verbatim in `StackFrame.raw`
|
|
29
|
-
* regardless.
|
|
14
|
+
* `column` is always null — CPython doesn't print one. 3.11+'s caret line
|
|
15
|
+
* could be used to infer a column by counting characters, but that would be
|
|
16
|
+
* a derived guess in a field callers read as fact; the caret line stays verbatim in `StackFrame.raw`.
|
|
30
17
|
*/
|
|
31
18
|
import type { SourceLocationResolver } from "@descryy/runtime-backend-observation";
|
|
32
19
|
export declare function createPythonSourceLocationResolver(): SourceLocationResolver;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"python-source-location-resolver.d.ts","sourceRoot":"","sources":["../src/python-source-location-resolver.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"python-source-location-resolver.d.ts","sourceRoot":"","sources":["../src/python-source-location-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAErG,wBAAgB,kCAAkC,IAAI,sBAAsB,CAc3E"}
|