@oneuptime/common 10.5.9 → 10.5.18
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/Models/AnalyticsModels/ExceptionInstance.ts +1 -1
- package/Models/AnalyticsModels/Log.ts +1 -1
- package/Models/AnalyticsModels/Metric.ts +1 -1
- package/Models/AnalyticsModels/Profile.ts +1 -1
- package/Models/AnalyticsModels/ProfileSample.ts +1 -1
- package/Models/AnalyticsModels/Span.ts +1 -1
- package/Models/DatabaseModels/TelemetryException.ts +46 -34
- package/Models/DatabaseModels/TelemetryUsageBilling.ts +35 -2
- package/Server/API/AIAgentDataAPI.ts +25 -7
- package/Server/API/TelemetryAPI.ts +6 -0
- package/Server/API/TelemetryExceptionAPI.ts +6 -2
- package/Server/EnvironmentConfig.ts +27 -0
- package/Server/Infrastructure/ClickhouseDatabase.ts +21 -1
- package/Server/Infrastructure/Postgres/DataSourceOptions.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.ts +28 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.ts +24 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.ts +47 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.ts +34 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Infrastructure/PostgresDatabase.ts +27 -1
- package/Server/Infrastructure/QueueWorker.ts +54 -4
- package/Server/Infrastructure/Redis.ts +11 -0
- package/Server/Services/AnalyticsDatabaseService.ts +87 -0
- package/Server/Services/DatabaseService.ts +73 -0
- package/Server/Services/TelemetryAttributeService.ts +38 -2
- package/Server/Services/TelemetryExceptionService.ts +24 -49
- package/Server/Services/TelemetryUsageBillingService.ts +289 -166
- package/Server/Types/AnalyticsDatabase/ModelPermission.ts +102 -72
- package/Server/Types/Database/Permissions/OwnedScopePermission.ts +81 -60
- package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +67 -0
- package/Server/Utils/Express.ts +32 -0
- package/Server/Utils/GracefulShutdown.ts +194 -0
- package/Server/Utils/Logger.ts +12 -1
- package/Server/Utils/Monitor/MonitorLogUtil.ts +22 -17
- package/Server/Utils/Profiling.ts +14 -6
- package/Server/Utils/StartServer.ts +13 -5
- package/Server/Utils/Telemetry/ContextSpanProcessor.ts +48 -0
- package/Server/Utils/Telemetry/LogExceptionExtractor.ts +289 -0
- package/Server/Utils/Telemetry/SpanUtil.ts +16 -35
- package/Server/Utils/Telemetry/StackTraceParser.ts +423 -0
- package/Server/Utils/Telemetry/TelemetryContext.ts +190 -0
- package/Server/Utils/Telemetry.ts +33 -7
- package/Tests/Server/Services/TelemetryAttributeService.test.ts +83 -0
- package/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.ts +0 -0
- package/Types/Database/AccessControl/OwnedThrough.ts +31 -3
- package/Types/Telemetry/ServiceType.ts +10 -0
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +7 -1
- package/UI/Components/Dictionary/Dictionary.tsx +19 -0
- package/UI/Components/Filters/FiltersForm.tsx +1 -0
- package/UI/Components/Filters/JSONFilter.tsx +2 -0
- package/UI/Components/Filters/Types/Filter.ts +1 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +16 -0
- package/UI/Utils/Project.ts +6 -0
- package/UI/Utils/Telemetry/Telemetry.ts +65 -0
- package/UI/Utils/TelemetryService.ts +150 -0
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -1
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Log.js +1 -1
- package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Metric.js +1 -1
- package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Profile.js +1 -1
- package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/ProfileSample.js +1 -1
- package/build/dist/Models/AnalyticsModels/ProfileSample.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Span.js +1 -1
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryException.js +47 -33
- package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryUsageBilling.js +36 -2
- package/build/dist/Models/DatabaseModels/TelemetryUsageBilling.js.map +1 -1
- package/build/dist/Server/API/AIAgentDataAPI.js +24 -8
- package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +4 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryExceptionAPI.js +6 -2
- package/build/dist/Server/API/TelemetryExceptionAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +19 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/ClickhouseDatabase.js +16 -2
- package/build/dist/Server/Infrastructure/ClickhouseDatabase.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +10 -9
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.js +19 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.js +25 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/PostgresDatabase.js +20 -1
- package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
- package/build/dist/Server/Infrastructure/QueueWorker.js +40 -3
- package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
- package/build/dist/Server/Infrastructure/Redis.js +5 -0
- package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +59 -0
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +62 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryAttributeService.js +23 -1
- package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryExceptionService.js +16 -41
- package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryUsageBillingService.js +211 -147
- package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +84 -63
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/OwnedScopePermission.js +67 -49
- package/build/dist/Server/Types/Database/Permissions/OwnedScopePermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +51 -0
- package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Express.js +23 -0
- package/build/dist/Server/Utils/Express.js.map +1 -1
- package/build/dist/Server/Utils/GracefulShutdown.js +145 -0
- package/build/dist/Server/Utils/GracefulShutdown.js.map +1 -0
- package/build/dist/Server/Utils/Logger.js +8 -1
- package/build/dist/Server/Utils/Logger.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -10
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
- package/build/dist/Server/Utils/Profiling.js +8 -3
- package/build/dist/Server/Utils/Profiling.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +12 -4
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ContextSpanProcessor.js +37 -0
- package/build/dist/Server/Utils/Telemetry/ContextSpanProcessor.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +214 -0
- package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/SpanUtil.js +15 -24
- package/build/dist/Server/Utils/Telemetry/SpanUtil.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/StackTraceParser.js +365 -0
- package/build/dist/Server/Utils/Telemetry/StackTraceParser.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryContext.js +124 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryContext.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry.js +22 -5
- package/build/dist/Server/Utils/Telemetry.js.map +1 -1
- package/build/dist/Tests/Server/Services/TelemetryAttributeService.test.js +50 -0
- package/build/dist/Tests/Server/Services/TelemetryAttributeService.test.js.map +1 -0
- package/build/dist/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.js +0 -0
- package/build/dist/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.js.map +1 -0
- package/build/dist/Types/Database/AccessControl/OwnedThrough.js +7 -2
- package/build/dist/Types/Database/AccessControl/OwnedThrough.js.map +1 -1
- package/build/dist/Types/Telemetry/ServiceType.js +10 -0
- package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +7 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js +10 -0
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Filters/FiltersForm.js +1 -1
- package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
- package/build/dist/UI/Components/Filters/JSONFilter.js +1 -1
- package/build/dist/UI/Components/Filters/JSONFilter.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +15 -0
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Utils/Project.js +5 -0
- package/build/dist/UI/Utils/Project.js.map +1 -1
- package/build/dist/UI/Utils/Telemetry/Telemetry.js +44 -0
- package/build/dist/UI/Utils/Telemetry/Telemetry.js.map +1 -1
- package/build/dist/UI/Utils/TelemetryService.js +113 -0
- package/build/dist/UI/Utils/TelemetryService.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import OpenTelemetryAPI, { Span } from "@opentelemetry/api";
|
|
2
1
|
import { DisableTelemetry } from "../../EnvironmentConfig";
|
|
2
|
+
import TelemetryContext from "./TelemetryContext";
|
|
3
|
+
import OpenTelemetryAPI, { Span } from "@opentelemetry/api";
|
|
3
4
|
|
|
4
5
|
export interface SpanAttributes {
|
|
5
6
|
userId?: string | undefined;
|
|
@@ -21,14 +22,26 @@ export interface SpanAttributes {
|
|
|
21
22
|
|
|
22
23
|
export default class SpanUtil {
|
|
23
24
|
/**
|
|
24
|
-
* Add attributes to the
|
|
25
|
-
*
|
|
25
|
+
* Add attributes to the current unit of work.
|
|
26
|
+
*
|
|
27
|
+
* This does two things:
|
|
28
|
+
* 1. Merges the attributes into the ambient {@link TelemetryContext} so that
|
|
29
|
+
* every span and log produced later in this request/job/check inherits
|
|
30
|
+
* them (OTel span attributes do NOT propagate parent -> child on their
|
|
31
|
+
* own, so this is what actually makes context flow downstream).
|
|
32
|
+
* 2. Tags the currently active span immediately, if there is one.
|
|
33
|
+
*
|
|
34
|
+
* Safe to call even when there is no active span or scope, or when telemetry
|
|
35
|
+
* is disabled.
|
|
26
36
|
*/
|
|
27
37
|
public static addAttributesToCurrentSpan(attributes: SpanAttributes): void {
|
|
28
38
|
if (DisableTelemetry) {
|
|
29
39
|
return;
|
|
30
40
|
}
|
|
31
41
|
|
|
42
|
+
// Propagate to all downstream spans + logs via the ambient context.
|
|
43
|
+
TelemetryContext.setAttributes(attributes);
|
|
44
|
+
|
|
32
45
|
const span: Span | undefined = OpenTelemetryAPI.trace.getActiveSpan();
|
|
33
46
|
|
|
34
47
|
if (!span) {
|
|
@@ -55,36 +68,4 @@ export default class SpanUtil {
|
|
|
55
68
|
}
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Build span attributes from a request-like object.
|
|
61
|
-
* Similar to getLogAttributesFromRequest in Logger but for spans.
|
|
62
|
-
*/
|
|
63
|
-
public static getSpanAttributesFromRequest(
|
|
64
|
-
req?: {
|
|
65
|
-
requestId?: string;
|
|
66
|
-
tenantId?: { toString(): string };
|
|
67
|
-
userAuthorization?: { userId?: { toString(): string } };
|
|
68
|
-
} | null,
|
|
69
|
-
): SpanAttributes {
|
|
70
|
-
if (!req) {
|
|
71
|
-
return {};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const attributes: SpanAttributes = {};
|
|
75
|
-
|
|
76
|
-
if (req.requestId) {
|
|
77
|
-
attributes["requestId"] = req.requestId;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (req.tenantId) {
|
|
81
|
-
attributes["projectId"] = req.tenantId.toString();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (req.userAuthorization?.userId) {
|
|
85
|
-
attributes["userId"] = req.userAuthorization.userId.toString();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return attributes;
|
|
89
|
-
}
|
|
90
71
|
}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stack trace parser that transforms raw stack trace strings into structured frames.
|
|
3
|
+
* Supports JavaScript/Node.js, Python, Java, Go, Ruby, C#/.NET, and PHP stack traces.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface StackFrame {
|
|
7
|
+
functionName: string;
|
|
8
|
+
fileName: string;
|
|
9
|
+
lineNumber: number;
|
|
10
|
+
columnNumber?: number;
|
|
11
|
+
inApp: boolean; // true if user code, false if library/framework code
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ParsedStackTrace {
|
|
15
|
+
frames: StackFrame[];
|
|
16
|
+
raw: string; // original raw stack trace string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Known library/framework path patterns that indicate non-app code
|
|
20
|
+
const LIBRARY_PATTERNS: Array<RegExp> = [
|
|
21
|
+
// Node.js internals
|
|
22
|
+
/^node:/,
|
|
23
|
+
/^internal\//,
|
|
24
|
+
/node_modules\//,
|
|
25
|
+
/^events\.js$/,
|
|
26
|
+
/^timers\.js$/,
|
|
27
|
+
/^util\.js$/,
|
|
28
|
+
/^net\.js$/,
|
|
29
|
+
/^stream\.js$/,
|
|
30
|
+
/^buffer\.js$/,
|
|
31
|
+
// Python
|
|
32
|
+
/\/site-packages\//,
|
|
33
|
+
/\/dist-packages\//,
|
|
34
|
+
/\/lib\/python\d+\.\d+\//,
|
|
35
|
+
/\/usr\/lib\//,
|
|
36
|
+
/\/usr\/local\/lib\//,
|
|
37
|
+
/\/venv\//,
|
|
38
|
+
/\/\.venv\//,
|
|
39
|
+
/\/virtualenv\//,
|
|
40
|
+
// Java
|
|
41
|
+
/^java\./,
|
|
42
|
+
/^javax\./,
|
|
43
|
+
/^sun\./,
|
|
44
|
+
/^com\.sun\./,
|
|
45
|
+
/^org\.springframework\./,
|
|
46
|
+
/^org\.apache\./,
|
|
47
|
+
/^org\.hibernate\./,
|
|
48
|
+
/^org\.eclipse\./,
|
|
49
|
+
/^io\.netty\./,
|
|
50
|
+
/^com\.google\./,
|
|
51
|
+
/^org\.junit\./,
|
|
52
|
+
// Go
|
|
53
|
+
/^runtime\//,
|
|
54
|
+
/^net\/http\//,
|
|
55
|
+
/^testing\//,
|
|
56
|
+
/\/vendor\//,
|
|
57
|
+
/\/pkg\/mod\//,
|
|
58
|
+
// Ruby
|
|
59
|
+
/\/gems\//,
|
|
60
|
+
/\/rubygems\//,
|
|
61
|
+
/\/ruby\/\d+\.\d+\.\d+\//,
|
|
62
|
+
// C#/.NET
|
|
63
|
+
/^System\./,
|
|
64
|
+
/^Microsoft\./,
|
|
65
|
+
/^Newtonsoft\./,
|
|
66
|
+
// PHP
|
|
67
|
+
/\/vendor\//,
|
|
68
|
+
/^phar:\/\//,
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
export default class StackTraceParser {
|
|
72
|
+
/**
|
|
73
|
+
* Parse a raw stack trace string into structured frames.
|
|
74
|
+
* Auto-detects the language and applies the appropriate parser.
|
|
75
|
+
*/
|
|
76
|
+
public static parse(rawStackTrace: string): ParsedStackTrace {
|
|
77
|
+
if (!rawStackTrace || rawStackTrace.trim().length === 0) {
|
|
78
|
+
return { frames: [], raw: rawStackTrace || "" };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const lines: string[] = rawStackTrace.split("\n").map((l: string) => {
|
|
82
|
+
return l.trim();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Try each parser and use the one that produces the most frames
|
|
86
|
+
const parsers: Array<(lines: string[]) => StackFrame[]> = [
|
|
87
|
+
StackTraceParser.parseJavaScript,
|
|
88
|
+
StackTraceParser.parsePython,
|
|
89
|
+
StackTraceParser.parseJava,
|
|
90
|
+
StackTraceParser.parseGo,
|
|
91
|
+
StackTraceParser.parseRuby,
|
|
92
|
+
StackTraceParser.parseCSharp,
|
|
93
|
+
StackTraceParser.parsePHP,
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
let bestFrames: StackFrame[] = [];
|
|
97
|
+
|
|
98
|
+
for (const parser of parsers) {
|
|
99
|
+
try {
|
|
100
|
+
const frames: StackFrame[] = parser(lines);
|
|
101
|
+
if (frames.length > bestFrames.length) {
|
|
102
|
+
bestFrames = frames;
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// Skip failing parsers
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
frames: bestFrames,
|
|
111
|
+
raw: rawStackTrace,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Determine if a file path is application code (not library/framework).
|
|
117
|
+
*/
|
|
118
|
+
private static isAppCode(filePath: string): boolean {
|
|
119
|
+
if (!filePath) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const pattern of LIBRARY_PATTERNS) {
|
|
124
|
+
if (pattern.test(filePath)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parse JavaScript/Node.js stack traces.
|
|
134
|
+
* Format: `at functionName (filePath:line:col)` or `at filePath:line:col`
|
|
135
|
+
*/
|
|
136
|
+
private static parseJavaScript(lines: string[]): StackFrame[] {
|
|
137
|
+
const frames: StackFrame[] = [];
|
|
138
|
+
|
|
139
|
+
// Pattern 1: at functionName (filePath:line:col)
|
|
140
|
+
const patternWithParens: RegExp = /^at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)$/;
|
|
141
|
+
// Pattern 2: at filePath:line:col
|
|
142
|
+
const patternWithoutParens: RegExp = /^at\s+(.+?):(\d+):(\d+)$/;
|
|
143
|
+
// Pattern 3: at functionName (filePath:line)
|
|
144
|
+
const patternWithParensNoCol: RegExp = /^at\s+(.+?)\s+\((.+?):(\d+)\)$/;
|
|
145
|
+
// Pattern 4: at eval (eval at functionName (filePath:line:col))
|
|
146
|
+
const patternEval: RegExp =
|
|
147
|
+
/^at\s+eval\s+\(eval\s+at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/;
|
|
148
|
+
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
let match: RegExpMatchArray | null = null;
|
|
151
|
+
|
|
152
|
+
match = line.match(patternEval);
|
|
153
|
+
if (match) {
|
|
154
|
+
frames.push({
|
|
155
|
+
functionName: `eval at ${match[1]!}`,
|
|
156
|
+
fileName: match[2]!,
|
|
157
|
+
lineNumber: parseInt(match[3]!, 10),
|
|
158
|
+
columnNumber: parseInt(match[4]!, 10),
|
|
159
|
+
inApp: StackTraceParser.isAppCode(match[2]!),
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
match = line.match(patternWithParens);
|
|
165
|
+
if (match) {
|
|
166
|
+
frames.push({
|
|
167
|
+
functionName: match[1]!,
|
|
168
|
+
fileName: match[2]!,
|
|
169
|
+
lineNumber: parseInt(match[3]!, 10),
|
|
170
|
+
columnNumber: parseInt(match[4]!, 10),
|
|
171
|
+
inApp: StackTraceParser.isAppCode(match[2]!),
|
|
172
|
+
});
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
match = line.match(patternWithParensNoCol);
|
|
177
|
+
if (match) {
|
|
178
|
+
frames.push({
|
|
179
|
+
functionName: match[1]!,
|
|
180
|
+
fileName: match[2]!,
|
|
181
|
+
lineNumber: parseInt(match[3]!, 10),
|
|
182
|
+
inApp: StackTraceParser.isAppCode(match[2]!),
|
|
183
|
+
});
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
match = line.match(patternWithoutParens);
|
|
188
|
+
if (match) {
|
|
189
|
+
frames.push({
|
|
190
|
+
functionName: "<anonymous>",
|
|
191
|
+
fileName: match[1]!,
|
|
192
|
+
lineNumber: parseInt(match[2]!, 10),
|
|
193
|
+
columnNumber: parseInt(match[3]!, 10),
|
|
194
|
+
inApp: StackTraceParser.isAppCode(match[1]!),
|
|
195
|
+
});
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return frames;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Parse Python stack traces.
|
|
205
|
+
* Format: `File "path", line N, in function`
|
|
206
|
+
*/
|
|
207
|
+
private static parsePython(lines: string[]): StackFrame[] {
|
|
208
|
+
const frames: StackFrame[] = [];
|
|
209
|
+
const pattern: RegExp =
|
|
210
|
+
/^File\s+"(.+?)",\s+line\s+(\d+)(?:,\s+in\s+(.+))?$/;
|
|
211
|
+
|
|
212
|
+
for (const line of lines) {
|
|
213
|
+
const match: RegExpMatchArray | null = line.match(pattern);
|
|
214
|
+
if (match) {
|
|
215
|
+
frames.push({
|
|
216
|
+
functionName: match[3] || "<module>",
|
|
217
|
+
fileName: match[1]!,
|
|
218
|
+
lineNumber: parseInt(match[2]!, 10),
|
|
219
|
+
inApp: StackTraceParser.isAppCode(match[1]!),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return frames;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Parse Java stack traces.
|
|
229
|
+
* Format: `at package.Class.method(File.java:line)`
|
|
230
|
+
*/
|
|
231
|
+
private static parseJava(lines: string[]): StackFrame[] {
|
|
232
|
+
const frames: StackFrame[] = [];
|
|
233
|
+
// Pattern: at com.package.Class.method(File.java:123)
|
|
234
|
+
const pattern: RegExp = /^at\s+([\w.$]+)\(([\w.]+):(\d+)\)$/;
|
|
235
|
+
// Pattern for native methods: at com.package.Class.method(Native Method)
|
|
236
|
+
const patternNative: RegExp = /^at\s+([\w.$]+)\(Native Method\)$/;
|
|
237
|
+
// Pattern for unknown source: at com.package.Class.method(Unknown Source)
|
|
238
|
+
const patternUnknown: RegExp = /^at\s+([\w.$]+)\(Unknown Source\)$/;
|
|
239
|
+
|
|
240
|
+
for (const line of lines) {
|
|
241
|
+
let match: RegExpMatchArray | null = null;
|
|
242
|
+
|
|
243
|
+
match = line.match(pattern);
|
|
244
|
+
if (match) {
|
|
245
|
+
const fullMethod: string = match[1]!;
|
|
246
|
+
frames.push({
|
|
247
|
+
functionName: fullMethod,
|
|
248
|
+
fileName: match[2]!,
|
|
249
|
+
lineNumber: parseInt(match[3]!, 10),
|
|
250
|
+
inApp: StackTraceParser.isAppCode(fullMethod),
|
|
251
|
+
});
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
match = line.match(patternNative);
|
|
256
|
+
if (match) {
|
|
257
|
+
frames.push({
|
|
258
|
+
functionName: match[1]!,
|
|
259
|
+
fileName: "Native Method",
|
|
260
|
+
lineNumber: 0,
|
|
261
|
+
inApp: false,
|
|
262
|
+
});
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
match = line.match(patternUnknown);
|
|
267
|
+
if (match) {
|
|
268
|
+
frames.push({
|
|
269
|
+
functionName: match[1]!,
|
|
270
|
+
fileName: "Unknown Source",
|
|
271
|
+
lineNumber: 0,
|
|
272
|
+
inApp: StackTraceParser.isAppCode(match[1]!),
|
|
273
|
+
});
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return frames;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Parse Go stack traces.
|
|
283
|
+
* Format: `package/file.go:line +0xNN` or `goroutine N [reason]:`
|
|
284
|
+
* Go stack traces have pairs of lines:
|
|
285
|
+
* functionName(args)
|
|
286
|
+
* /path/to/file.go:line +0xNN
|
|
287
|
+
*/
|
|
288
|
+
private static parseGo(lines: string[]): StackFrame[] {
|
|
289
|
+
const frames: StackFrame[] = [];
|
|
290
|
+
const filePattern: RegExp = /^(.+\.go):(\d+)\s*(?:\+0x[0-9a-f]+)?$/;
|
|
291
|
+
|
|
292
|
+
for (let i: number = 0; i < lines.length; i++) {
|
|
293
|
+
const line: string = lines[i]!;
|
|
294
|
+
|
|
295
|
+
// Skip goroutine headers
|
|
296
|
+
if (line.startsWith("goroutine ")) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Look for file:line pattern
|
|
301
|
+
const match: RegExpMatchArray | null = line.match(filePattern);
|
|
302
|
+
if (match) {
|
|
303
|
+
// The previous line should be the function name
|
|
304
|
+
let functionName: string = "<unknown>";
|
|
305
|
+
if (i > 0 && lines[i - 1]) {
|
|
306
|
+
// Remove arguments from function name
|
|
307
|
+
const funcLine: string = lines[i - 1]!;
|
|
308
|
+
const parenIndex: number = funcLine.indexOf("(");
|
|
309
|
+
functionName =
|
|
310
|
+
parenIndex > 0 ? funcLine.substring(0, parenIndex) : funcLine;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
frames.push({
|
|
314
|
+
functionName: functionName,
|
|
315
|
+
fileName: match[1]!,
|
|
316
|
+
lineNumber: parseInt(match[2]!, 10),
|
|
317
|
+
inApp: StackTraceParser.isAppCode(match[1]!),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return frames;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Parse Ruby stack traces.
|
|
327
|
+
* Format: `file:line:in 'method'` or `file:line:in \`method'`
|
|
328
|
+
*/
|
|
329
|
+
private static parseRuby(lines: string[]): StackFrame[] {
|
|
330
|
+
const frames: StackFrame[] = [];
|
|
331
|
+
const pattern: RegExp = /^(.+?):(\d+):in\s+[`'](.+?)'$/;
|
|
332
|
+
|
|
333
|
+
for (const line of lines) {
|
|
334
|
+
const match: RegExpMatchArray | null = line.match(pattern);
|
|
335
|
+
if (match) {
|
|
336
|
+
frames.push({
|
|
337
|
+
functionName: match[3]!,
|
|
338
|
+
fileName: match[1]!,
|
|
339
|
+
lineNumber: parseInt(match[2]!, 10),
|
|
340
|
+
inApp: StackTraceParser.isAppCode(match[1]!),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return frames;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Parse C#/.NET stack traces.
|
|
350
|
+
* Format: `at Namespace.Class.Method(params) in file:line N`
|
|
351
|
+
*/
|
|
352
|
+
private static parseCSharp(lines: string[]): StackFrame[] {
|
|
353
|
+
const frames: StackFrame[] = [];
|
|
354
|
+
// Pattern: at Namespace.Class.Method(params) in /path/to/file.cs:line 42
|
|
355
|
+
const patternWithFile: RegExp = /^at\s+(.+?)\s+in\s+(.+?):line\s+(\d+)$/;
|
|
356
|
+
// Pattern: at Namespace.Class.Method(params)
|
|
357
|
+
const patternWithoutFile: RegExp = /^at\s+([\w.<>+]+\(.*?\))$/;
|
|
358
|
+
|
|
359
|
+
for (const line of lines) {
|
|
360
|
+
let match: RegExpMatchArray | null = null;
|
|
361
|
+
|
|
362
|
+
match = line.match(patternWithFile);
|
|
363
|
+
if (match) {
|
|
364
|
+
frames.push({
|
|
365
|
+
functionName: match[1]!,
|
|
366
|
+
fileName: match[2]!,
|
|
367
|
+
lineNumber: parseInt(match[3]!, 10),
|
|
368
|
+
inApp: StackTraceParser.isAppCode(match[1]!),
|
|
369
|
+
});
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
match = line.match(patternWithoutFile);
|
|
374
|
+
if (match) {
|
|
375
|
+
frames.push({
|
|
376
|
+
functionName: match[1]!,
|
|
377
|
+
fileName: "",
|
|
378
|
+
lineNumber: 0,
|
|
379
|
+
inApp: StackTraceParser.isAppCode(match[1]!),
|
|
380
|
+
});
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return frames;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Parse PHP stack traces.
|
|
390
|
+
* Format: `#N /path/to/file.php(line): Class->method()`
|
|
391
|
+
*/
|
|
392
|
+
private static parsePHP(lines: string[]): StackFrame[] {
|
|
393
|
+
const frames: StackFrame[] = [];
|
|
394
|
+
// Pattern: #0 /path/to/file.php(42): ClassName->method()
|
|
395
|
+
const pattern: RegExp = /^#\d+\s+(.+?)\((\d+)\):\s+(.+)$/;
|
|
396
|
+
// Pattern: #0 {main}
|
|
397
|
+
const patternMain: RegExp = /^#\d+\s+\{main\}$/;
|
|
398
|
+
|
|
399
|
+
for (const line of lines) {
|
|
400
|
+
if (patternMain.test(line)) {
|
|
401
|
+
frames.push({
|
|
402
|
+
functionName: "{main}",
|
|
403
|
+
fileName: "",
|
|
404
|
+
lineNumber: 0,
|
|
405
|
+
inApp: true,
|
|
406
|
+
});
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const match: RegExpMatchArray | null = line.match(pattern);
|
|
411
|
+
if (match) {
|
|
412
|
+
frames.push({
|
|
413
|
+
functionName: match[3]!.replace(/\(\)$/, ""),
|
|
414
|
+
fileName: match[1]!,
|
|
415
|
+
lineNumber: parseInt(match[2]!, 10),
|
|
416
|
+
inApp: StackTraceParser.isAppCode(match[1]!),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return frames;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { DisableTelemetry } from "../../EnvironmentConfig";
|
|
2
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
3
|
+
|
|
4
|
+
export type TelemetryContextAttributeValue =
|
|
5
|
+
| string
|
|
6
|
+
| number
|
|
7
|
+
| boolean
|
|
8
|
+
| undefined;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Canonical set of tenant/business attributes we propagate across a unit of
|
|
12
|
+
* work (an HTTP request, a worker job, a probe check, a cron run). The open
|
|
13
|
+
* index signature allows additional ad-hoc keys, but prefer the named ones so
|
|
14
|
+
* dashboards and queries stay consistent.
|
|
15
|
+
*/
|
|
16
|
+
export interface TelemetryContextAttributes {
|
|
17
|
+
userId?: string | undefined;
|
|
18
|
+
projectId?: string | undefined;
|
|
19
|
+
requestId?: string | undefined;
|
|
20
|
+
incidentId?: string | undefined;
|
|
21
|
+
alertId?: string | undefined;
|
|
22
|
+
monitorId?: string | undefined;
|
|
23
|
+
statusPageId?: string | undefined;
|
|
24
|
+
scheduledMaintenanceId?: string | undefined;
|
|
25
|
+
onCallDutyPolicyId?: string | undefined;
|
|
26
|
+
onCallDutyPolicyScheduleId?: string | undefined;
|
|
27
|
+
incidentEpisodeId?: string | undefined;
|
|
28
|
+
alertEpisodeId?: string | undefined;
|
|
29
|
+
workspaceType?: string | undefined;
|
|
30
|
+
channelId?: string | undefined;
|
|
31
|
+
[key: string]: TelemetryContextAttributeValue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface TelemetryContextStore {
|
|
35
|
+
attributes: Record<string, string | number | boolean>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Ambient, mutable telemetry context backed by AsyncLocalStorage.
|
|
40
|
+
*
|
|
41
|
+
* Why this exists: OpenTelemetry span attributes do NOT propagate from a
|
|
42
|
+
* parent span to its children, and OneUptime sets tenant context (projectId,
|
|
43
|
+
* userId, ...) deep in middleware while the spans that matter are created much
|
|
44
|
+
* further down the call stack. Rather than thread attributes through every
|
|
45
|
+
* function or tag ~1958 `@CaptureSpan` call sites by hand, we keep a small
|
|
46
|
+
* mutable attribute bag scoped to the current unit of work. `ContextSpanProcessor`
|
|
47
|
+
* stamps it onto every span at creation, and `Logger` merges it into every log
|
|
48
|
+
* record — so context flows everywhere automatically.
|
|
49
|
+
*
|
|
50
|
+
* Seed a scope at each entry point with `runWithContext`, then enrich it as
|
|
51
|
+
* more identifiers become known with `setAttributes`.
|
|
52
|
+
*/
|
|
53
|
+
export default class TelemetryContext {
|
|
54
|
+
private static storage: AsyncLocalStorage<TelemetryContextStore> =
|
|
55
|
+
new AsyncLocalStorage<TelemetryContextStore>();
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Identifier keys we look for when seeding context from an arbitrary payload
|
|
59
|
+
* (see {@link pickKnownAttributes}).
|
|
60
|
+
*/
|
|
61
|
+
private static readonly KNOWN_ID_KEYS: Array<string> = [
|
|
62
|
+
"projectId",
|
|
63
|
+
"userId",
|
|
64
|
+
"monitorId",
|
|
65
|
+
"incidentId",
|
|
66
|
+
"alertId",
|
|
67
|
+
"statusPageId",
|
|
68
|
+
"scheduledMaintenanceId",
|
|
69
|
+
"onCallDutyPolicyId",
|
|
70
|
+
"onCallDutyPolicyScheduleId",
|
|
71
|
+
"incidentEpisodeId",
|
|
72
|
+
"alertEpisodeId",
|
|
73
|
+
"workspaceType",
|
|
74
|
+
"channelId",
|
|
75
|
+
"requestId",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Run `fn` within a fresh telemetry-context scope seeded with `attributes`.
|
|
80
|
+
* Any attributes from an enclosing scope are inherited so nested units of
|
|
81
|
+
* work (e.g. a job spawned while handling a request) keep their context.
|
|
82
|
+
*/
|
|
83
|
+
public static runWithContext<T>(
|
|
84
|
+
attributes: TelemetryContextAttributes,
|
|
85
|
+
fn: () => T,
|
|
86
|
+
): T {
|
|
87
|
+
if (DisableTelemetry) {
|
|
88
|
+
return fn();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const inherited: Record<string, string | number | boolean> =
|
|
92
|
+
this.getAttributes();
|
|
93
|
+
|
|
94
|
+
const store: TelemetryContextStore = {
|
|
95
|
+
attributes: { ...inherited },
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
this.mergeInto(store.attributes, attributes);
|
|
99
|
+
|
|
100
|
+
return this.storage.run(store, fn);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Merge `attributes` into the current scope. No-op when there is no active
|
|
105
|
+
* scope (e.g. code running outside any seeded entry point) or telemetry is
|
|
106
|
+
* disabled, so it is always safe to call.
|
|
107
|
+
*/
|
|
108
|
+
public static setAttributes(attributes: TelemetryContextAttributes): void {
|
|
109
|
+
if (DisableTelemetry) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const store: TelemetryContextStore | undefined = this.storage.getStore();
|
|
114
|
+
|
|
115
|
+
if (!store) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.mergeInto(store.attributes, attributes);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read the attributes for the current scope. Returns an empty object when
|
|
124
|
+
* there is no active scope. The returned object is the live store — treat it
|
|
125
|
+
* as read-only (consumers copy it before mutating).
|
|
126
|
+
*/
|
|
127
|
+
public static getAttributes(): Record<string, string | number | boolean> {
|
|
128
|
+
const store: TelemetryContextStore | undefined = this.storage.getStore();
|
|
129
|
+
|
|
130
|
+
if (!store) {
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return store.attributes;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Best-effort extraction of known tenant/business identifiers from an
|
|
139
|
+
* arbitrary object (e.g. a queue job's `data` payload), so worker jobs can
|
|
140
|
+
* seed context without every job knowing about telemetry. Values are coerced
|
|
141
|
+
* to strings; unknown/empty values are skipped.
|
|
142
|
+
*/
|
|
143
|
+
public static pickKnownAttributes(data: unknown): TelemetryContextAttributes {
|
|
144
|
+
const attributes: TelemetryContextAttributes = {};
|
|
145
|
+
|
|
146
|
+
if (!data || typeof data !== "object") {
|
|
147
|
+
return attributes;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const record: Record<string, unknown> = data as Record<string, unknown>;
|
|
151
|
+
|
|
152
|
+
for (const key of this.KNOWN_ID_KEYS) {
|
|
153
|
+
const value: unknown = record[key];
|
|
154
|
+
|
|
155
|
+
if (value === undefined || value === null) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (
|
|
160
|
+
typeof value === "string" ||
|
|
161
|
+
typeof value === "number" ||
|
|
162
|
+
typeof value === "boolean"
|
|
163
|
+
) {
|
|
164
|
+
attributes[key] = value;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ObjectID-like values expose a meaningful toString().
|
|
169
|
+
const asString: string = String(value);
|
|
170
|
+
if (asString && asString !== "[object Object]") {
|
|
171
|
+
attributes[key] = asString;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return attributes;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private static mergeInto(
|
|
179
|
+
target: Record<string, string | number | boolean>,
|
|
180
|
+
attributes: TelemetryContextAttributes,
|
|
181
|
+
): void {
|
|
182
|
+
for (const key in attributes) {
|
|
183
|
+
const value: TelemetryContextAttributeValue = attributes[key];
|
|
184
|
+
|
|
185
|
+
if (value !== undefined && value !== null) {
|
|
186
|
+
target[key] = value;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|