@oneuptime/common 10.5.1 → 10.5.2
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/TelemetryException.ts +10 -0
- package/Server/API/TelemetryAPI.ts +406 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1779879993421-MigrationName.ts +20 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1779900000000-DedupeTelemetryExceptionsAndAddUniqueIndex.ts +115 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/ExceptionAggregationService.ts +51 -3
- package/Server/Services/LogAggregationService.ts +1 -0
- package/Server/Services/MetricAggregationService.ts +227 -0
- package/Server/Services/OpenTelemetryIngestService.ts +101 -1
- package/Server/Services/TraceAggregationService.ts +1 -0
- package/Server/Utils/Monitor/MonitorLogUtil.ts +146 -6
- package/Server/Utils/Telemetry/ResourceFacetResolver.ts +299 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +10 -0
- package/UI/Components/LogsViewer/components/FacetSection.tsx +40 -3
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +23 -0
- package/UI/Components/LogsViewer/types.ts +2 -0
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +8 -0
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSection.tsx +49 -3
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +16 -0
- package/UI/Components/TelemetryViewer/types.ts +12 -0
- package/build/dist/Models/DatabaseModels/TelemetryException.js +11 -0
- package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +285 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779879993421-MigrationName.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779879993421-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779900000000-DedupeTelemetryExceptionsAndAddUniqueIndex.js +106 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779900000000-DedupeTelemetryExceptionsAndAddUniqueIndex.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/ExceptionAggregationService.js +44 -4
- package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MetricAggregationService.js +159 -0
- package/build/dist/Server/Services/MetricAggregationService.js.map +1 -0
- package/build/dist/Server/Services/OpenTelemetryIngestService.js +60 -3
- package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +127 -4
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +204 -0
- package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/FacetSection.js +26 -6
- package/build/dist/UI/Components/LogsViewer/components/FacetSection.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +12 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js +32 -6
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +6 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
2
|
+
import PositiveNumber from "../../../Types/PositiveNumber";
|
|
3
|
+
import Search from "../../../Types/BaseDatabase/Search";
|
|
4
|
+
import MultiSearch from "../../../Types/BaseDatabase/MultiSearch";
|
|
5
|
+
import ServiceModel from "../../../Models/DatabaseModels/Service";
|
|
6
|
+
import HostModel from "../../../Models/DatabaseModels/Host";
|
|
7
|
+
import DockerHostModel from "../../../Models/DatabaseModels/DockerHost";
|
|
8
|
+
import KubernetesClusterModel from "../../../Models/DatabaseModels/KubernetesCluster";
|
|
9
|
+
import ServiceService from "../../Services/ServiceService";
|
|
10
|
+
import HostService from "../../Services/HostService";
|
|
11
|
+
import DockerHostService from "../../Services/DockerHostService";
|
|
12
|
+
import KubernetesClusterService from "../../Services/KubernetesClusterService";
|
|
13
|
+
import CaptureSpan from "./CaptureSpan";
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* Facet keys whose values are entity IDs backed by a Postgres source-of-truth
|
|
17
|
+
* table. ClickHouse aggregation only sees IDs that appear in the sampled
|
|
18
|
+
* trace/log window, so low-volume resources never reach the filter sidebar.
|
|
19
|
+
* Resolving the value list from Postgres instead means every project resource
|
|
20
|
+
* shows up regardless of recent telemetry activity, and the sidebar search
|
|
21
|
+
* matches across the full set (not just the loaded subset).
|
|
22
|
+
*/
|
|
23
|
+
export const RESOURCE_FACET_KEYS: ReadonlySet<string> = new Set([
|
|
24
|
+
"serviceId",
|
|
25
|
+
"hostId",
|
|
26
|
+
"dockerHostId",
|
|
27
|
+
"kubernetesClusterId",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export interface ResourceFacetSpec {
|
|
31
|
+
facetKey: string;
|
|
32
|
+
counts: Map<string, number>;
|
|
33
|
+
searchText?: string | undefined;
|
|
34
|
+
limit?: number | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ResolvedFacetValue {
|
|
38
|
+
value: string;
|
|
39
|
+
count: number;
|
|
40
|
+
displayName: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default class ResourceFacetResolver {
|
|
44
|
+
private static readonly DEFAULT_LIMIT: number = 500;
|
|
45
|
+
|
|
46
|
+
public static isResourceFacet(facetKey: string): boolean {
|
|
47
|
+
return RESOURCE_FACET_KEYS.has(facetKey);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@CaptureSpan()
|
|
51
|
+
public static async resolve(
|
|
52
|
+
projectId: ObjectID,
|
|
53
|
+
specs: Array<ResourceFacetSpec>,
|
|
54
|
+
): Promise<Record<string, Array<ResolvedFacetValue>>> {
|
|
55
|
+
const results: Array<readonly [string, Array<ResolvedFacetValue>]> =
|
|
56
|
+
await Promise.all(
|
|
57
|
+
specs.map(
|
|
58
|
+
async (
|
|
59
|
+
spec: ResourceFacetSpec,
|
|
60
|
+
): Promise<readonly [string, Array<ResolvedFacetValue>]> => {
|
|
61
|
+
try {
|
|
62
|
+
const values: Array<ResolvedFacetValue> =
|
|
63
|
+
await ResourceFacetResolver.resolveOne(projectId, spec);
|
|
64
|
+
return [spec.facetKey, values] as const;
|
|
65
|
+
} catch {
|
|
66
|
+
return [spec.facetKey, []] as const;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return Object.fromEntries(results);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private static async resolveOne(
|
|
76
|
+
projectId: ObjectID,
|
|
77
|
+
spec: ResourceFacetSpec,
|
|
78
|
+
): Promise<Array<ResolvedFacetValue>> {
|
|
79
|
+
const limit: number = spec.limit ?? ResourceFacetResolver.DEFAULT_LIMIT;
|
|
80
|
+
const searchText: string | undefined =
|
|
81
|
+
spec.searchText && spec.searchText.trim().length > 0
|
|
82
|
+
? spec.searchText.trim()
|
|
83
|
+
: undefined;
|
|
84
|
+
|
|
85
|
+
switch (spec.facetKey) {
|
|
86
|
+
case "serviceId":
|
|
87
|
+
return ResourceFacetResolver.queryServices(
|
|
88
|
+
projectId,
|
|
89
|
+
spec.counts,
|
|
90
|
+
searchText,
|
|
91
|
+
limit,
|
|
92
|
+
);
|
|
93
|
+
case "hostId":
|
|
94
|
+
return ResourceFacetResolver.queryHosts(
|
|
95
|
+
projectId,
|
|
96
|
+
spec.counts,
|
|
97
|
+
searchText,
|
|
98
|
+
limit,
|
|
99
|
+
);
|
|
100
|
+
case "dockerHostId":
|
|
101
|
+
return ResourceFacetResolver.queryDockerHosts(
|
|
102
|
+
projectId,
|
|
103
|
+
spec.counts,
|
|
104
|
+
searchText,
|
|
105
|
+
limit,
|
|
106
|
+
);
|
|
107
|
+
case "kubernetesClusterId":
|
|
108
|
+
return ResourceFacetResolver.queryKubernetesClusters(
|
|
109
|
+
projectId,
|
|
110
|
+
spec.counts,
|
|
111
|
+
searchText,
|
|
112
|
+
limit,
|
|
113
|
+
);
|
|
114
|
+
default:
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private static async queryServices(
|
|
120
|
+
projectId: ObjectID,
|
|
121
|
+
counts: Map<string, number>,
|
|
122
|
+
searchText: string | undefined,
|
|
123
|
+
limit: number,
|
|
124
|
+
): Promise<Array<ResolvedFacetValue>> {
|
|
125
|
+
const query: Record<string, unknown> = { projectId };
|
|
126
|
+
if (searchText) {
|
|
127
|
+
query["name"] = new Search<string>(searchText);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const services: Array<ServiceModel> = await ServiceService.findBy({
|
|
131
|
+
query: query as any,
|
|
132
|
+
select: {
|
|
133
|
+
_id: true,
|
|
134
|
+
name: true,
|
|
135
|
+
},
|
|
136
|
+
limit: new PositiveNumber(limit),
|
|
137
|
+
skip: new PositiveNumber(0),
|
|
138
|
+
props: { isRoot: true },
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return ResourceFacetResolver.mergeCounts(
|
|
142
|
+
services.map((s: ServiceModel): { id: string; displayName: string } => {
|
|
143
|
+
return {
|
|
144
|
+
id: s._id ? s._id.toString() : "",
|
|
145
|
+
displayName: s.name || "Unknown",
|
|
146
|
+
};
|
|
147
|
+
}),
|
|
148
|
+
counts,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private static async queryHosts(
|
|
153
|
+
projectId: ObjectID,
|
|
154
|
+
counts: Map<string, number>,
|
|
155
|
+
searchText: string | undefined,
|
|
156
|
+
limit: number,
|
|
157
|
+
): Promise<Array<ResolvedFacetValue>> {
|
|
158
|
+
const query: Record<string, unknown> = { projectId };
|
|
159
|
+
if (searchText) {
|
|
160
|
+
query["name"] = new MultiSearch({
|
|
161
|
+
fields: ["name", "hostIdentifier"],
|
|
162
|
+
value: searchText,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const hosts: Array<HostModel> = await HostService.findBy({
|
|
167
|
+
query: query as any,
|
|
168
|
+
select: {
|
|
169
|
+
_id: true,
|
|
170
|
+
name: true,
|
|
171
|
+
hostIdentifier: true,
|
|
172
|
+
},
|
|
173
|
+
limit: new PositiveNumber(limit),
|
|
174
|
+
skip: new PositiveNumber(0),
|
|
175
|
+
props: { isRoot: true },
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return ResourceFacetResolver.mergeCounts(
|
|
179
|
+
hosts.map((h: HostModel): { id: string; displayName: string } => {
|
|
180
|
+
return {
|
|
181
|
+
id: h._id ? h._id.toString() : "",
|
|
182
|
+
displayName: h.name || h.hostIdentifier || "Unknown",
|
|
183
|
+
};
|
|
184
|
+
}),
|
|
185
|
+
counts,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private static async queryDockerHosts(
|
|
190
|
+
projectId: ObjectID,
|
|
191
|
+
counts: Map<string, number>,
|
|
192
|
+
searchText: string | undefined,
|
|
193
|
+
limit: number,
|
|
194
|
+
): Promise<Array<ResolvedFacetValue>> {
|
|
195
|
+
const query: Record<string, unknown> = { projectId };
|
|
196
|
+
if (searchText) {
|
|
197
|
+
query["name"] = new MultiSearch({
|
|
198
|
+
fields: ["name", "hostIdentifier"],
|
|
199
|
+
value: searchText,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const dockerHosts: Array<DockerHostModel> = await DockerHostService.findBy({
|
|
204
|
+
query: query as any,
|
|
205
|
+
select: {
|
|
206
|
+
_id: true,
|
|
207
|
+
name: true,
|
|
208
|
+
hostIdentifier: true,
|
|
209
|
+
},
|
|
210
|
+
limit: new PositiveNumber(limit),
|
|
211
|
+
skip: new PositiveNumber(0),
|
|
212
|
+
props: { isRoot: true },
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return ResourceFacetResolver.mergeCounts(
|
|
216
|
+
dockerHosts.map(
|
|
217
|
+
(d: DockerHostModel): { id: string; displayName: string } => {
|
|
218
|
+
return {
|
|
219
|
+
id: d._id ? d._id.toString() : "",
|
|
220
|
+
displayName: d.name || d.hostIdentifier || "Unknown",
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
),
|
|
224
|
+
counts,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private static async queryKubernetesClusters(
|
|
229
|
+
projectId: ObjectID,
|
|
230
|
+
counts: Map<string, number>,
|
|
231
|
+
searchText: string | undefined,
|
|
232
|
+
limit: number,
|
|
233
|
+
): Promise<Array<ResolvedFacetValue>> {
|
|
234
|
+
const query: Record<string, unknown> = { projectId };
|
|
235
|
+
if (searchText) {
|
|
236
|
+
query["name"] = new MultiSearch({
|
|
237
|
+
fields: ["name", "clusterIdentifier"],
|
|
238
|
+
value: searchText,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const clusters: Array<KubernetesClusterModel> =
|
|
243
|
+
await KubernetesClusterService.findBy({
|
|
244
|
+
query: query as any,
|
|
245
|
+
select: {
|
|
246
|
+
_id: true,
|
|
247
|
+
name: true,
|
|
248
|
+
clusterIdentifier: true,
|
|
249
|
+
},
|
|
250
|
+
limit: new PositiveNumber(limit),
|
|
251
|
+
skip: new PositiveNumber(0),
|
|
252
|
+
props: { isRoot: true },
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return ResourceFacetResolver.mergeCounts(
|
|
256
|
+
clusters.map(
|
|
257
|
+
(c: KubernetesClusterModel): { id: string; displayName: string } => {
|
|
258
|
+
return {
|
|
259
|
+
id: c._id ? c._id.toString() : "",
|
|
260
|
+
displayName: c.name || c.clusterIdentifier || "Unknown",
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
),
|
|
264
|
+
counts,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/*
|
|
269
|
+
* Combine Postgres-sourced entities with counts from the ClickHouse sample.
|
|
270
|
+
* Entities without a count default to 0 (they exist in the project but
|
|
271
|
+
* had no telemetry in the active window). Sorts active-first so the
|
|
272
|
+
* highest-traffic resources surface at the top of the sidebar.
|
|
273
|
+
*/
|
|
274
|
+
private static mergeCounts(
|
|
275
|
+
entities: Array<{ id: string; displayName: string }>,
|
|
276
|
+
counts: Map<string, number>,
|
|
277
|
+
): Array<ResolvedFacetValue> {
|
|
278
|
+
const out: Array<ResolvedFacetValue> = entities
|
|
279
|
+
.filter((e: { id: string }): boolean => {
|
|
280
|
+
return e.id.length > 0;
|
|
281
|
+
})
|
|
282
|
+
.map((e: { id: string; displayName: string }): ResolvedFacetValue => {
|
|
283
|
+
return {
|
|
284
|
+
value: e.id,
|
|
285
|
+
count: counts.get(e.id) || 0,
|
|
286
|
+
displayName: e.displayName,
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
out.sort((a: ResolvedFacetValue, b: ResolvedFacetValue): number => {
|
|
291
|
+
if (a.count !== b.count) {
|
|
292
|
+
return b.count - a.count;
|
|
293
|
+
}
|
|
294
|
+
return a.displayName.localeCompare(b.displayName);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -89,6 +89,15 @@ export interface ComponentProps {
|
|
|
89
89
|
facetLoading?: boolean;
|
|
90
90
|
onFacetInclude?: (facetKey: string, value: string) => void;
|
|
91
91
|
onFacetExclude?: (facetKey: string, value: string) => void;
|
|
92
|
+
/*
|
|
93
|
+
* Debounced search emit for resource facets (serviceId / hostId / etc.).
|
|
94
|
+
* Parent updates state and re-fetches facets so the result includes
|
|
95
|
+
* matching resources from the Postgres source-of-truth, not just those
|
|
96
|
+
* already loaded into the sidebar.
|
|
97
|
+
*/
|
|
98
|
+
onFacetSearchChange?:
|
|
99
|
+
| ((facetKey: string, searchText: string) => void)
|
|
100
|
+
| undefined;
|
|
92
101
|
showFacetSidebar?: boolean;
|
|
93
102
|
activeFilters?: Array<ActiveFilter> | undefined;
|
|
94
103
|
baseActiveFilters?: Array<ActiveFilter> | undefined;
|
|
@@ -1063,6 +1072,7 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
1063
1072
|
savedViews={props.savedViews}
|
|
1064
1073
|
selectedSavedViewId={props.selectedSavedViewId}
|
|
1065
1074
|
onSavedViewSelect={props.onSavedViewSelect}
|
|
1075
|
+
onFacetSearchChange={props.onFacetSearchChange}
|
|
1066
1076
|
/>
|
|
1067
1077
|
)}
|
|
1068
1078
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React, {
|
|
2
2
|
FunctionComponent,
|
|
3
3
|
ReactElement,
|
|
4
|
+
useEffect,
|
|
5
|
+
useRef,
|
|
4
6
|
useState,
|
|
5
7
|
useMemo,
|
|
6
8
|
} from "react";
|
|
@@ -19,10 +21,17 @@ export interface FacetSectionProps {
|
|
|
19
21
|
valueDisplayMap?: Record<string, string> | undefined;
|
|
20
22
|
valueColorMap?: Record<string, string> | undefined;
|
|
21
23
|
activeValues?: Set<string> | undefined;
|
|
24
|
+
/*
|
|
25
|
+
* When set, the search box also emits typed text to the parent (debounced)
|
|
26
|
+
* so it can refetch values from the backend. Client-side filtering still
|
|
27
|
+
* runs as defense-in-depth.
|
|
28
|
+
*/
|
|
29
|
+
onSearchChange?: ((text: string) => void) | undefined;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
const DEFAULT_VISIBLE_COUNT: number = 5;
|
|
25
33
|
const SEARCH_THRESHOLD: number = 6;
|
|
34
|
+
const SEARCH_DEBOUNCE_MS: number = 300;
|
|
26
35
|
|
|
27
36
|
const FacetSection: FunctionComponent<FacetSectionProps> = (
|
|
28
37
|
props: FacetSectionProps,
|
|
@@ -31,7 +40,31 @@ const FacetSection: FunctionComponent<FacetSectionProps> = (
|
|
|
31
40
|
const [showAll, setShowAll] = useState<boolean>(false);
|
|
32
41
|
const [searchText, setSearchText] = useState<string>("");
|
|
33
42
|
|
|
34
|
-
const showSearch: boolean =
|
|
43
|
+
const showSearch: boolean =
|
|
44
|
+
props.onSearchChange !== undefined ||
|
|
45
|
+
props.values.length >= SEARCH_THRESHOLD;
|
|
46
|
+
|
|
47
|
+
const onSearchChange: FacetSectionProps["onSearchChange"] =
|
|
48
|
+
props.onSearchChange;
|
|
49
|
+
const lastEmittedRef: React.MutableRefObject<string | null> = useRef<
|
|
50
|
+
string | null
|
|
51
|
+
>(null);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!onSearchChange) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const trimmed: string = searchText.trim();
|
|
57
|
+
if (lastEmittedRef.current === trimmed) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const handle: ReturnType<typeof setTimeout> = setTimeout(() => {
|
|
61
|
+
lastEmittedRef.current = trimmed;
|
|
62
|
+
onSearchChange(trimmed);
|
|
63
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
64
|
+
return () => {
|
|
65
|
+
clearTimeout(handle);
|
|
66
|
+
};
|
|
67
|
+
}, [searchText, onSearchChange]);
|
|
35
68
|
|
|
36
69
|
const filteredValues: Array<FacetValue> = useMemo(() => {
|
|
37
70
|
if (!searchText.trim()) {
|
|
@@ -40,7 +73,9 @@ const FacetSection: FunctionComponent<FacetSectionProps> = (
|
|
|
40
73
|
const query: string = searchText.toLowerCase().trim();
|
|
41
74
|
return props.values.filter((facet: FacetValue) => {
|
|
42
75
|
const displayName: string =
|
|
43
|
-
|
|
76
|
+
facet.displayName ??
|
|
77
|
+
props.valueDisplayMap?.[facet.value] ??
|
|
78
|
+
facet.value;
|
|
44
79
|
return displayName.toLowerCase().includes(query);
|
|
45
80
|
});
|
|
46
81
|
}, [props.values, props.valueDisplayMap, searchText]);
|
|
@@ -114,7 +149,9 @@ const FacetSection: FunctionComponent<FacetSectionProps> = (
|
|
|
114
149
|
<FacetValueRow
|
|
115
150
|
key={facet.value}
|
|
116
151
|
value={facet.value}
|
|
117
|
-
displayValue={
|
|
152
|
+
displayValue={
|
|
153
|
+
facet.displayName ?? props.valueDisplayMap?.[facet.value]
|
|
154
|
+
}
|
|
118
155
|
count={facet.count}
|
|
119
156
|
maxCount={maxCount}
|
|
120
157
|
color={props.valueColorMap?.[facet.value]}
|
|
@@ -28,8 +28,23 @@ export interface LogsFacetSidebarProps {
|
|
|
28
28
|
savedViews?: Array<LogsSavedViewOption> | undefined;
|
|
29
29
|
selectedSavedViewId?: string | null | undefined;
|
|
30
30
|
onSavedViewSelect?: ((viewId: string) => void) | undefined;
|
|
31
|
+
/*
|
|
32
|
+
* Called (debounced) when typing in a resource facet's search box. Lets
|
|
33
|
+
* the parent re-issue the facets request with the typed text scoped to
|
|
34
|
+
* that facet, so the result includes resources beyond the loaded subset.
|
|
35
|
+
*/
|
|
36
|
+
onFacetSearchChange?:
|
|
37
|
+
| ((facetKey: string, searchText: string) => void)
|
|
38
|
+
| undefined;
|
|
31
39
|
}
|
|
32
40
|
|
|
41
|
+
const RESOURCE_FACET_KEYS: ReadonlySet<string> = new Set([
|
|
42
|
+
"serviceId",
|
|
43
|
+
"hostId",
|
|
44
|
+
"dockerHostId",
|
|
45
|
+
"kubernetesClusterId",
|
|
46
|
+
]);
|
|
47
|
+
|
|
33
48
|
const SEVERITY_ORDER: Array<string> = [
|
|
34
49
|
LogSeverity.Fatal,
|
|
35
50
|
LogSeverity.Error,
|
|
@@ -278,6 +293,13 @@ const LogsFacetSidebar: FunctionComponent<LogsFacetSidebarProps> = (
|
|
|
278
293
|
valueColorMap = severityColorMap;
|
|
279
294
|
}
|
|
280
295
|
|
|
296
|
+
const onSearchChange: ((text: string) => void) | undefined =
|
|
297
|
+
RESOURCE_FACET_KEYS.has(key) && props.onFacetSearchChange
|
|
298
|
+
? (text: string) => {
|
|
299
|
+
props.onFacetSearchChange!(key, text);
|
|
300
|
+
}
|
|
301
|
+
: undefined;
|
|
302
|
+
|
|
281
303
|
return (
|
|
282
304
|
<FacetSection
|
|
283
305
|
key={key}
|
|
@@ -289,6 +311,7 @@ const LogsFacetSidebar: FunctionComponent<LogsFacetSidebarProps> = (
|
|
|
289
311
|
valueDisplayMap={valueDisplayMap}
|
|
290
312
|
valueColorMap={valueColorMap}
|
|
291
313
|
activeValues={activeValuesByKey[key]}
|
|
314
|
+
onSearchChange={onSearchChange}
|
|
292
315
|
/>
|
|
293
316
|
);
|
|
294
317
|
})}
|
|
@@ -13,6 +13,8 @@ export interface HistogramBucket {
|
|
|
13
13
|
export interface FacetValue {
|
|
14
14
|
value: string;
|
|
15
15
|
count: number;
|
|
16
|
+
// Server-resolved display name (e.g. serviceId → service name).
|
|
17
|
+
displayName?: string | undefined;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export type FacetData = Record<string, Array<FacetValue>>;
|
|
@@ -70,6 +70,13 @@ export interface TelemetryViewerProps<T> {
|
|
|
70
70
|
facetLoading?: boolean;
|
|
71
71
|
onFacetInclude?: ((facetKey: string, value: string) => void) | undefined;
|
|
72
72
|
onFacetExclude?: ((facetKey: string, value: string) => void) | undefined;
|
|
73
|
+
/*
|
|
74
|
+
* Called (debounced) when the user types in a server-searchable facet's
|
|
75
|
+
* search box. Parent typically updates state and refetches facetData.
|
|
76
|
+
*/
|
|
77
|
+
onFacetSearchChange?:
|
|
78
|
+
| ((facetKey: string, searchText: string) => void)
|
|
79
|
+
| undefined;
|
|
73
80
|
|
|
74
81
|
// -- Active filters --
|
|
75
82
|
activeFilters?: Array<ActiveFilter> | undefined;
|
|
@@ -219,6 +226,7 @@ function TelemetryViewerInner<T>(props: TelemetryViewerProps<T>): ReactElement {
|
|
|
219
226
|
onExcludeFilter={(key: string, value: string) => {
|
|
220
227
|
props.onFacetExclude?.(key, value);
|
|
221
228
|
}}
|
|
229
|
+
onFacetSearchChange={props.onFacetSearchChange}
|
|
222
230
|
/>
|
|
223
231
|
)}
|
|
224
232
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React, {
|
|
2
2
|
FunctionComponent,
|
|
3
3
|
ReactElement,
|
|
4
|
+
useEffect,
|
|
5
|
+
useRef,
|
|
4
6
|
useState,
|
|
5
7
|
useMemo,
|
|
6
8
|
} from "react";
|
|
@@ -20,10 +22,20 @@ export interface TelemetryFacetSectionProps {
|
|
|
20
22
|
valueColorMap?: Record<string, string> | undefined;
|
|
21
23
|
activeValues?: Set<string> | undefined;
|
|
22
24
|
defaultExpanded?: boolean;
|
|
25
|
+
/*
|
|
26
|
+
* When set, the search box also emits typed text to the parent (debounced)
|
|
27
|
+
* so it can refetch values from the backend. Client-side filtering still
|
|
28
|
+
* runs as defense-in-depth — the server response replaces props.values
|
|
29
|
+
* which then flows through the local filter.
|
|
30
|
+
*/
|
|
31
|
+
onSearchChange?: ((text: string) => void) | undefined;
|
|
32
|
+
// Force the search input to render even when below the local item threshold.
|
|
33
|
+
alwaysShowSearch?: boolean;
|
|
23
34
|
}
|
|
24
35
|
|
|
25
36
|
const DEFAULT_VISIBLE_COUNT: number = 5;
|
|
26
37
|
const SEARCH_THRESHOLD: number = 6;
|
|
38
|
+
const SEARCH_DEBOUNCE_MS: number = 300;
|
|
27
39
|
|
|
28
40
|
const TelemetryFacetSection: FunctionComponent<TelemetryFacetSectionProps> = (
|
|
29
41
|
props: TelemetryFacetSectionProps,
|
|
@@ -34,7 +46,37 @@ const TelemetryFacetSection: FunctionComponent<TelemetryFacetSectionProps> = (
|
|
|
34
46
|
const [showAll, setShowAll] = useState<boolean>(false);
|
|
35
47
|
const [searchText, setSearchText] = useState<string>("");
|
|
36
48
|
|
|
37
|
-
const showSearch: boolean =
|
|
49
|
+
const showSearch: boolean =
|
|
50
|
+
props.alwaysShowSearch === true ||
|
|
51
|
+
props.onSearchChange !== undefined ||
|
|
52
|
+
props.values.length >= SEARCH_THRESHOLD;
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
* Debounce the upward search emit so each keystroke doesn't fire a
|
|
56
|
+
* round-trip. Local filter still applies immediately for fast feedback
|
|
57
|
+
* against the currently loaded set.
|
|
58
|
+
*/
|
|
59
|
+
const onSearchChange: TelemetryFacetSectionProps["onSearchChange"] =
|
|
60
|
+
props.onSearchChange;
|
|
61
|
+
const lastEmittedRef: React.MutableRefObject<string | null> = useRef<
|
|
62
|
+
string | null
|
|
63
|
+
>(null);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!onSearchChange) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const trimmed: string = searchText.trim();
|
|
69
|
+
if (lastEmittedRef.current === trimmed) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const handle: ReturnType<typeof setTimeout> = setTimeout(() => {
|
|
73
|
+
lastEmittedRef.current = trimmed;
|
|
74
|
+
onSearchChange(trimmed);
|
|
75
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
76
|
+
return () => {
|
|
77
|
+
clearTimeout(handle);
|
|
78
|
+
};
|
|
79
|
+
}, [searchText, onSearchChange]);
|
|
38
80
|
|
|
39
81
|
const filteredValues: Array<FacetValue> = useMemo(() => {
|
|
40
82
|
if (!searchText.trim()) {
|
|
@@ -43,7 +85,9 @@ const TelemetryFacetSection: FunctionComponent<TelemetryFacetSectionProps> = (
|
|
|
43
85
|
const query: string = searchText.toLowerCase().trim();
|
|
44
86
|
return props.values.filter((facet: FacetValue) => {
|
|
45
87
|
const displayName: string =
|
|
46
|
-
|
|
88
|
+
facet.displayName ??
|
|
89
|
+
props.valueDisplayMap?.[facet.value] ??
|
|
90
|
+
facet.value;
|
|
47
91
|
return displayName.toLowerCase().includes(query);
|
|
48
92
|
});
|
|
49
93
|
}, [props.values, props.valueDisplayMap, searchText]);
|
|
@@ -117,7 +161,9 @@ const TelemetryFacetSection: FunctionComponent<TelemetryFacetSectionProps> = (
|
|
|
117
161
|
<TelemetryFacetValueRow
|
|
118
162
|
key={facet.value}
|
|
119
163
|
value={facet.value}
|
|
120
|
-
displayValue={
|
|
164
|
+
displayValue={
|
|
165
|
+
facet.displayName ?? props.valueDisplayMap?.[facet.value]
|
|
166
|
+
}
|
|
121
167
|
count={facet.count}
|
|
122
168
|
maxCount={maxCount}
|
|
123
169
|
color={props.valueColorMap?.[facet.value]}
|
|
@@ -12,6 +12,14 @@ export interface TelemetryFacetSidebarProps {
|
|
|
12
12
|
onExcludeFilter: (facetKey: string, value: string) => void;
|
|
13
13
|
activeFilters?: Array<ActiveFilter> | undefined;
|
|
14
14
|
headerLabel?: string | undefined;
|
|
15
|
+
/*
|
|
16
|
+
* Called (debounced) when a search input changes for a facet whose config
|
|
17
|
+
* has serverSearchable=true. Lets the parent re-issue a facets request
|
|
18
|
+
* with the typed text scoped to that facet.
|
|
19
|
+
*/
|
|
20
|
+
onFacetSearchChange?:
|
|
21
|
+
| ((facetKey: string, searchText: string) => void)
|
|
22
|
+
| undefined;
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
const TelemetryFacetSidebar: FunctionComponent<TelemetryFacetSidebarProps> = (
|
|
@@ -63,6 +71,13 @@ const TelemetryFacetSidebar: FunctionComponent<TelemetryFacetSidebarProps> = (
|
|
|
63
71
|
{orderedConfigs.map((config: FacetConfig) => {
|
|
64
72
|
const values: Array<FacetValue> = props.facetData[config.key] || [];
|
|
65
73
|
|
|
74
|
+
const onSearchChange: ((text: string) => void) | undefined =
|
|
75
|
+
config.serverSearchable && props.onFacetSearchChange
|
|
76
|
+
? (text: string) => {
|
|
77
|
+
props.onFacetSearchChange!(config.key, text);
|
|
78
|
+
}
|
|
79
|
+
: undefined;
|
|
80
|
+
|
|
66
81
|
return (
|
|
67
82
|
<TelemetryFacetSection
|
|
68
83
|
key={config.key}
|
|
@@ -74,6 +89,7 @@ const TelemetryFacetSidebar: FunctionComponent<TelemetryFacetSidebarProps> = (
|
|
|
74
89
|
valueDisplayMap={config.valueDisplayMap}
|
|
75
90
|
valueColorMap={config.valueColorMap}
|
|
76
91
|
activeValues={activeValuesByKey[config.key]}
|
|
92
|
+
onSearchChange={onSearchChange}
|
|
77
93
|
/>
|
|
78
94
|
);
|
|
79
95
|
})}
|
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
export interface FacetValue {
|
|
7
7
|
value: string;
|
|
8
8
|
count: number;
|
|
9
|
+
/*
|
|
10
|
+
* Optional server-resolved display name. Set by the backend when the facet
|
|
11
|
+
* value is resolved against a source-of-truth table (e.g. serviceId →
|
|
12
|
+
* service name). Falls back to the parent's valueDisplayMap if not present.
|
|
13
|
+
*/
|
|
14
|
+
displayName?: string | undefined;
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
export type FacetData = Record<string, Array<FacetValue>>;
|
|
@@ -52,6 +58,12 @@ export interface FacetConfig {
|
|
|
52
58
|
valueColorMap?: Record<string, string> | undefined;
|
|
53
59
|
// Ordering priority: lower = shown first.
|
|
54
60
|
priority?: number | undefined;
|
|
61
|
+
/*
|
|
62
|
+
* When true, the section's search box also emits typed text to the
|
|
63
|
+
* sidebar's `onFacetSearchChange` callback so the parent can refetch
|
|
64
|
+
* values from the backend (used for resource facets backed by Postgres).
|
|
65
|
+
*/
|
|
66
|
+
serverSearchable?: boolean | undefined;
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
export interface SearchHelpRow {
|
|
@@ -1264,6 +1264,17 @@ TelemetryException = __decorate([
|
|
|
1264
1264
|
name: "TelemetryException",
|
|
1265
1265
|
}),
|
|
1266
1266
|
Index(["projectId", "isResolved", "isArchived"]) // Exceptions dashboard counts/filters
|
|
1267
|
+
/*
|
|
1268
|
+
* Composite uniqueness on the dedup key used by the OTel traces ingest
|
|
1269
|
+
* batched upsert. The ingest path collapses every exception event in a
|
|
1270
|
+
* worker batch into a single INSERT … ON CONFLICT (projectId,
|
|
1271
|
+
* serviceId, fingerprint) DO UPDATE statement; this index is what makes
|
|
1272
|
+
* that conflict target resolvable and stops two concurrent workers from
|
|
1273
|
+
* racing the old findOneBy + update path into duplicate rows or lost
|
|
1274
|
+
* occuranceCount increments.
|
|
1275
|
+
*/
|
|
1276
|
+
,
|
|
1277
|
+
Index(["projectId", "serviceId", "fingerprint"], { unique: true })
|
|
1267
1278
|
], TelemetryException);
|
|
1268
1279
|
export default TelemetryException;
|
|
1269
1280
|
//# sourceMappingURL=TelemetryException.js.map
|