@oneuptime/common 10.0.54 → 10.0.56
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/DatabaseModels/DockerHost.ts +662 -0
- package/Models/DatabaseModels/GlobalConfig.ts +112 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Server/API/TelemetryAPI.ts +352 -16
- package/Server/Infrastructure/ClickhouseConfig.ts +9 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1774000000002-MigrationName.ts +76 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1775766676723-MigrationName.ts +133 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1775900000000-AddGlobalSmtpOAuth.ts +51 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Services/DockerHostService.ts +173 -0
- package/Server/Services/ExceptionAggregationService.ts +335 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/LogAggregationService.ts +17 -0
- package/Server/Services/MonitorProbeService.ts +42 -21
- package/Server/Services/MonitorService.ts +21 -21
- package/Server/Services/TraceAggregationService.ts +514 -0
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +73 -1
- package/Tests/Server/Services/LogAggregationService.test.ts +2 -2
- package/Tests/__mocks__/mermaid.js +18 -0
- package/Tests/__mocks__/react-markdown.js +17 -0
- package/Tests/__mocks__/react-syntax-highlighter.js +19 -0
- package/Tests/__mocks__/remark-gfm.js +8 -0
- package/Types/Icon/IconProp.ts +1 -0
- package/Types/Monitor/DockerAlertTemplates.ts +507 -0
- package/Types/Monitor/DockerMetricCatalog.ts +226 -0
- package/Types/Monitor/MonitorStep.ts +33 -0
- package/Types/Monitor/MonitorStepDockerMonitor.ts +38 -0
- package/Types/Monitor/MonitorType.ts +15 -1
- package/Types/Permission.ts +38 -0
- package/UI/Components/Icon/Icon.tsx +87 -0
- package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +7 -132
- package/UI/Components/ModelDetail/CardModelDetail.tsx +11 -1
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +285 -0
- package/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.tsx +85 -0
- package/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.tsx +156 -0
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSection.tsx +160 -0
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +85 -0
- package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +102 -0
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +280 -0
- package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +125 -0
- package/UI/Components/TelemetryViewer/components/TelemetryPagination.tsx +114 -0
- package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +378 -0
- package/UI/Components/TelemetryViewer/components/TelemetrySearchHelp.tsx +78 -0
- package/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.tsx +64 -0
- package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +193 -0
- package/UI/Components/TelemetryViewer/types.ts +67 -0
- package/build/dist/Models/DatabaseModels/DockerHost.js +686 -0
- package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +117 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +237 -16
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/ClickhouseConfig.js +9 -0
- package/build/dist/Server/Infrastructure/ClickhouseConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1774000000002-MigrationName.js +35 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1774000000002-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775766676723-MigrationName.js +52 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775766676723-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775900000000-AddGlobalSmtpOAuth.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775900000000-AddGlobalSmtpOAuth.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +162 -0
- package/build/dist/Server/Services/DockerHostService.js.map +1 -0
- package/build/dist/Server/Services/ExceptionAggregationService.js +224 -0
- package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -0
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +11 -0
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MonitorProbeService.js +28 -14
- package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +19 -17
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/TraceAggregationService.js +364 -0
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +46 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Tests/Server/Services/LogAggregationService.test.js +2 -2
- package/build/dist/Tests/Server/Services/LogAggregationService.test.js.map +1 -1
- package/build/dist/Types/Icon/IconProp.js +1 -0
- package/build/dist/Types/Icon/IconProp.js.map +1 -1
- package/build/dist/Types/Monitor/DockerAlertTemplates.js +410 -0
- package/build/dist/Types/Monitor/DockerAlertTemplates.js.map +1 -0
- package/build/dist/Types/Monitor/DockerMetricCatalog.js +192 -0
- package/build/dist/Types/Monitor/DockerMetricCatalog.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorStep.js +23 -0
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStepDockerMonitor.js +21 -0
- package/build/dist/Types/Monitor/MonitorStepDockerMonitor.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorType.js +14 -1
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/Permission.js +36 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +13 -0
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +7 -75
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +8 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +71 -0
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js +39 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js +61 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js +66 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +41 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +35 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +132 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +65 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js +52 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +224 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchHelp.js +35 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchHelp.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js +27 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +97 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/types.js +6 -0
- package/build/dist/UI/Components/TelemetryViewer/types.js.map +1 -0
- package/jest.config.json +6 -1
- package/package.json +1 -1
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import { SQL, Statement } from "../Utils/AnalyticsDatabase/Statement";
|
|
2
|
+
import SpanService from "./SpanService";
|
|
3
|
+
import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
|
|
4
|
+
import { JSONObject } from "../../Types/JSON";
|
|
5
|
+
import ObjectID from "../../Types/ObjectID";
|
|
6
|
+
import BadDataException from "../../Types/Exception/BadDataException";
|
|
7
|
+
import Includes from "../../Types/BaseDatabase/Includes";
|
|
8
|
+
import AnalyticsTableName from "../../Types/AnalyticsDatabase/AnalyticsTableName";
|
|
9
|
+
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
10
|
+
import { DbJSONResponse, Results } from "./AnalyticsDatabaseService";
|
|
11
|
+
|
|
12
|
+
export interface HistogramBucket {
|
|
13
|
+
time: string;
|
|
14
|
+
series: string;
|
|
15
|
+
count: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TraceFilters {
|
|
19
|
+
serviceIds?: Array<ObjectID> | undefined;
|
|
20
|
+
statusCodes?: Array<number> | undefined;
|
|
21
|
+
spanKinds?: Array<string> | undefined;
|
|
22
|
+
spanNames?: Array<string> | undefined;
|
|
23
|
+
traceIds?: Array<string> | undefined;
|
|
24
|
+
nameSearchText?: string | undefined;
|
|
25
|
+
rootOnly?: boolean | undefined;
|
|
26
|
+
attributes?: Record<string, string> | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface HistogramRequest extends TraceFilters {
|
|
30
|
+
projectId: ObjectID;
|
|
31
|
+
startTime: Date;
|
|
32
|
+
endTime: Date;
|
|
33
|
+
bucketSizeInMinutes: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface FacetValue {
|
|
37
|
+
value: string;
|
|
38
|
+
count: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface FacetRequest extends TraceFilters {
|
|
42
|
+
projectId: ObjectID;
|
|
43
|
+
startTime: Date;
|
|
44
|
+
endTime: Date;
|
|
45
|
+
facetKey: string;
|
|
46
|
+
limit?: number | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface MultiFacetRequest extends TraceFilters {
|
|
50
|
+
projectId: ObjectID;
|
|
51
|
+
startTime: Date;
|
|
52
|
+
endTime: Date;
|
|
53
|
+
facetKeys: Array<string>;
|
|
54
|
+
limit?: number | undefined;
|
|
55
|
+
sampleSize?: number | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class TraceAggregationService {
|
|
59
|
+
private static readonly DEFAULT_FACET_LIMIT: number = 500;
|
|
60
|
+
private static readonly TABLE_NAME: string = AnalyticsTableName.Span;
|
|
61
|
+
private static readonly TOP_LEVEL_COLUMNS: Set<string> = new Set([
|
|
62
|
+
"serviceId",
|
|
63
|
+
"traceId",
|
|
64
|
+
"spanId",
|
|
65
|
+
"parentSpanId",
|
|
66
|
+
"name",
|
|
67
|
+
"kind",
|
|
68
|
+
"statusCode",
|
|
69
|
+
]);
|
|
70
|
+
private static readonly ATTRIBUTE_KEY_PATTERN: RegExp = /^[a-zA-Z0-9._:/-]+$/;
|
|
71
|
+
private static readonly MAX_FACET_KEY_LENGTH: number = 256;
|
|
72
|
+
|
|
73
|
+
@CaptureSpan()
|
|
74
|
+
public static async getHistogram(
|
|
75
|
+
request: HistogramRequest,
|
|
76
|
+
): Promise<Array<HistogramBucket>> {
|
|
77
|
+
const statement: Statement =
|
|
78
|
+
TraceAggregationService.buildHistogramStatement(request);
|
|
79
|
+
|
|
80
|
+
const dbResult: Results = await SpanService.executeQuery(statement);
|
|
81
|
+
const response: DbJSONResponse = await dbResult.json<{
|
|
82
|
+
data?: Array<JSONObject>;
|
|
83
|
+
}>();
|
|
84
|
+
|
|
85
|
+
const rows: Array<JSONObject> = response.data || [];
|
|
86
|
+
|
|
87
|
+
return rows.map((row: JSONObject): HistogramBucket => {
|
|
88
|
+
return {
|
|
89
|
+
time: String(row["bucket"] || ""),
|
|
90
|
+
series: TraceAggregationService.mapStatusCodeToSeries(
|
|
91
|
+
Number(row["statusCode"] || 0),
|
|
92
|
+
),
|
|
93
|
+
count: Number(row["cnt"] || 0),
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@CaptureSpan()
|
|
99
|
+
public static async getFacetValues(
|
|
100
|
+
request: FacetRequest,
|
|
101
|
+
): Promise<Array<FacetValue>> {
|
|
102
|
+
const statement: Statement =
|
|
103
|
+
TraceAggregationService.buildFacetStatement(request);
|
|
104
|
+
|
|
105
|
+
const dbResult: Results = await SpanService.executeQuery(statement);
|
|
106
|
+
const response: DbJSONResponse = await dbResult.json<{
|
|
107
|
+
data?: Array<JSONObject>;
|
|
108
|
+
}>();
|
|
109
|
+
|
|
110
|
+
const rows: Array<JSONObject> = response.data || [];
|
|
111
|
+
|
|
112
|
+
return rows
|
|
113
|
+
.map((row: JSONObject): FacetValue => {
|
|
114
|
+
return {
|
|
115
|
+
value: String(row["val"] || ""),
|
|
116
|
+
count: Number(row["cnt"] || 0),
|
|
117
|
+
};
|
|
118
|
+
})
|
|
119
|
+
.filter((facet: FacetValue): boolean => {
|
|
120
|
+
return facet.value.length > 0;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/*
|
|
125
|
+
* Sample-based facet computation. Runs a single sort-key aligned query
|
|
126
|
+
* (ORDER BY startTime DESC LIMIT sampleSize) and computes top-K per facet
|
|
127
|
+
* in Node.js. This avoids ClickHouse GROUP BY aggregations that can't
|
|
128
|
+
* return partial results under max_execution_time 'break' mode, and
|
|
129
|
+
* leverages the (projectId, startTime, ...) primary key for efficient
|
|
130
|
+
* backwards scans. Facet values reflect the most recent N root spans in
|
|
131
|
+
* the window — the most actionable view for filtering.
|
|
132
|
+
*/
|
|
133
|
+
@CaptureSpan()
|
|
134
|
+
public static async getFacetValuesFromSample(
|
|
135
|
+
request: MultiFacetRequest,
|
|
136
|
+
): Promise<Record<string, Array<FacetValue>>> {
|
|
137
|
+
const limit: number =
|
|
138
|
+
request.limit ?? TraceAggregationService.DEFAULT_FACET_LIMIT;
|
|
139
|
+
const sampleSize: number = request.sampleSize ?? 1000;
|
|
140
|
+
|
|
141
|
+
for (const facetKey of request.facetKeys) {
|
|
142
|
+
TraceAggregationService.validateFacetKey(facetKey);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const topLevelKeys: Array<string> = request.facetKeys.filter(
|
|
146
|
+
(key: string): boolean => {
|
|
147
|
+
return TraceAggregationService.isTopLevelColumn(key);
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
const attributeKeys: Array<string> = request.facetKeys.filter(
|
|
151
|
+
(key: string): boolean => {
|
|
152
|
+
return !TraceAggregationService.isTopLevelColumn(key);
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const selectColumns: Array<string> = [];
|
|
157
|
+
if (topLevelKeys.length > 0) {
|
|
158
|
+
selectColumns.push(...topLevelKeys);
|
|
159
|
+
}
|
|
160
|
+
if (attributeKeys.length > 0) {
|
|
161
|
+
selectColumns.push("attributes");
|
|
162
|
+
}
|
|
163
|
+
if (selectColumns.length === 0) {
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/*
|
|
168
|
+
* Safe to interpolate: top-level column names come from a hardcoded
|
|
169
|
+
* allowlist (TOP_LEVEL_COLUMNS) and "attributes" is a literal. TABLE_NAME
|
|
170
|
+
* is also a private constant.
|
|
171
|
+
*/
|
|
172
|
+
const statement: Statement = new Statement();
|
|
173
|
+
statement.append(
|
|
174
|
+
`SELECT ${selectColumns.join(", ")} FROM ${TraceAggregationService.TABLE_NAME}`,
|
|
175
|
+
);
|
|
176
|
+
statement.append(
|
|
177
|
+
SQL` WHERE projectId = ${{
|
|
178
|
+
type: TableColumnType.ObjectID,
|
|
179
|
+
value: request.projectId,
|
|
180
|
+
}} AND startTime >= ${{
|
|
181
|
+
type: TableColumnType.Date,
|
|
182
|
+
value: request.startTime,
|
|
183
|
+
}} AND startTime <= ${{
|
|
184
|
+
type: TableColumnType.Date,
|
|
185
|
+
value: request.endTime,
|
|
186
|
+
}}`,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
TraceAggregationService.appendCommonFilters(statement, request);
|
|
190
|
+
|
|
191
|
+
statement.append(
|
|
192
|
+
SQL` ORDER BY startTime DESC LIMIT ${{
|
|
193
|
+
type: TableColumnType.Number,
|
|
194
|
+
value: sampleSize,
|
|
195
|
+
}}`,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
/*
|
|
199
|
+
* Defense in depth: the sample query is sort-key aligned and should
|
|
200
|
+
* return in well under a second, but cap runtime below nginx's 60s
|
|
201
|
+
* proxy_read_timeout regardless.
|
|
202
|
+
*/
|
|
203
|
+
statement.append(
|
|
204
|
+
" SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const dbResult: Results = await SpanService.executeQuery(statement);
|
|
208
|
+
const response: DbJSONResponse = await dbResult.json<{
|
|
209
|
+
data?: Array<JSONObject>;
|
|
210
|
+
}>();
|
|
211
|
+
|
|
212
|
+
const rows: Array<JSONObject> = response.data || [];
|
|
213
|
+
|
|
214
|
+
const counts: Record<string, Map<string, number>> = {};
|
|
215
|
+
for (const key of request.facetKeys) {
|
|
216
|
+
counts[key] = new Map<string, number>();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const row of rows) {
|
|
220
|
+
for (const key of topLevelKeys) {
|
|
221
|
+
const raw: unknown = row[key];
|
|
222
|
+
if (raw === undefined || raw === null) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const value: string = String(raw);
|
|
226
|
+
if (value.length === 0) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const map: Map<string, number> = counts[key]!;
|
|
230
|
+
map.set(value, (map.get(value) || 0) + 1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (attributeKeys.length > 0) {
|
|
234
|
+
const attrs: unknown = row["attributes"];
|
|
235
|
+
let parsed: Record<string, unknown> | null = null;
|
|
236
|
+
if (attrs && typeof attrs === "object") {
|
|
237
|
+
parsed = attrs as Record<string, unknown>;
|
|
238
|
+
} else if (typeof attrs === "string" && attrs.length > 0) {
|
|
239
|
+
try {
|
|
240
|
+
parsed = JSON.parse(attrs) as Record<string, unknown>;
|
|
241
|
+
} catch {
|
|
242
|
+
parsed = null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (parsed) {
|
|
246
|
+
for (const key of attributeKeys) {
|
|
247
|
+
const raw: unknown = parsed[key];
|
|
248
|
+
if (raw === undefined || raw === null) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const value: string =
|
|
252
|
+
typeof raw === "object" ? JSON.stringify(raw) : String(raw);
|
|
253
|
+
if (value.length === 0) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const map: Map<string, number> = counts[key]!;
|
|
257
|
+
map.set(value, (map.get(value) || 0) + 1);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const result: Record<string, Array<FacetValue>> = {};
|
|
264
|
+
for (const key of request.facetKeys) {
|
|
265
|
+
const entries: Array<FacetValue> = Array.from(counts[key]!.entries()).map(
|
|
266
|
+
([value, count]: [string, number]): FacetValue => {
|
|
267
|
+
return { value, count };
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
entries.sort((a: FacetValue, b: FacetValue): number => {
|
|
271
|
+
return b.count - a.count;
|
|
272
|
+
});
|
|
273
|
+
result[key] = entries.slice(0, limit);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private static mapStatusCodeToSeries(code: number): string {
|
|
280
|
+
if (code === 1) {
|
|
281
|
+
return "ok";
|
|
282
|
+
}
|
|
283
|
+
if (code === 2) {
|
|
284
|
+
return "error";
|
|
285
|
+
}
|
|
286
|
+
return "unset";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private static buildHistogramStatement(request: HistogramRequest): Statement {
|
|
290
|
+
const intervalSeconds: number = request.bucketSizeInMinutes * 60;
|
|
291
|
+
|
|
292
|
+
const statement: Statement = SQL`
|
|
293
|
+
SELECT
|
|
294
|
+
toStartOfInterval(startTime, INTERVAL ${{
|
|
295
|
+
type: TableColumnType.Number,
|
|
296
|
+
value: intervalSeconds,
|
|
297
|
+
}} SECOND) AS bucket,
|
|
298
|
+
statusCode,
|
|
299
|
+
count() AS cnt
|
|
300
|
+
FROM ${TraceAggregationService.TABLE_NAME}
|
|
301
|
+
WHERE projectId = ${{
|
|
302
|
+
type: TableColumnType.ObjectID,
|
|
303
|
+
value: request.projectId,
|
|
304
|
+
}}
|
|
305
|
+
AND startTime >= ${{
|
|
306
|
+
type: TableColumnType.Date,
|
|
307
|
+
value: request.startTime,
|
|
308
|
+
}}
|
|
309
|
+
AND startTime <= ${{
|
|
310
|
+
type: TableColumnType.Date,
|
|
311
|
+
value: request.endTime,
|
|
312
|
+
}}
|
|
313
|
+
`;
|
|
314
|
+
|
|
315
|
+
TraceAggregationService.appendCommonFilters(statement, request);
|
|
316
|
+
|
|
317
|
+
statement.append(" GROUP BY bucket, statusCode ORDER BY bucket ASC");
|
|
318
|
+
|
|
319
|
+
/*
|
|
320
|
+
* Defense in depth: cap histogram runtime below nginx's 60s
|
|
321
|
+
* proxy_read_timeout. ClickHouse returns partial aggregated results
|
|
322
|
+
* with 'break' mode rather than throwing, which is acceptable for
|
|
323
|
+
* a density visualization.
|
|
324
|
+
*/
|
|
325
|
+
statement.append(
|
|
326
|
+
" SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
return statement;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private static buildFacetStatement(request: FacetRequest): Statement {
|
|
333
|
+
const limit: number =
|
|
334
|
+
request.limit ?? TraceAggregationService.DEFAULT_FACET_LIMIT;
|
|
335
|
+
|
|
336
|
+
TraceAggregationService.validateFacetKey(request.facetKey);
|
|
337
|
+
|
|
338
|
+
const isTopLevelColumn: boolean = TraceAggregationService.isTopLevelColumn(
|
|
339
|
+
request.facetKey,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const statement: Statement = new Statement();
|
|
343
|
+
|
|
344
|
+
if (isTopLevelColumn) {
|
|
345
|
+
statement.append(
|
|
346
|
+
SQL`SELECT toString(${request.facetKey}) AS val, count() AS cnt FROM ${TraceAggregationService.TABLE_NAME}`,
|
|
347
|
+
);
|
|
348
|
+
} else {
|
|
349
|
+
statement.append(
|
|
350
|
+
SQL`SELECT JSONExtractRaw(attributes, ${{
|
|
351
|
+
type: TableColumnType.Text,
|
|
352
|
+
value: request.facetKey,
|
|
353
|
+
}}) AS val, count() AS cnt FROM ${TraceAggregationService.TABLE_NAME}`,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
statement.append(
|
|
358
|
+
SQL` WHERE projectId = ${{
|
|
359
|
+
type: TableColumnType.ObjectID,
|
|
360
|
+
value: request.projectId,
|
|
361
|
+
}} AND startTime >= ${{
|
|
362
|
+
type: TableColumnType.Date,
|
|
363
|
+
value: request.startTime,
|
|
364
|
+
}} AND startTime <= ${{
|
|
365
|
+
type: TableColumnType.Date,
|
|
366
|
+
value: request.endTime,
|
|
367
|
+
}}`,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
if (!isTopLevelColumn) {
|
|
371
|
+
statement.append(
|
|
372
|
+
SQL` AND JSONHas(attributes, ${{
|
|
373
|
+
type: TableColumnType.Text,
|
|
374
|
+
value: request.facetKey,
|
|
375
|
+
}}) = 1`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
TraceAggregationService.appendCommonFilters(statement, request);
|
|
380
|
+
|
|
381
|
+
statement.append(
|
|
382
|
+
SQL` GROUP BY val ORDER BY cnt DESC LIMIT ${{
|
|
383
|
+
type: TableColumnType.Number,
|
|
384
|
+
value: limit,
|
|
385
|
+
}}`,
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
/*
|
|
389
|
+
* Defense in depth: cap individual facet query runtime below nginx's
|
|
390
|
+
* 60s proxy_read_timeout so a slow facet never starves the endpoint.
|
|
391
|
+
*/
|
|
392
|
+
statement.append(
|
|
393
|
+
" SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
return statement;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private static appendCommonFilters(
|
|
400
|
+
statement: Statement,
|
|
401
|
+
request: TraceFilters,
|
|
402
|
+
): void {
|
|
403
|
+
if (request.rootOnly) {
|
|
404
|
+
statement.append(" AND (parentSpanId = '' OR parentSpanId IS NULL)");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (request.serviceIds && request.serviceIds.length > 0) {
|
|
408
|
+
statement.append(
|
|
409
|
+
SQL` AND serviceId IN (${{
|
|
410
|
+
type: TableColumnType.ObjectID,
|
|
411
|
+
value: new Includes(
|
|
412
|
+
request.serviceIds.map((id: ObjectID) => {
|
|
413
|
+
return id.toString();
|
|
414
|
+
}),
|
|
415
|
+
),
|
|
416
|
+
}})`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (request.statusCodes && request.statusCodes.length > 0) {
|
|
421
|
+
statement.append(
|
|
422
|
+
SQL` AND statusCode IN (${{
|
|
423
|
+
type: TableColumnType.Number,
|
|
424
|
+
value: new Includes(
|
|
425
|
+
request.statusCodes.map((code: number) => {
|
|
426
|
+
return String(code);
|
|
427
|
+
}),
|
|
428
|
+
),
|
|
429
|
+
}})`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (request.spanKinds && request.spanKinds.length > 0) {
|
|
434
|
+
statement.append(
|
|
435
|
+
SQL` AND toString(kind) IN (${{
|
|
436
|
+
type: TableColumnType.Text,
|
|
437
|
+
value: new Includes(request.spanKinds),
|
|
438
|
+
}})`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (request.spanNames && request.spanNames.length > 0) {
|
|
443
|
+
statement.append(
|
|
444
|
+
SQL` AND name IN (${{
|
|
445
|
+
type: TableColumnType.Text,
|
|
446
|
+
value: new Includes(request.spanNames),
|
|
447
|
+
}})`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (request.traceIds && request.traceIds.length > 0) {
|
|
452
|
+
statement.append(
|
|
453
|
+
SQL` AND traceId IN (${{
|
|
454
|
+
type: TableColumnType.Text,
|
|
455
|
+
value: new Includes(request.traceIds),
|
|
456
|
+
}})`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (request.nameSearchText && request.nameSearchText.trim().length > 0) {
|
|
461
|
+
statement.append(
|
|
462
|
+
SQL` AND name ILIKE ${{
|
|
463
|
+
type: TableColumnType.Text,
|
|
464
|
+
value: `%${request.nameSearchText.trim()}%`,
|
|
465
|
+
}}`,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (request.attributes && Object.keys(request.attributes).length > 0) {
|
|
470
|
+
for (const [attrKey, attrValue] of Object.entries(request.attributes)) {
|
|
471
|
+
TraceAggregationService.validateFacetKey(attrKey);
|
|
472
|
+
|
|
473
|
+
statement.append(
|
|
474
|
+
SQL` AND attributes[${{
|
|
475
|
+
type: TableColumnType.Text,
|
|
476
|
+
value: attrKey,
|
|
477
|
+
}}] = ${{
|
|
478
|
+
type: TableColumnType.Text,
|
|
479
|
+
value: attrValue,
|
|
480
|
+
}}`,
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private static isTopLevelColumn(key: string): boolean {
|
|
487
|
+
return TraceAggregationService.TOP_LEVEL_COLUMNS.has(key);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private static validateFacetKey(
|
|
491
|
+
facetKey: unknown,
|
|
492
|
+
): asserts facetKey is string {
|
|
493
|
+
if (typeof facetKey !== "string") {
|
|
494
|
+
throw new BadDataException("Invalid facetKey");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (
|
|
498
|
+
facetKey.length === 0 ||
|
|
499
|
+
facetKey.length > TraceAggregationService.MAX_FACET_KEY_LENGTH
|
|
500
|
+
) {
|
|
501
|
+
throw new BadDataException("Invalid facetKey");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (TraceAggregationService.isTopLevelColumn(facetKey)) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (!TraceAggregationService.ATTRIBUTE_KEY_PATTERN.test(facetKey)) {
|
|
509
|
+
throw new BadDataException("Invalid facetKey");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export default TraceAggregationService;
|
|
@@ -49,6 +49,7 @@ import MetricMonitorResponse, {
|
|
|
49
49
|
KubernetesAffectedResource,
|
|
50
50
|
KubernetesResourceBreakdown,
|
|
51
51
|
} from "../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
|
|
52
|
+
import MonitorStepDockerMonitor from "../../../Types/Monitor/MonitorStepDockerMonitor";
|
|
52
53
|
|
|
53
54
|
export default class MonitorCriteriaEvaluator {
|
|
54
55
|
public static async processMonitorStep(input: {
|
|
@@ -459,7 +460,8 @@ ${contextBlock}
|
|
|
459
460
|
|
|
460
461
|
if (
|
|
461
462
|
input.monitor.monitorType === MonitorType.Metrics ||
|
|
462
|
-
input.monitor.monitorType === MonitorType.Kubernetes
|
|
463
|
+
input.monitor.monitorType === MonitorType.Kubernetes ||
|
|
464
|
+
input.monitor.monitorType === MonitorType.Docker
|
|
463
465
|
) {
|
|
464
466
|
const metricMonitorResult: string | null =
|
|
465
467
|
await MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
|
|
@@ -574,6 +576,11 @@ ${contextBlock}
|
|
|
574
576
|
);
|
|
575
577
|
}
|
|
576
578
|
|
|
579
|
+
// Handle Docker monitors with resource context
|
|
580
|
+
if (input.monitor.monitorType === MonitorType.Docker) {
|
|
581
|
+
return MonitorCriteriaEvaluator.buildDockerRootCauseContext(input);
|
|
582
|
+
}
|
|
583
|
+
|
|
577
584
|
const requestDetails: Array<string> = [];
|
|
578
585
|
const responseDetails: Array<string> = [];
|
|
579
586
|
const failureDetails: Array<string> = [];
|
|
@@ -922,6 +929,71 @@ ${contextBlock}
|
|
|
922
929
|
return sections.join("\n");
|
|
923
930
|
}
|
|
924
931
|
|
|
932
|
+
private static buildDockerRootCauseContext(input: {
|
|
933
|
+
dataToProcess: DataToProcess;
|
|
934
|
+
monitorStep: MonitorStep;
|
|
935
|
+
monitor: Monitor;
|
|
936
|
+
}): string | null {
|
|
937
|
+
const metricResponse: MetricMonitorResponse =
|
|
938
|
+
input.dataToProcess as MetricMonitorResponse;
|
|
939
|
+
|
|
940
|
+
const sections: Array<string> = [];
|
|
941
|
+
|
|
942
|
+
// Docker host context
|
|
943
|
+
const dockerMonitor: MonitorStepDockerMonitor | undefined =
|
|
944
|
+
input.monitorStep.data?.dockerMonitor;
|
|
945
|
+
|
|
946
|
+
if (dockerMonitor) {
|
|
947
|
+
const hostDetails: Array<string> = [];
|
|
948
|
+
hostDetails.push(`- Host: ${dockerMonitor.hostIdentifier || "Unknown"}`);
|
|
949
|
+
|
|
950
|
+
if (dockerMonitor.containerFilters?.containerName) {
|
|
951
|
+
hostDetails.push(
|
|
952
|
+
`- Container Name Filter: ${dockerMonitor.containerFilters.containerName}`,
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (dockerMonitor.containerFilters?.containerImage) {
|
|
957
|
+
hostDetails.push(
|
|
958
|
+
`- Container Image Filter: ${dockerMonitor.containerFilters.containerImage}`,
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Add metric name from the query config
|
|
963
|
+
if (
|
|
964
|
+
dockerMonitor.metricViewConfig?.queryConfigs?.length > 0 &&
|
|
965
|
+
dockerMonitor.metricViewConfig.queryConfigs[0]
|
|
966
|
+
) {
|
|
967
|
+
const metricName: string = dockerMonitor.metricViewConfig
|
|
968
|
+
.queryConfigs[0].metricQueryData?.filterData?.metricName as string;
|
|
969
|
+
if (metricName) {
|
|
970
|
+
hostDetails.push(`- Metric: \`${metricName}\``);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
sections.push(`**Docker Host Details**\n${hostDetails.join("\n")}`);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Metric results summary
|
|
978
|
+
if (metricResponse.metricResult && metricResponse.metricResult.length > 0) {
|
|
979
|
+
const resultDetails: Array<string> = [];
|
|
980
|
+
|
|
981
|
+
for (const result of metricResponse.metricResult) {
|
|
982
|
+
if (result.data && result.data.length > 0) {
|
|
983
|
+
resultDetails.push(
|
|
984
|
+
`- ${result.data.length} metric data point(s) returned`,
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (resultDetails.length > 0) {
|
|
990
|
+
sections.push(`\n\n**Metric Summary**\n${resultDetails.join("\n")}`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return sections.length > 0 ? sections.join("\n") : null;
|
|
995
|
+
}
|
|
996
|
+
|
|
925
997
|
private static buildKubernetesRootCauseAnalysis(input: {
|
|
926
998
|
breakdown: KubernetesResourceBreakdown;
|
|
927
999
|
topResource: KubernetesAffectedResource;
|
|
@@ -31,7 +31,7 @@ describe("LogAggregationService", () => {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
expect(statement.query).toBe(
|
|
34
|
-
"SELECT toString({p0:Identifier}) AS val, count() AS cnt FROM {p1:Identifier} WHERE projectId = {p2:String} AND time >= {p3:DateTime} AND time <= {p4:DateTime} GROUP BY val ORDER BY cnt DESC LIMIT {p5:Int32}",
|
|
34
|
+
"SELECT toString({p0:Identifier}) AS val, count() AS cnt FROM {p1:Identifier} WHERE projectId = {p2:String} AND time >= {p3:DateTime} AND time <= {p4:DateTime} GROUP BY val ORDER BY cnt DESC LIMIT {p5:Int32} SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
|
|
35
35
|
);
|
|
36
36
|
|
|
37
37
|
expect(statement.query_params).toStrictEqual({
|
|
@@ -51,7 +51,7 @@ describe("LogAggregationService", () => {
|
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
expect(statement.query).toBe(
|
|
54
|
-
"SELECT JSONExtractRaw(attributes, {p0:String}) AS val, count() AS cnt FROM {p1:Identifier} WHERE projectId = {p2:String} AND time >= {p3:DateTime} AND time <= {p4:DateTime} AND JSONHas(attributes, {p5:String}) = 1 GROUP BY val ORDER BY cnt DESC LIMIT {p6:Int32}",
|
|
54
|
+
"SELECT JSONExtractRaw(attributes, {p0:String}) AS val, count() AS cnt FROM {p1:Identifier} WHERE projectId = {p2:String} AND time >= {p3:DateTime} AND time <= {p4:DateTime} AND JSONHas(attributes, {p5:String}) = 1 GROUP BY val ORDER BY cnt DESC LIMIT {p6:Int32} SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
|
|
55
55
|
);
|
|
56
56
|
|
|
57
57
|
expect(statement.query_params).toStrictEqual({
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const mermaid = {
|
|
4
|
+
initialize: function () {},
|
|
5
|
+
render: function () {
|
|
6
|
+
return Promise.resolve({ svg: "" });
|
|
7
|
+
},
|
|
8
|
+
run: function () {
|
|
9
|
+
return Promise.resolve();
|
|
10
|
+
},
|
|
11
|
+
parse: function () {
|
|
12
|
+
return Promise.resolve(true);
|
|
13
|
+
},
|
|
14
|
+
contentLoaded: function () {},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
module.exports = mermaid;
|
|
18
|
+
module.exports.default = mermaid;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const React = require("react");
|
|
4
|
+
|
|
5
|
+
function ReactMarkdown(props) {
|
|
6
|
+
return React.createElement(
|
|
7
|
+
"div",
|
|
8
|
+
{ "data-testid": "react-markdown" },
|
|
9
|
+
props && props.children,
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = ReactMarkdown;
|
|
14
|
+
module.exports.default = ReactMarkdown;
|
|
15
|
+
module.exports.defaultUrlTransform = function (url) {
|
|
16
|
+
return url;
|
|
17
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const React = require("react");
|
|
4
|
+
|
|
5
|
+
function SyntaxHighlighter(props) {
|
|
6
|
+
return React.createElement(
|
|
7
|
+
"pre",
|
|
8
|
+
{ "data-testid": "syntax-highlighter" },
|
|
9
|
+
props && props.children,
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
SyntaxHighlighter.registerLanguage = function () {};
|
|
14
|
+
|
|
15
|
+
module.exports = SyntaxHighlighter;
|
|
16
|
+
module.exports.default = SyntaxHighlighter;
|
|
17
|
+
module.exports.Prism = SyntaxHighlighter;
|
|
18
|
+
module.exports.Light = SyntaxHighlighter;
|
|
19
|
+
module.exports.PrismLight = SyntaxHighlighter;
|