@oneuptime/common 10.5.1 → 10.5.3
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/StatusPageGroup.ts +212 -0
- package/Models/DatabaseModels/StatusPageResource.ts +86 -0
- package/Models/DatabaseModels/TelemetryException.ts +10 -0
- package/Server/API/StatusPageAPI.ts +15 -0
- package/Server/API/TelemetryAPI.ts +406 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1779879993421-MigrationName.ts +68 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1779882573463-MigrationName.ts +65 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +5 -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/StatusPageService.ts +5 -0
- package/Server/Services/TraceAggregationService.ts +1 -0
- package/Server/Utils/Monitor/MonitorLogUtil.ts +146 -6
- package/Server/Utils/Telemetry/ResourceFacetResolver.ts +299 -0
- package/Types/Monitor/MonitorStep.ts +85 -0
- package/Types/StatusPage/StatusPageGroupViewMode.ts +6 -0
- package/UI/Components/Accordion/Accordion.tsx +32 -26
- 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/StatusPageGroup.js +217 -0
- package/build/dist/Models/DatabaseModels/StatusPageGroup.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageResource.js +88 -0
- package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryException.js +11 -0
- package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +15 -0
- package/build/dist/Server/API/StatusPageAPI.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 +42 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779879993421-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779882573463-MigrationName.js +28 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779882573463-MigrationName.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/StatusPageService.js +5 -0
- package/build/dist/Server/Services/StatusPageService.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/Types/Monitor/MonitorStep.js +59 -0
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/StatusPage/StatusPageGroupViewMode.js +7 -0
- package/build/dist/Types/StatusPage/StatusPageGroupViewMode.js.map +1 -0
- package/build/dist/UI/Components/Accordion/Accordion.js +11 -11
- package/build/dist/UI/Components/Accordion/Accordion.js.map +1 -1
- 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
|
+
}
|
|
@@ -52,6 +52,39 @@ import MonitorStepDockerMonitor, {
|
|
|
52
52
|
} from "./MonitorStepDockerMonitor";
|
|
53
53
|
import Zod, { ZodSchema } from "../../Utils/Schema/Zod";
|
|
54
54
|
|
|
55
|
+
/*
|
|
56
|
+
* Caps and defaults for per-step request timeout and retry settings.
|
|
57
|
+
* Users may lower these via the UI; values higher than the cap are clamped.
|
|
58
|
+
*/
|
|
59
|
+
export const MAX_MONITOR_REQUEST_TIMEOUT_IN_MS: number = 60000; // 60 seconds
|
|
60
|
+
export const DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS: number = 60000;
|
|
61
|
+
export const MAX_MONITOR_RETRY_COUNT: number = 3;
|
|
62
|
+
export const DEFAULT_MONITOR_RETRY_COUNT: number = 3;
|
|
63
|
+
|
|
64
|
+
export const clampMonitorRequestTimeoutInMs: (value: number) => number = (
|
|
65
|
+
value: number,
|
|
66
|
+
): number => {
|
|
67
|
+
if (!value || value <= 0) {
|
|
68
|
+
return DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS;
|
|
69
|
+
}
|
|
70
|
+
if (value > MAX_MONITOR_REQUEST_TIMEOUT_IN_MS) {
|
|
71
|
+
return MAX_MONITOR_REQUEST_TIMEOUT_IN_MS;
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const clampMonitorRetryCount: (value: number) => number = (
|
|
77
|
+
value: number,
|
|
78
|
+
): number => {
|
|
79
|
+
if (value === undefined || value === null || isNaN(value) || value < 0) {
|
|
80
|
+
return DEFAULT_MONITOR_RETRY_COUNT;
|
|
81
|
+
}
|
|
82
|
+
if (value > MAX_MONITOR_RETRY_COUNT) {
|
|
83
|
+
return MAX_MONITOR_RETRY_COUNT;
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
};
|
|
87
|
+
|
|
55
88
|
export interface MonitorStepType {
|
|
56
89
|
id: string;
|
|
57
90
|
monitorDestination?: URL | IP | Hostname | undefined;
|
|
@@ -88,6 +121,19 @@ export interface MonitorStepType {
|
|
|
88
121
|
// retry count for synthetic monitors - number of times to retry on error
|
|
89
122
|
retryCountOnError?: number | undefined;
|
|
90
123
|
|
|
124
|
+
/*
|
|
125
|
+
* Per-step request timeout in milliseconds for probe-based monitors
|
|
126
|
+
* (Website, API, Ping, IP, Port, SSLCertificate). Defaults to and is
|
|
127
|
+
* capped at 60000 ms (60 seconds).
|
|
128
|
+
*/
|
|
129
|
+
requestTimeoutInMs?: number | undefined;
|
|
130
|
+
|
|
131
|
+
/*
|
|
132
|
+
* Per-step retry count for probe-based monitors when a check fails.
|
|
133
|
+
* Defaults to and is capped at 3.
|
|
134
|
+
*/
|
|
135
|
+
retryCount?: number | undefined;
|
|
136
|
+
|
|
91
137
|
// Log monitor type.
|
|
92
138
|
logMonitor?: MonitorStepLogMonitor | undefined;
|
|
93
139
|
|
|
@@ -148,6 +194,8 @@ export default class MonitorStep extends DatabaseProperty {
|
|
|
148
194
|
screenSizeTypes: undefined,
|
|
149
195
|
browserTypes: undefined,
|
|
150
196
|
retryCountOnError: undefined,
|
|
197
|
+
requestTimeoutInMs: undefined,
|
|
198
|
+
retryCount: undefined,
|
|
151
199
|
logMonitor: undefined,
|
|
152
200
|
traceMonitor: undefined,
|
|
153
201
|
metricMonitor: undefined,
|
|
@@ -190,6 +238,8 @@ export default class MonitorStep extends DatabaseProperty {
|
|
|
190
238
|
screenSizeTypes: undefined,
|
|
191
239
|
browserTypes: undefined,
|
|
192
240
|
retryCountOnError: undefined,
|
|
241
|
+
requestTimeoutInMs: undefined,
|
|
242
|
+
retryCount: undefined,
|
|
193
243
|
logMonitor: undefined,
|
|
194
244
|
traceMonitor: undefined,
|
|
195
245
|
metricMonitor: undefined,
|
|
@@ -294,6 +344,27 @@ export default class MonitorStep extends DatabaseProperty {
|
|
|
294
344
|
return this;
|
|
295
345
|
}
|
|
296
346
|
|
|
347
|
+
public setRequestTimeoutInMs(
|
|
348
|
+
requestTimeoutInMs: number | undefined,
|
|
349
|
+
): MonitorStep {
|
|
350
|
+
if (requestTimeoutInMs === undefined) {
|
|
351
|
+
this.data!.requestTimeoutInMs = undefined;
|
|
352
|
+
return this;
|
|
353
|
+
}
|
|
354
|
+
this.data!.requestTimeoutInMs =
|
|
355
|
+
clampMonitorRequestTimeoutInMs(requestTimeoutInMs);
|
|
356
|
+
return this;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
public setRetryCount(retryCount: number | undefined): MonitorStep {
|
|
360
|
+
if (retryCount === undefined) {
|
|
361
|
+
this.data!.retryCount = undefined;
|
|
362
|
+
return this;
|
|
363
|
+
}
|
|
364
|
+
this.data!.retryCount = clampMonitorRetryCount(retryCount);
|
|
365
|
+
return this;
|
|
366
|
+
}
|
|
367
|
+
|
|
297
368
|
public setLogMonitor(logMonitor: MonitorStepLogMonitor): MonitorStep {
|
|
298
369
|
this.data!.logMonitor = logMonitor;
|
|
299
370
|
return this;
|
|
@@ -400,6 +471,8 @@ export default class MonitorStep extends DatabaseProperty {
|
|
|
400
471
|
screenSizeTypes: undefined,
|
|
401
472
|
browserTypes: undefined,
|
|
402
473
|
retryCountOnError: undefined,
|
|
474
|
+
requestTimeoutInMs: undefined,
|
|
475
|
+
retryCount: undefined,
|
|
403
476
|
logMonitor: undefined,
|
|
404
477
|
exceptionMonitor: undefined,
|
|
405
478
|
kubernetesMonitor: undefined,
|
|
@@ -597,6 +670,11 @@ export default class MonitorStep extends DatabaseProperty {
|
|
|
597
670
|
screenSizeTypes: this.data.screenSizeTypes || undefined,
|
|
598
671
|
browserTypes: this.data.browserTypes || undefined,
|
|
599
672
|
retryCountOnError: this.data.retryCountOnError || undefined,
|
|
673
|
+
requestTimeoutInMs: this.data.requestTimeoutInMs || undefined,
|
|
674
|
+
retryCount:
|
|
675
|
+
this.data.retryCount === undefined
|
|
676
|
+
? undefined
|
|
677
|
+
: this.data.retryCount,
|
|
600
678
|
logMonitor: this.data.logMonitor
|
|
601
679
|
? MonitorStepLogMonitorUtil.toJSON(
|
|
602
680
|
this.data.logMonitor || MonitorStepLogMonitorUtil.getDefault(),
|
|
@@ -745,6 +823,11 @@ export default class MonitorStep extends DatabaseProperty {
|
|
|
745
823
|
(json["screenSizeTypes"] as Array<ScreenSizeType>) || undefined,
|
|
746
824
|
browserTypes: (json["browserTypes"] as Array<BrowserType>) || undefined,
|
|
747
825
|
retryCountOnError: (json["retryCountOnError"] as number) || undefined,
|
|
826
|
+
requestTimeoutInMs: (json["requestTimeoutInMs"] as number) || undefined,
|
|
827
|
+
retryCount:
|
|
828
|
+
json["retryCount"] === undefined || json["retryCount"] === null
|
|
829
|
+
? undefined
|
|
830
|
+
: (json["retryCount"] as number),
|
|
748
831
|
logMonitor: json["logMonitor"]
|
|
749
832
|
? (json["logMonitor"] as JSONObject)
|
|
750
833
|
: undefined,
|
|
@@ -806,6 +889,8 @@ export default class MonitorStep extends DatabaseProperty {
|
|
|
806
889
|
screenSizeTypes: Zod.any().optional(),
|
|
807
890
|
browserTypes: Zod.any().optional(),
|
|
808
891
|
retryCountOnError: Zod.number().optional(),
|
|
892
|
+
requestTimeoutInMs: Zod.number().optional(),
|
|
893
|
+
retryCount: Zod.number().optional(),
|
|
809
894
|
logMonitor: Zod.any().optional(),
|
|
810
895
|
traceMonitor: Zod.any().optional(),
|
|
811
896
|
metricMonitor: Zod.any().optional(),
|
|
@@ -75,7 +75,9 @@ const Accordion: FunctionComponent<ComponentProps> = (
|
|
|
75
75
|
<div className={className}>
|
|
76
76
|
<div>
|
|
77
77
|
<div
|
|
78
|
-
className={`flex justify-between cursor-pointer
|
|
78
|
+
className={`flex justify-between items-start gap-3 cursor-pointer group/accordion-header rounded-lg -mx-2 px-2 py-2 transition-colors ${
|
|
79
|
+
isOpen ? "" : "hover:bg-gray-50/80"
|
|
80
|
+
}`}
|
|
79
81
|
role="button"
|
|
80
82
|
tabIndex={0}
|
|
81
83
|
aria-expanded={isOpen}
|
|
@@ -85,43 +87,47 @@ const Accordion: FunctionComponent<ComponentProps> = (
|
|
|
85
87
|
}}
|
|
86
88
|
onKeyDown={handleKeyDown}
|
|
87
89
|
>
|
|
88
|
-
<div className="flex">
|
|
90
|
+
<div className="flex items-start min-w-0 flex-1">
|
|
89
91
|
{props.title && (
|
|
90
|
-
<div
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
92
|
+
<div
|
|
93
|
+
className={`flex-shrink-0 mt-0.5 flex items-center justify-center w-6 h-6 rounded-md transition-all duration-200 ${
|
|
94
|
+
isOpen
|
|
95
|
+
? "bg-gray-900/5 text-gray-700"
|
|
96
|
+
: "text-gray-400 group-hover/accordion-header:bg-gray-900/5 group-hover/accordion-header:text-gray-700"
|
|
97
|
+
}`}
|
|
98
|
+
aria-hidden="true"
|
|
99
|
+
>
|
|
100
|
+
<Icon
|
|
101
|
+
className={`h-3.5 w-3.5 transition-transform duration-200 ease-out ${
|
|
102
|
+
isOpen ? "rotate-90" : ""
|
|
103
|
+
}`}
|
|
104
|
+
icon={IconProp.ChevronRight}
|
|
105
|
+
thick={ThickProp.Thick}
|
|
106
|
+
/>
|
|
105
107
|
</div>
|
|
106
108
|
)}
|
|
107
109
|
{props.title && (
|
|
108
110
|
<div
|
|
109
|
-
className={`ml-
|
|
111
|
+
className={`ml-2.5 min-w-0 flex-1 ${
|
|
110
112
|
props.onClick ? "cursor-pointer" : ""
|
|
111
113
|
}`}
|
|
112
114
|
>
|
|
113
|
-
<div
|
|
114
|
-
{props.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
{props.description && (
|
|
118
|
-
<MarkdownViewer text={props.description || ""} />
|
|
119
|
-
)}
|
|
115
|
+
<div
|
|
116
|
+
className={`text-gray-900 leading-snug ${props.titleClassName || ""}`}
|
|
117
|
+
>
|
|
118
|
+
{props.title}
|
|
120
119
|
</div>
|
|
120
|
+
{props.description && (
|
|
121
|
+
<div className="mt-1 text-sm text-gray-500 leading-relaxed">
|
|
122
|
+
<MarkdownViewer text={props.description} />
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
121
125
|
</div>
|
|
122
126
|
)}
|
|
123
127
|
</div>
|
|
124
|
-
{!isOpen &&
|
|
128
|
+
{!isOpen && props.rightElement && (
|
|
129
|
+
<div className="flex-shrink-0 mt-0.5">{props.rightElement}</div>
|
|
130
|
+
)}
|
|
125
131
|
</div>
|
|
126
132
|
{isOpen && (
|
|
127
133
|
<div
|
|
@@ -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>>;
|