@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.
Files changed (164) hide show
  1. package/Models/AnalyticsModels/ExceptionInstance.ts +1 -1
  2. package/Models/AnalyticsModels/Log.ts +1 -1
  3. package/Models/AnalyticsModels/Metric.ts +1 -1
  4. package/Models/AnalyticsModels/Profile.ts +1 -1
  5. package/Models/AnalyticsModels/ProfileSample.ts +1 -1
  6. package/Models/AnalyticsModels/Span.ts +1 -1
  7. package/Models/DatabaseModels/TelemetryException.ts +46 -34
  8. package/Models/DatabaseModels/TelemetryUsageBilling.ts +35 -2
  9. package/Server/API/AIAgentDataAPI.ts +25 -7
  10. package/Server/API/TelemetryAPI.ts +6 -0
  11. package/Server/API/TelemetryExceptionAPI.ts +6 -2
  12. package/Server/EnvironmentConfig.ts +27 -0
  13. package/Server/Infrastructure/ClickhouseDatabase.ts +21 -1
  14. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +19 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.ts +28 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.ts +24 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.ts +47 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.ts +34 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  20. package/Server/Infrastructure/PostgresDatabase.ts +27 -1
  21. package/Server/Infrastructure/QueueWorker.ts +54 -4
  22. package/Server/Infrastructure/Redis.ts +11 -0
  23. package/Server/Services/AnalyticsDatabaseService.ts +87 -0
  24. package/Server/Services/DatabaseService.ts +73 -0
  25. package/Server/Services/TelemetryAttributeService.ts +38 -2
  26. package/Server/Services/TelemetryExceptionService.ts +24 -49
  27. package/Server/Services/TelemetryUsageBillingService.ts +289 -166
  28. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +102 -72
  29. package/Server/Types/Database/Permissions/OwnedScopePermission.ts +81 -60
  30. package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +67 -0
  31. package/Server/Utils/Express.ts +32 -0
  32. package/Server/Utils/GracefulShutdown.ts +194 -0
  33. package/Server/Utils/Logger.ts +12 -1
  34. package/Server/Utils/Monitor/MonitorLogUtil.ts +22 -17
  35. package/Server/Utils/Profiling.ts +14 -6
  36. package/Server/Utils/StartServer.ts +13 -5
  37. package/Server/Utils/Telemetry/ContextSpanProcessor.ts +48 -0
  38. package/Server/Utils/Telemetry/LogExceptionExtractor.ts +289 -0
  39. package/Server/Utils/Telemetry/SpanUtil.ts +16 -35
  40. package/Server/Utils/Telemetry/StackTraceParser.ts +423 -0
  41. package/Server/Utils/Telemetry/TelemetryContext.ts +190 -0
  42. package/Server/Utils/Telemetry.ts +33 -7
  43. package/Tests/Server/Services/TelemetryAttributeService.test.ts +83 -0
  44. package/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.ts +0 -0
  45. package/Types/Database/AccessControl/OwnedThrough.ts +31 -3
  46. package/Types/Telemetry/ServiceType.ts +10 -0
  47. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +7 -1
  48. package/UI/Components/Dictionary/Dictionary.tsx +19 -0
  49. package/UI/Components/Filters/FiltersForm.tsx +1 -0
  50. package/UI/Components/Filters/JSONFilter.tsx +2 -0
  51. package/UI/Components/Filters/Types/Filter.ts +1 -0
  52. package/UI/Components/LogsViewer/LogsViewer.tsx +16 -0
  53. package/UI/Utils/Project.ts +6 -0
  54. package/UI/Utils/Telemetry/Telemetry.ts +65 -0
  55. package/UI/Utils/TelemetryService.ts +150 -0
  56. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -1
  57. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  58. package/build/dist/Models/AnalyticsModels/Log.js +1 -1
  59. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  60. package/build/dist/Models/AnalyticsModels/Metric.js +1 -1
  61. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  62. package/build/dist/Models/AnalyticsModels/Profile.js +1 -1
  63. package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
  64. package/build/dist/Models/AnalyticsModels/ProfileSample.js +1 -1
  65. package/build/dist/Models/AnalyticsModels/ProfileSample.js.map +1 -1
  66. package/build/dist/Models/AnalyticsModels/Span.js +1 -1
  67. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  68. package/build/dist/Models/DatabaseModels/TelemetryException.js +47 -33
  69. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  70. package/build/dist/Models/DatabaseModels/TelemetryUsageBilling.js +36 -2
  71. package/build/dist/Models/DatabaseModels/TelemetryUsageBilling.js.map +1 -1
  72. package/build/dist/Server/API/AIAgentDataAPI.js +24 -8
  73. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  74. package/build/dist/Server/API/TelemetryAPI.js +4 -0
  75. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  76. package/build/dist/Server/API/TelemetryExceptionAPI.js +6 -2
  77. package/build/dist/Server/API/TelemetryExceptionAPI.js.map +1 -1
  78. package/build/dist/Server/EnvironmentConfig.js +19 -0
  79. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  80. package/build/dist/Server/Infrastructure/ClickhouseDatabase.js +16 -2
  81. package/build/dist/Server/Infrastructure/ClickhouseDatabase.js.map +1 -1
  82. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +10 -9
  83. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  84. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.js +23 -0
  85. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.js.map +1 -0
  86. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.js +19 -0
  87. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.js.map +1 -0
  88. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.js +22 -0
  89. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.js.map +1 -0
  90. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.js +25 -0
  91. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.js.map +1 -0
  92. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  93. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  94. package/build/dist/Server/Infrastructure/PostgresDatabase.js +20 -1
  95. package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
  96. package/build/dist/Server/Infrastructure/QueueWorker.js +40 -3
  97. package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
  98. package/build/dist/Server/Infrastructure/Redis.js +5 -0
  99. package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
  100. package/build/dist/Server/Services/AnalyticsDatabaseService.js +59 -0
  101. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  102. package/build/dist/Server/Services/DatabaseService.js +62 -0
  103. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  104. package/build/dist/Server/Services/TelemetryAttributeService.js +23 -1
  105. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  106. package/build/dist/Server/Services/TelemetryExceptionService.js +16 -41
  107. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  108. package/build/dist/Server/Services/TelemetryUsageBillingService.js +211 -147
  109. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  110. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +84 -63
  111. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  112. package/build/dist/Server/Types/Database/Permissions/OwnedScopePermission.js +67 -49
  113. package/build/dist/Server/Types/Database/Permissions/OwnedScopePermission.js.map +1 -1
  114. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +51 -0
  115. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
  116. package/build/dist/Server/Utils/Express.js +23 -0
  117. package/build/dist/Server/Utils/Express.js.map +1 -1
  118. package/build/dist/Server/Utils/GracefulShutdown.js +145 -0
  119. package/build/dist/Server/Utils/GracefulShutdown.js.map +1 -0
  120. package/build/dist/Server/Utils/Logger.js +8 -1
  121. package/build/dist/Server/Utils/Logger.js.map +1 -1
  122. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -10
  123. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
  124. package/build/dist/Server/Utils/Profiling.js +8 -3
  125. package/build/dist/Server/Utils/Profiling.js.map +1 -1
  126. package/build/dist/Server/Utils/StartServer.js +12 -4
  127. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  128. package/build/dist/Server/Utils/Telemetry/ContextSpanProcessor.js +37 -0
  129. package/build/dist/Server/Utils/Telemetry/ContextSpanProcessor.js.map +1 -0
  130. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +214 -0
  131. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -0
  132. package/build/dist/Server/Utils/Telemetry/SpanUtil.js +15 -24
  133. package/build/dist/Server/Utils/Telemetry/SpanUtil.js.map +1 -1
  134. package/build/dist/Server/Utils/Telemetry/StackTraceParser.js +365 -0
  135. package/build/dist/Server/Utils/Telemetry/StackTraceParser.js.map +1 -0
  136. package/build/dist/Server/Utils/Telemetry/TelemetryContext.js +124 -0
  137. package/build/dist/Server/Utils/Telemetry/TelemetryContext.js.map +1 -0
  138. package/build/dist/Server/Utils/Telemetry.js +22 -5
  139. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  140. package/build/dist/Tests/Server/Services/TelemetryAttributeService.test.js +50 -0
  141. package/build/dist/Tests/Server/Services/TelemetryAttributeService.test.js.map +1 -0
  142. package/build/dist/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.js +0 -0
  143. package/build/dist/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.js.map +1 -0
  144. package/build/dist/Types/Database/AccessControl/OwnedThrough.js +7 -2
  145. package/build/dist/Types/Database/AccessControl/OwnedThrough.js.map +1 -1
  146. package/build/dist/Types/Telemetry/ServiceType.js +10 -0
  147. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  148. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +7 -1
  149. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  150. package/build/dist/UI/Components/Dictionary/Dictionary.js +10 -0
  151. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  152. package/build/dist/UI/Components/Filters/FiltersForm.js +1 -1
  153. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  154. package/build/dist/UI/Components/Filters/JSONFilter.js +1 -1
  155. package/build/dist/UI/Components/Filters/JSONFilter.js.map +1 -1
  156. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +15 -0
  157. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  158. package/build/dist/UI/Utils/Project.js +5 -0
  159. package/build/dist/UI/Utils/Project.js.map +1 -1
  160. package/build/dist/UI/Utils/Telemetry/Telemetry.js +44 -0
  161. package/build/dist/UI/Utils/Telemetry/Telemetry.js.map +1 -1
  162. package/build/dist/UI/Utils/TelemetryService.js +113 -0
  163. package/build/dist/UI/Utils/TelemetryService.js.map +1 -0
  164. 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 currently active span.
25
- * Safe to call even when there is no active span or telemetry is disabled.
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
+ }