@oneuptime/common 10.0.19 → 10.0.21
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/Server/API/GitHubAPI.ts +104 -12
- package/Server/API/TelemetryAPI.ts +208 -0
- package/Server/API/UserCallAPI.ts +29 -0
- package/Server/API/UserEmailAPI.ts +29 -0
- package/Server/API/UserSmsAPI.ts +29 -0
- package/Server/API/UserWhatsAppAPI.ts +29 -0
- package/Server/Services/LogAggregationService.ts +251 -0
- package/Server/Utils/VM/VMRunner.ts +45 -0
- package/Types/Log/LogQueryParser.ts +252 -0
- package/Types/Log/LogQueryToFilter.ts +131 -0
- package/UI/Components/CopyTextButton/CopyTextButton.tsx +3 -3
- package/UI/Components/LogsViewer/LogsViewer.tsx +166 -93
- package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +58 -0
- package/UI/Components/LogsViewer/components/FacetSection.tsx +119 -0
- package/UI/Components/LogsViewer/components/FacetValueRow.tsx +102 -0
- package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +122 -0
- package/UI/Components/LogsViewer/components/LiveLogsToggle.tsx +4 -4
- package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +22 -26
- package/UI/Components/LogsViewer/components/LogSearchBar.tsx +360 -0
- package/UI/Components/LogsViewer/components/LogSearchHelp.tsx +128 -0
- package/UI/Components/LogsViewer/components/LogSearchSuggestions.tsx +64 -0
- package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +199 -0
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +172 -0
- package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +27 -57
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +268 -0
- package/UI/Components/LogsViewer/components/LogsPagination.tsx +12 -10
- package/UI/Components/LogsViewer/components/LogsTable.tsx +33 -32
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +16 -18
- package/UI/Components/LogsViewer/components/severityColors.ts +31 -0
- package/UI/Components/LogsViewer/components/severityTheme.ts +25 -25
- package/UI/Components/LogsViewer/types.ts +20 -0
- package/build/dist/Server/API/GitHubAPI.js +40 -9
- package/build/dist/Server/API/GitHubAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +136 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/API/UserCallAPI.js +17 -0
- package/build/dist/Server/API/UserCallAPI.js.map +1 -1
- package/build/dist/Server/API/UserEmailAPI.js +17 -0
- package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
- package/build/dist/Server/API/UserSmsAPI.js +17 -0
- package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
- package/build/dist/Server/API/UserWhatsAppAPI.js +17 -0
- package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +163 -0
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -0
- package/build/dist/Server/Utils/VM/VMRunner.js +31 -0
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Types/Log/LogQueryParser.js +200 -0
- package/build/dist/Types/Log/LogQueryParser.js.map +1 -0
- package/build/dist/Types/Log/LogQueryToFilter.js +96 -0
- package/build/dist/Types/Log/LogQueryToFilter.js.map +1 -0
- package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js +3 -3
- package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +64 -42
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +24 -0
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/FacetSection.js +46 -0
- package/build/dist/UI/Components/LogsViewer/components/FacetSection.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +35 -0
- package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +64 -0
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LiveLogsToggle.js +4 -4
- package/build/dist/UI/Components/LogsViewer/components/LiveLogsToggle.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +19 -21
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +230 -0
- package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogSearchHelp.js +84 -0
- package/build/dist/UI/Components/LogsViewer/components/LogSearchHelp.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js +27 -0
- package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +100 -0
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +104 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +14 -35
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +127 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +9 -9
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +31 -30
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +7 -8
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/severityColors.js +22 -0
- package/build/dist/UI/Components/LogsViewer/components/severityColors.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/severityTheme.js +25 -25
- package/build/dist/UI/Components/LogsViewer/components/severityTheme.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { SQL, Statement } from "../Utils/AnalyticsDatabase/Statement";
|
|
2
|
+
import LogDatabaseService from "./LogService";
|
|
3
|
+
import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
|
|
4
|
+
import { JSONObject } from "../../Types/JSON";
|
|
5
|
+
import ObjectID from "../../Types/ObjectID";
|
|
6
|
+
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
7
|
+
import { DbJSONResponse, Results } from "./AnalyticsDatabaseService";
|
|
8
|
+
|
|
9
|
+
export interface HistogramBucket {
|
|
10
|
+
time: string;
|
|
11
|
+
severity: string;
|
|
12
|
+
count: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface HistogramRequest {
|
|
16
|
+
projectId: ObjectID;
|
|
17
|
+
startTime: Date;
|
|
18
|
+
endTime: Date;
|
|
19
|
+
bucketSizeInMinutes: number;
|
|
20
|
+
serviceIds?: Array<ObjectID> | undefined;
|
|
21
|
+
severityTexts?: Array<string> | undefined;
|
|
22
|
+
bodySearchText?: string | undefined;
|
|
23
|
+
traceIds?: Array<string> | undefined;
|
|
24
|
+
spanIds?: Array<string> | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface FacetValue {
|
|
28
|
+
value: string;
|
|
29
|
+
count: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface FacetRequest {
|
|
33
|
+
projectId: ObjectID;
|
|
34
|
+
startTime: Date;
|
|
35
|
+
endTime: Date;
|
|
36
|
+
facetKey: string;
|
|
37
|
+
limit?: number | undefined;
|
|
38
|
+
serviceIds?: Array<ObjectID> | undefined;
|
|
39
|
+
severityTexts?: Array<string> | undefined;
|
|
40
|
+
bodySearchText?: string | undefined;
|
|
41
|
+
traceIds?: Array<string> | undefined;
|
|
42
|
+
spanIds?: Array<string> | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class LogAggregationService {
|
|
46
|
+
private static readonly DEFAULT_FACET_LIMIT: number = 10;
|
|
47
|
+
private static readonly TABLE_NAME: string = "LogItem";
|
|
48
|
+
|
|
49
|
+
@CaptureSpan()
|
|
50
|
+
public static async getHistogram(
|
|
51
|
+
request: HistogramRequest,
|
|
52
|
+
): Promise<Array<HistogramBucket>> {
|
|
53
|
+
const statement: Statement =
|
|
54
|
+
LogAggregationService.buildHistogramStatement(request);
|
|
55
|
+
|
|
56
|
+
const dbResult: Results = await LogDatabaseService.executeQuery(statement);
|
|
57
|
+
const response: DbJSONResponse = await dbResult.json<{
|
|
58
|
+
data?: Array<JSONObject>;
|
|
59
|
+
}>();
|
|
60
|
+
|
|
61
|
+
const rows: Array<JSONObject> = response.data || [];
|
|
62
|
+
|
|
63
|
+
return rows.map((row: JSONObject): HistogramBucket => {
|
|
64
|
+
return {
|
|
65
|
+
time: String(row["bucket"] || ""),
|
|
66
|
+
severity: String(row["severityText"] || "Unspecified"),
|
|
67
|
+
count: Number(row["cnt"] || 0),
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@CaptureSpan()
|
|
73
|
+
public static async getFacetValues(
|
|
74
|
+
request: FacetRequest,
|
|
75
|
+
): Promise<Array<FacetValue>> {
|
|
76
|
+
const statement: Statement =
|
|
77
|
+
LogAggregationService.buildFacetStatement(request);
|
|
78
|
+
|
|
79
|
+
const dbResult: Results = await LogDatabaseService.executeQuery(statement);
|
|
80
|
+
const response: DbJSONResponse = await dbResult.json<{
|
|
81
|
+
data?: Array<JSONObject>;
|
|
82
|
+
}>();
|
|
83
|
+
|
|
84
|
+
const rows: Array<JSONObject> = response.data || [];
|
|
85
|
+
|
|
86
|
+
return rows
|
|
87
|
+
.map((row: JSONObject): FacetValue => {
|
|
88
|
+
return {
|
|
89
|
+
value: String(row["val"] || ""),
|
|
90
|
+
count: Number(row["cnt"] || 0),
|
|
91
|
+
};
|
|
92
|
+
})
|
|
93
|
+
.filter((facet: FacetValue): boolean => {
|
|
94
|
+
return facet.value.length > 0;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private static buildHistogramStatement(request: HistogramRequest): Statement {
|
|
99
|
+
const intervalSeconds: number = request.bucketSizeInMinutes * 60;
|
|
100
|
+
|
|
101
|
+
const statement: Statement = SQL`
|
|
102
|
+
SELECT
|
|
103
|
+
toStartOfInterval(time, INTERVAL ${{
|
|
104
|
+
type: TableColumnType.Number,
|
|
105
|
+
value: intervalSeconds,
|
|
106
|
+
}} SECOND) AS bucket,
|
|
107
|
+
severityText,
|
|
108
|
+
count() AS cnt
|
|
109
|
+
FROM ${LogAggregationService.TABLE_NAME}
|
|
110
|
+
WHERE projectId = ${{
|
|
111
|
+
type: TableColumnType.ObjectID,
|
|
112
|
+
value: request.projectId,
|
|
113
|
+
}}
|
|
114
|
+
AND time >= ${{
|
|
115
|
+
type: TableColumnType.Date,
|
|
116
|
+
value: request.startTime,
|
|
117
|
+
}}
|
|
118
|
+
AND time <= ${{
|
|
119
|
+
type: TableColumnType.Date,
|
|
120
|
+
value: request.endTime,
|
|
121
|
+
}}
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
LogAggregationService.appendCommonFilters(statement, request);
|
|
125
|
+
|
|
126
|
+
statement.append(" GROUP BY bucket, severityText ORDER BY bucket ASC");
|
|
127
|
+
|
|
128
|
+
return statement;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private static buildFacetStatement(request: FacetRequest): Statement {
|
|
132
|
+
const limit: number =
|
|
133
|
+
request.limit ?? LogAggregationService.DEFAULT_FACET_LIMIT;
|
|
134
|
+
|
|
135
|
+
const isTopLevelColumn: boolean = LogAggregationService.isTopLevelColumn(
|
|
136
|
+
request.facetKey,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const escapedKey: string = LogAggregationService.escapeSingleQuotes(
|
|
140
|
+
request.facetKey,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const valueExpression: string = isTopLevelColumn
|
|
144
|
+
? `toString(${escapedKey})`
|
|
145
|
+
: `JSONExtractRaw(attributes, '${escapedKey}')`;
|
|
146
|
+
|
|
147
|
+
// Build with raw SQL for the expression part, then parameterize WHERE values
|
|
148
|
+
const statement: Statement = new Statement();
|
|
149
|
+
|
|
150
|
+
statement.append(
|
|
151
|
+
`SELECT ${valueExpression} AS val, count() AS cnt FROM ${LogAggregationService.TABLE_NAME}`,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
statement.append(
|
|
155
|
+
SQL` WHERE projectId = ${{
|
|
156
|
+
type: TableColumnType.ObjectID,
|
|
157
|
+
value: request.projectId,
|
|
158
|
+
}} AND time >= ${{
|
|
159
|
+
type: TableColumnType.Date,
|
|
160
|
+
value: request.startTime,
|
|
161
|
+
}} AND time <= ${{
|
|
162
|
+
type: TableColumnType.Date,
|
|
163
|
+
value: request.endTime,
|
|
164
|
+
}}`,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (!isTopLevelColumn) {
|
|
168
|
+
statement.append(` AND JSONHas(attributes, '${escapedKey}') = 1`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
LogAggregationService.appendCommonFilters(statement, request);
|
|
172
|
+
|
|
173
|
+
statement.append(
|
|
174
|
+
SQL` GROUP BY val ORDER BY cnt DESC LIMIT ${{
|
|
175
|
+
type: TableColumnType.Number,
|
|
176
|
+
value: limit,
|
|
177
|
+
}}`,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return statement;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private static appendCommonFilters(
|
|
184
|
+
statement: Statement,
|
|
185
|
+
request: Pick<
|
|
186
|
+
HistogramRequest,
|
|
187
|
+
"serviceIds" | "severityTexts" | "bodySearchText" | "traceIds" | "spanIds"
|
|
188
|
+
>,
|
|
189
|
+
): void {
|
|
190
|
+
if (request.serviceIds && request.serviceIds.length > 0) {
|
|
191
|
+
const idStrings: Array<string> = request.serviceIds.map(
|
|
192
|
+
(id: ObjectID): string => {
|
|
193
|
+
return `'${id.toString()}'`;
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
statement.append(` AND serviceId IN (${idStrings.join(",")})`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (request.severityTexts && request.severityTexts.length > 0) {
|
|
200
|
+
const sevStrings: Array<string> = request.severityTexts.map(
|
|
201
|
+
(s: string): string => {
|
|
202
|
+
return `'${LogAggregationService.escapeSingleQuotes(s)}'`;
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
statement.append(` AND severityText IN (${sevStrings.join(",")})`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (request.traceIds && request.traceIds.length > 0) {
|
|
209
|
+
const traceStrings: Array<string> = request.traceIds.map(
|
|
210
|
+
(s: string): string => {
|
|
211
|
+
return `'${LogAggregationService.escapeSingleQuotes(s)}'`;
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
statement.append(` AND traceId IN (${traceStrings.join(",")})`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (request.spanIds && request.spanIds.length > 0) {
|
|
218
|
+
const spanStrings: Array<string> = request.spanIds.map(
|
|
219
|
+
(s: string): string => {
|
|
220
|
+
return `'${LogAggregationService.escapeSingleQuotes(s)}'`;
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
statement.append(` AND spanId IN (${spanStrings.join(",")})`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (request.bodySearchText && request.bodySearchText.trim().length > 0) {
|
|
227
|
+
statement.append(
|
|
228
|
+
` AND body ILIKE ${{
|
|
229
|
+
type: TableColumnType.Text,
|
|
230
|
+
value: `%${request.bodySearchText.trim()}%`,
|
|
231
|
+
}}`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private static isTopLevelColumn(key: string): boolean {
|
|
237
|
+
const topLevelColumns: Set<string> = new Set([
|
|
238
|
+
"severityText",
|
|
239
|
+
"serviceId",
|
|
240
|
+
"traceId",
|
|
241
|
+
"spanId",
|
|
242
|
+
]);
|
|
243
|
+
return topLevelColumns.has(key);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private static escapeSingleQuotes(value: string): string {
|
|
247
|
+
return value.replace(/'/g, "\\'");
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export default LogAggregationService;
|
|
@@ -22,6 +22,16 @@ const BLOCKED_SANDBOX_PROPERTIES: ReadonlySet<string> = new Set([
|
|
|
22
22
|
"__proto__",
|
|
23
23
|
"prototype",
|
|
24
24
|
"mainModule",
|
|
25
|
+
/*
|
|
26
|
+
* Block Playwright methods that can spawn processes or access internals.
|
|
27
|
+
* Prevents RCE via browser.browserType().launch({executablePath:"/bin/sh"})
|
|
28
|
+
* and traversal via page.context().browser().browserType().launch(...)
|
|
29
|
+
*/
|
|
30
|
+
"browserType", // Browser → BrowserType (which has launch/connect)
|
|
31
|
+
"launch", // BrowserType.launch() spawns a child process
|
|
32
|
+
"launchPersistentContext", // BrowserType.launchPersistentContext() spawns a child process
|
|
33
|
+
"connectOverCDP", // BrowserType.connectOverCDP() connects via Chrome DevTools Protocol
|
|
34
|
+
"newCDPSession", // BrowserContext/Page.newCDPSession() opens raw CDP sessions
|
|
25
35
|
]);
|
|
26
36
|
|
|
27
37
|
/**
|
|
@@ -123,6 +133,27 @@ function createSandboxProxy(
|
|
|
123
133
|
);
|
|
124
134
|
});
|
|
125
135
|
},
|
|
136
|
+
getOwnPropertyDescriptor(
|
|
137
|
+
fnTarget: (...args: unknown[]) => unknown,
|
|
138
|
+
prop: string | symbol,
|
|
139
|
+
): PropertyDescriptor | undefined {
|
|
140
|
+
if (
|
|
141
|
+
typeof prop === "string" &&
|
|
142
|
+
BLOCKED_SANDBOX_PROPERTIES.has(prop)
|
|
143
|
+
) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const desc: PropertyDescriptor | undefined =
|
|
147
|
+
Reflect.getOwnPropertyDescriptor(fnTarget, prop);
|
|
148
|
+
if (desc && "value" in desc) {
|
|
149
|
+
desc.value = createSandboxProxy(
|
|
150
|
+
desc.value,
|
|
151
|
+
cache,
|
|
152
|
+
fnTarget as GenericObject,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return desc;
|
|
156
|
+
},
|
|
126
157
|
},
|
|
127
158
|
);
|
|
128
159
|
return fnProxy;
|
|
@@ -165,6 +196,20 @@ function createSandboxProxy(
|
|
|
165
196
|
return !(typeof k === "string" && BLOCKED_SANDBOX_PROPERTIES.has(k));
|
|
166
197
|
});
|
|
167
198
|
},
|
|
199
|
+
getOwnPropertyDescriptor(
|
|
200
|
+
objTarget: GenericObject,
|
|
201
|
+
prop: string | symbol,
|
|
202
|
+
): PropertyDescriptor | undefined {
|
|
203
|
+
if (typeof prop === "string" && BLOCKED_SANDBOX_PROPERTIES.has(prop)) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
const desc: PropertyDescriptor | undefined =
|
|
207
|
+
Reflect.getOwnPropertyDescriptor(objTarget, prop);
|
|
208
|
+
if (desc && "value" in desc) {
|
|
209
|
+
desc.value = createSandboxProxy(desc.value, cache, objTarget);
|
|
210
|
+
}
|
|
211
|
+
return desc;
|
|
212
|
+
},
|
|
168
213
|
});
|
|
169
214
|
|
|
170
215
|
cache.set(target, objProxy);
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LogQueryParser
|
|
3
|
+
*
|
|
4
|
+
* Parses a Datadog-style log search query string into structured filter tokens.
|
|
5
|
+
*
|
|
6
|
+
* Supported syntax:
|
|
7
|
+
* - Free text: `connection refused` → body ILIKE '%connection refused%'
|
|
8
|
+
* - Quoted phrase: `"connection refused"` → body ILIKE '%connection refused%'
|
|
9
|
+
* - Field-specific: `severity:error` → severityText = 'error'
|
|
10
|
+
* - Attribute access: `@http.status_code:500` → attributes.http.status_code = '500'
|
|
11
|
+
* - Negation (prefix): `-severity:debug` → severityText != 'debug'
|
|
12
|
+
* - Wildcard: `service:api-*` → serviceId ILIKE 'api-%'
|
|
13
|
+
* - Numeric range: `@duration:>1000` → attributes.duration > 1000
|
|
14
|
+
* - Boolean: `severity:error AND service:api` (AND is default between tokens)
|
|
15
|
+
*
|
|
16
|
+
* Produces an array of ParsedToken objects consumed by the search bar and query builder.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export enum TokenType {
|
|
20
|
+
FreeText = "FreeText",
|
|
21
|
+
FieldFilter = "FieldFilter",
|
|
22
|
+
AttributeFilter = "AttributeFilter",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export enum FilterOperator {
|
|
26
|
+
Equals = "Equals",
|
|
27
|
+
NotEquals = "NotEquals",
|
|
28
|
+
Contains = "Contains",
|
|
29
|
+
NotContains = "NotContains",
|
|
30
|
+
GreaterThan = "GreaterThan",
|
|
31
|
+
GreaterThanOrEqual = "GreaterThanOrEqual",
|
|
32
|
+
LessThan = "LessThan",
|
|
33
|
+
LessThanOrEqual = "LessThanOrEqual",
|
|
34
|
+
Wildcard = "Wildcard",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ParsedToken {
|
|
38
|
+
type: TokenType;
|
|
39
|
+
field?: string;
|
|
40
|
+
operator: FilterOperator;
|
|
41
|
+
value: string;
|
|
42
|
+
negated: boolean;
|
|
43
|
+
raw: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const FIELD_ALIASES: Record<string, string> = {
|
|
47
|
+
severity: "severityText",
|
|
48
|
+
level: "severityText",
|
|
49
|
+
service: "serviceId",
|
|
50
|
+
trace: "traceId",
|
|
51
|
+
span: "spanId",
|
|
52
|
+
message: "body",
|
|
53
|
+
msg: "body",
|
|
54
|
+
log: "body",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const BOOLEAN_KEYWORDS: Set<string> = new Set(["AND", "OR", "NOT"]);
|
|
58
|
+
|
|
59
|
+
function resolveFieldName(raw: string): string {
|
|
60
|
+
const lower: string = raw.toLowerCase();
|
|
61
|
+
return FIELD_ALIASES[lower] || raw;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function detectOperator(value: string): {
|
|
65
|
+
operator: FilterOperator;
|
|
66
|
+
cleanValue: string;
|
|
67
|
+
} {
|
|
68
|
+
if (value.startsWith(">=")) {
|
|
69
|
+
return {
|
|
70
|
+
operator: FilterOperator.GreaterThanOrEqual,
|
|
71
|
+
cleanValue: value.slice(2),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (value.startsWith("<=")) {
|
|
76
|
+
return {
|
|
77
|
+
operator: FilterOperator.LessThanOrEqual,
|
|
78
|
+
cleanValue: value.slice(2),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (value.startsWith(">")) {
|
|
83
|
+
return {
|
|
84
|
+
operator: FilterOperator.GreaterThan,
|
|
85
|
+
cleanValue: value.slice(1),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (value.startsWith("<")) {
|
|
90
|
+
return {
|
|
91
|
+
operator: FilterOperator.LessThan,
|
|
92
|
+
cleanValue: value.slice(1),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (value.includes("*")) {
|
|
97
|
+
return {
|
|
98
|
+
operator: FilterOperator.Wildcard,
|
|
99
|
+
cleanValue: value,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
operator: FilterOperator.Equals,
|
|
105
|
+
cleanValue: value,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function tokenizeRawInput(input: string): Array<string> {
|
|
110
|
+
const tokens: Array<string> = [];
|
|
111
|
+
let current: string = "";
|
|
112
|
+
let inQuotes: boolean = false;
|
|
113
|
+
|
|
114
|
+
for (let i: number = 0; i < input.length; i++) {
|
|
115
|
+
const char: string = input[i]!;
|
|
116
|
+
|
|
117
|
+
if (char === '"') {
|
|
118
|
+
inQuotes = !inQuotes;
|
|
119
|
+
current += char;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (char === " " && !inQuotes) {
|
|
124
|
+
if (current.length > 0) {
|
|
125
|
+
tokens.push(current);
|
|
126
|
+
current = "";
|
|
127
|
+
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
current += char;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (current.length > 0) {
|
|
135
|
+
tokens.push(current);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return tokens;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function stripQuotes(value: string): string {
|
|
142
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
143
|
+
return value.slice(1, -1);
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parseFieldToken(raw: string): ParsedToken {
|
|
149
|
+
let workingRaw: string = raw;
|
|
150
|
+
let negated: boolean = false;
|
|
151
|
+
|
|
152
|
+
if (workingRaw.startsWith("-")) {
|
|
153
|
+
negated = true;
|
|
154
|
+
workingRaw = workingRaw.slice(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const isAttribute: boolean = workingRaw.startsWith("@");
|
|
158
|
+
|
|
159
|
+
if (isAttribute) {
|
|
160
|
+
workingRaw = workingRaw.slice(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const colonIndex: number = workingRaw.indexOf(":");
|
|
164
|
+
|
|
165
|
+
if (colonIndex === -1) {
|
|
166
|
+
return {
|
|
167
|
+
type: TokenType.FreeText,
|
|
168
|
+
operator: FilterOperator.Contains,
|
|
169
|
+
value: stripQuotes(workingRaw),
|
|
170
|
+
negated,
|
|
171
|
+
raw,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const rawField: string = workingRaw.slice(0, colonIndex);
|
|
176
|
+
const rawValue: string = stripQuotes(workingRaw.slice(colonIndex + 1));
|
|
177
|
+
|
|
178
|
+
const { operator, cleanValue } = detectOperator(rawValue);
|
|
179
|
+
|
|
180
|
+
const field: string = isAttribute ? rawField : resolveFieldName(rawField);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
type: isAttribute ? TokenType.AttributeFilter : TokenType.FieldFilter,
|
|
184
|
+
field,
|
|
185
|
+
operator:
|
|
186
|
+
negated && operator === FilterOperator.Equals
|
|
187
|
+
? FilterOperator.NotEquals
|
|
188
|
+
: operator,
|
|
189
|
+
value: cleanValue,
|
|
190
|
+
negated,
|
|
191
|
+
raw,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function parseLogQuery(query: string): Array<ParsedToken> {
|
|
196
|
+
const trimmed: string = query.trim();
|
|
197
|
+
|
|
198
|
+
if (trimmed.length === 0) {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const rawTokens: Array<string> = tokenizeRawInput(trimmed);
|
|
203
|
+
const tokens: Array<ParsedToken> = [];
|
|
204
|
+
const freeTextParts: Array<string> = [];
|
|
205
|
+
|
|
206
|
+
const flushFreeText: () => void = (): void => {
|
|
207
|
+
if (freeTextParts.length > 0) {
|
|
208
|
+
const combined: string = freeTextParts.join(" ");
|
|
209
|
+
tokens.push({
|
|
210
|
+
type: TokenType.FreeText,
|
|
211
|
+
operator: FilterOperator.Contains,
|
|
212
|
+
value: combined,
|
|
213
|
+
negated: false,
|
|
214
|
+
raw: combined,
|
|
215
|
+
});
|
|
216
|
+
freeTextParts.length = 0;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
for (const rawToken of rawTokens) {
|
|
221
|
+
if (BOOLEAN_KEYWORDS.has(rawToken)) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const hasColon: boolean =
|
|
226
|
+
rawToken.includes(":") && !rawToken.startsWith('"');
|
|
227
|
+
|
|
228
|
+
const isNegatedField: boolean =
|
|
229
|
+
rawToken.startsWith("-") && rawToken.slice(1).includes(":");
|
|
230
|
+
|
|
231
|
+
if (hasColon || isNegatedField) {
|
|
232
|
+
flushFreeText();
|
|
233
|
+
tokens.push(parseFieldToken(rawToken));
|
|
234
|
+
} else {
|
|
235
|
+
freeTextParts.push(stripQuotes(rawToken));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
flushFreeText();
|
|
240
|
+
|
|
241
|
+
return tokens;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function tokensToDisplayString(tokens: Array<ParsedToken>): string {
|
|
245
|
+
return tokens
|
|
246
|
+
.map((t: ParsedToken) => {
|
|
247
|
+
return t.raw;
|
|
248
|
+
})
|
|
249
|
+
.join(" ");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export default parseLogQuery;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts parsed log query tokens into a Query<Log> object compatible with
|
|
3
|
+
* the AnalyticsDatabaseService query system.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ParsedToken,
|
|
8
|
+
TokenType,
|
|
9
|
+
FilterOperator,
|
|
10
|
+
parseLogQuery,
|
|
11
|
+
} from "./LogQueryParser";
|
|
12
|
+
import Search from "../BaseDatabase/Search";
|
|
13
|
+
import GreaterThan from "../BaseDatabase/GreaterThan";
|
|
14
|
+
import GreaterThanOrEqual from "../BaseDatabase/GreaterThanOrEqual";
|
|
15
|
+
import LessThan from "../BaseDatabase/LessThan";
|
|
16
|
+
import LessThanOrEqual from "../BaseDatabase/LessThanOrEqual";
|
|
17
|
+
|
|
18
|
+
export interface LogFilter {
|
|
19
|
+
body?: string | Search<string>;
|
|
20
|
+
severityText?: string;
|
|
21
|
+
serviceId?: string;
|
|
22
|
+
traceId?: string;
|
|
23
|
+
spanId?: string;
|
|
24
|
+
attributes?: Record<string, unknown>;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const TOP_LEVEL_FIELDS: Set<string> = new Set([
|
|
29
|
+
"severityText",
|
|
30
|
+
"serviceId",
|
|
31
|
+
"traceId",
|
|
32
|
+
"spanId",
|
|
33
|
+
"body",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
function applyFieldFilter(filter: LogFilter, token: ParsedToken): void {
|
|
37
|
+
if (!token.field) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const value: string = token.value;
|
|
42
|
+
|
|
43
|
+
if (TOP_LEVEL_FIELDS.has(token.field)) {
|
|
44
|
+
applyTopLevelFilter(filter, token.field, value, token.operator);
|
|
45
|
+
} else {
|
|
46
|
+
applyAttributeFilter(filter, token.field, value, token.operator);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function applyTopLevelFilter(
|
|
51
|
+
filter: LogFilter,
|
|
52
|
+
field: string,
|
|
53
|
+
value: string,
|
|
54
|
+
operator: FilterOperator,
|
|
55
|
+
): void {
|
|
56
|
+
switch (operator) {
|
|
57
|
+
case FilterOperator.Contains:
|
|
58
|
+
case FilterOperator.Wildcard:
|
|
59
|
+
filter[field] = new Search(value.replace(/\*/g, ""));
|
|
60
|
+
break;
|
|
61
|
+
case FilterOperator.GreaterThan:
|
|
62
|
+
filter[field] = new GreaterThan(parseNumericOrString(value));
|
|
63
|
+
break;
|
|
64
|
+
case FilterOperator.GreaterThanOrEqual:
|
|
65
|
+
filter[field] = new GreaterThanOrEqual(parseNumericOrString(value));
|
|
66
|
+
break;
|
|
67
|
+
case FilterOperator.LessThan:
|
|
68
|
+
filter[field] = new LessThan(parseNumericOrString(value));
|
|
69
|
+
break;
|
|
70
|
+
case FilterOperator.LessThanOrEqual:
|
|
71
|
+
filter[field] = new LessThanOrEqual(parseNumericOrString(value));
|
|
72
|
+
break;
|
|
73
|
+
case FilterOperator.Equals:
|
|
74
|
+
case FilterOperator.NotEquals:
|
|
75
|
+
default:
|
|
76
|
+
filter[field] = value;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function applyAttributeFilter(
|
|
82
|
+
filter: LogFilter,
|
|
83
|
+
field: string,
|
|
84
|
+
value: string,
|
|
85
|
+
_operator: FilterOperator,
|
|
86
|
+
): void {
|
|
87
|
+
if (!filter.attributes) {
|
|
88
|
+
filter.attributes = {};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
filter.attributes[field] = value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function applyFreeTextFilter(filter: LogFilter, token: ParsedToken): void {
|
|
95
|
+
if (filter.body && filter.body instanceof Search) {
|
|
96
|
+
const existing: string = filter.body.toString();
|
|
97
|
+
filter.body = new Search(`${existing} ${token.value}`);
|
|
98
|
+
} else if (filter.body && typeof filter.body === "string") {
|
|
99
|
+
filter.body = new Search(`${filter.body} ${token.value}`);
|
|
100
|
+
} else {
|
|
101
|
+
filter.body = new Search(token.value);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseNumericOrString(value: string): number | string {
|
|
106
|
+
const num: number = Number(value);
|
|
107
|
+
return isNaN(num) ? value : num;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function queryStringToFilter(queryString: string): LogFilter {
|
|
111
|
+
const tokens: Array<ParsedToken> = parseLogQuery(queryString);
|
|
112
|
+
const filter: LogFilter = {};
|
|
113
|
+
|
|
114
|
+
for (const token of tokens) {
|
|
115
|
+
switch (token.type) {
|
|
116
|
+
case TokenType.FreeText:
|
|
117
|
+
applyFreeTextFilter(filter, token);
|
|
118
|
+
break;
|
|
119
|
+
case TokenType.FieldFilter:
|
|
120
|
+
applyFieldFilter(filter, token);
|
|
121
|
+
break;
|
|
122
|
+
case TokenType.AttributeFilter:
|
|
123
|
+
applyFieldFilter(filter, token);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return filter;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default queryStringToFilter;
|