From 23d5fde879cf00037c38bcdea1063e3d236e00dd Mon Sep 17 00:00:00 2001 From: Aaron Knudtson <87577305+knudtty@users.noreply.github.com> Date: Mon, 22 Sep 2025 16:11:38 -0400 Subject: [PATCH 1/6] unify source types and remove ambiguity --- .../__tests__/renderChartConfig.test.ts | 6 +- packages/api/src/controllers/sources.ts | 42 +++- packages/api/src/models/source.ts | 182 +++++++++++------- .../src/routers/api/__tests__/sources.test.ts | 12 +- packages/api/src/routers/api/sources.ts | 27 ++- .../external-api/__tests__/charts.test.ts | 21 +- .../src/tasks/__tests__/checkAlerts.test.ts | 23 ++- .../__tests__/singleInvocationAlert.test.ts | 10 +- packages/api/src/tasks/checkAlerts.ts | 14 +- .../tasks/providers/__tests__/default.test.ts | 18 +- packages/api/src/tasks/template.ts | 12 +- packages/app/src/ChartUtils.tsx | 17 +- packages/app/src/DBDashboardPage.tsx | 33 ++-- packages/app/src/DBSearchPage.tsx | 162 +++++++++------- packages/app/src/KubernetesDashboardPage.tsx | 41 ++-- .../app/src/NamespaceDetailsSidePanel.tsx | 13 +- packages/app/src/NodeDetailsSidePanel.tsx | 14 +- packages/app/src/PodDetailsSidePanel.tsx | 13 +- packages/app/src/ServicesDashboardPage.tsx | 47 ++--- packages/app/src/SessionSidePanel.tsx | 7 +- packages/app/src/SessionSubpanel.tsx | 7 +- packages/app/src/SessionsPage.tsx | 2 + .../src/__tests__/serviceDashboard.test.ts | 8 +- packages/app/src/__tests__/utils.test.ts | 12 +- .../app/src/components/AlertPreviewChart.tsx | 11 +- .../app/src/components/ContextSidePanel.tsx | 14 +- .../src/components/DBEditTimeChartForm.tsx | 48 +++-- packages/app/src/components/DBInfraPanel.tsx | 15 +- .../app/src/components/DBRowDataPanel.tsx | 24 ++- .../app/src/components/DBRowOverviewPanel.tsx | 7 +- .../app/src/components/DBRowSidePanel.tsx | 24 ++- packages/app/src/components/DBRowTable.tsx | 18 +- .../app/src/components/DBSessionPanel.tsx | 35 ++-- packages/app/src/components/DBTimeChart.tsx | 12 +- packages/app/src/components/DBTracePanel.tsx | 20 +- .../src/components/DBTraceWaterfallChart.tsx | 116 +++++------ .../app/src/components/ExpandableRowTable.tsx | 8 +- .../app/src/components/KubeComponents.tsx | 8 +- .../app/src/components/KubernetesFilters.tsx | 6 +- .../app/src/components/MetricNameSelect.tsx | 8 +- .../app/src/components/OnboardingModal.tsx | 27 +-- .../app/src/components/PatternSidePanel.tsx | 4 +- packages/app/src/components/PatternTable.tsx | 9 +- .../ServiceDashboardDbQuerySidePanel.tsx | 11 +- ...rviceDashboardEndpointPerformanceChart.tsx | 4 +- .../ServiceDashboardEndpointSidePanel.tsx | 4 +- .../ServiceDashboardSlowestEventsTile.tsx | 32 +-- packages/app/src/components/SourceForm.tsx | 59 +++--- .../__tests__/DBTraceWaterfallChart.test.tsx | 20 +- packages/app/src/serviceDashboard.ts | 13 +- packages/app/src/sessions.ts | 3 +- packages/app/src/source.ts | 99 +++++++--- packages/app/src/utils.ts | 4 +- packages/common-utils/src/types.ts | 104 +++------- 54 files changed, 890 insertions(+), 620 deletions(-) diff --git a/packages/api/src/clickhouse/__tests__/renderChartConfig.test.ts b/packages/api/src/clickhouse/__tests__/renderChartConfig.test.ts index 27fd4212d..139bdf646 100644 --- a/packages/api/src/clickhouse/__tests__/renderChartConfig.test.ts +++ b/packages/api/src/clickhouse/__tests__/renderChartConfig.test.ts @@ -26,7 +26,7 @@ import { getServer, } from '@/fixtures'; import Connection from '@/models/connection'; -import { Source } from '@/models/source'; +import { LogSource, MetricSource } from '@/models/source'; const TEST_METRIC_TABLES = { sum: DEFAULT_METRICS_TABLE.SUM, @@ -77,7 +77,7 @@ describe('renderChartConfig', () => { username: config.CLICKHOUSE_USER, password: config.CLICKHOUSE_PASSWORD, }); - logSource = await Source.create({ + logSource = await LogSource.create({ kind: 'log', team: team._id, from: { @@ -88,7 +88,7 @@ describe('renderChartConfig', () => { connection: connection.id, name: 'Logs', }); - metricSource = await Source.create({ + metricSource = await MetricSource.create({ kind: 'metric', team: team._id, from: { diff --git a/packages/api/src/controllers/sources.ts b/packages/api/src/controllers/sources.ts index 791031603..bdc7d5b64 100644 --- a/packages/api/src/controllers/sources.ts +++ b/packages/api/src/controllers/sources.ts @@ -1,4 +1,12 @@ -import { ISource, Source } from '@/models/source'; +import { SourceKind } from '@/../../common-utils/dist/types'; +import { + ISource, + LogSource, + MetricSource, + SessionSource, + Source, + TraceSource, +} from '@/models/source'; export function getSources(team: string) { return Source.find({ team }); @@ -9,7 +17,16 @@ export function getSource(team: string, sourceId: string) { } export function createSource(team: string, source: Omit) { - return Source.create({ ...source, team }); + switch (source.kind) { + case SourceKind.Log: + return LogSource.create({ ...source, team }); + case SourceKind.Trace: + return TraceSource.create({ ...source, team }); + case SourceKind.Metric: + return MetricSource.create({ ...source, team }); + case SourceKind.Session: + return SessionSource.create({ ...source, team }); + } } export function updateSource( @@ -17,9 +34,24 @@ export function updateSource( sourceId: string, source: Omit, ) { - return Source.findOneAndUpdate({ _id: sourceId, team }, source, { - new: true, - }); + switch (source.kind) { + case SourceKind.Log: + return LogSource.findOneAndUpdate({ _id: sourceId, team }, source, { + new: true, + }); + case SourceKind.Trace: + return TraceSource.findOneAndUpdate({ _id: sourceId, team }, source, { + new: true, + }); + case SourceKind.Metric: + return MetricSource.findOneAndUpdate({ _id: sourceId, team }, source, { + new: true, + }); + case SourceKind.Session: + return SessionSource.findOneAndUpdate({ _id: sourceId, team }, source, { + new: true, + }); + } } export function deleteSource(team: string, sourceId: string) { diff --git a/packages/api/src/models/source.ts b/packages/api/src/models/source.ts index 078e63b37..1eac266d1 100644 --- a/packages/api/src/models/source.ts +++ b/packages/api/src/models/source.ts @@ -1,86 +1,132 @@ import { + LogSourceSchema, MetricsDataType, + MetricSourceSchema, + SessionSourceSchema, SourceKind, + TraceSourceSchema, TSource, } from '@hyperdx/common-utils/dist/types'; import mongoose, { Schema } from 'mongoose'; +import { z } from 'zod'; -type ObjectId = mongoose.Types.ObjectId; - -export interface ISource extends Omit { - team: ObjectId; - connection: ObjectId | string; -} +import { objectIdSchema } from '@/utils/zod'; +const sourceExtension = { + team: objectIdSchema.or(z.instanceof(mongoose.Types.ObjectId)), + connection: objectIdSchema.or(z.instanceof(mongoose.Types.ObjectId)), +}; +const SourceModelSchema = z.discriminatedUnion('kind', [ + LogSourceSchema.extend(sourceExtension), + TraceSourceSchema.extend(sourceExtension), + SessionSourceSchema.extend(sourceExtension), + MetricSourceSchema.extend(sourceExtension), +]); +export type ISource = z.infer; export type SourceDocument = mongoose.HydratedDocument; export const Source = mongoose.model( 'Source', - new Schema( - { - kind: { - type: String, - enum: Object.values(SourceKind), - required: true, - }, - team: { - type: mongoose.Schema.Types.ObjectId, - required: true, - ref: 'Team', - }, - from: { - databaseName: String, - tableName: String, - }, - timestampValueExpression: String, - connection: { - type: mongoose.Schema.Types.ObjectId, - required: true, - ref: 'Connection', - }, - - name: String, - displayedTimestampValueExpression: String, - implicitColumnExpression: String, - serviceNameExpression: String, - bodyExpression: String, - tableFilterExpression: String, - eventAttributesExpression: String, - resourceAttributesExpression: String, - defaultTableSelectExpression: String, - uniqueRowIdExpression: String, - severityTextExpression: String, - traceIdExpression: String, - spanIdExpression: String, - traceSourceId: String, - sessionSourceId: String, - metricSourceId: String, + new Schema({ + name: String, + kind: { + type: String, + enum: Object.values(SourceKind), + required: true, + }, + from: { + databaseName: String, + tableName: String, + }, + team: { + type: mongoose.Schema.Types.ObjectId, + required: true, + ref: 'Team', + }, + connection: { + type: mongoose.Schema.Types.ObjectId, + required: true, + ref: 'Connection', + }, + }), +); - durationExpression: String, - durationPrecision: Number, - parentSpanIdExpression: String, - spanNameExpression: String, +export const LogSource = Source.discriminator< + Extract +>( + SourceKind.Log, + new mongoose.Schema>({ + timestampValueExpression: String, + defaultTableSelectExpression: String, + serviceNameExpression: String, + severityTextExpression: String, + bodyExpression: String, + eventAttributesExpression: String, + resourceAttributesExpression: String, + displayedTimestampValueExpression: String, + metricSourceId: String, + traceSourceId: String, + traceIdExpression: String, + spanIdExpression: String, + implicitColumnExpression: String, + uniqueRowIdExpression: String, + tableFilterExpression: String, + }), +); - logSourceId: String, - spanKindExpression: String, - statusCodeExpression: String, - statusMessageExpression: String, - spanEventsValueExpression: String, +export const TraceSource = Source.discriminator< + Extract +>( + SourceKind.Trace, + new mongoose.Schema>({ + defaultTableSelectExpression: String, + timestampValueExpression: String, + durationExpression: String, + durationPrecision: Number, + traceIdExpression: String, + spanIdExpression: String, + parentSpanIdExpression: String, + spanNameExpression: String, + spanKindExpression: String, + logSourceId: String, + sessionSourceId: String, + metricSourceId: String, + statusCodeExpression: String, + statusMessageExpression: String, + serviceNameExpression: String, + resourceAttributesExpression: String, + eventAttributesExpression: String, + spanEventsValueExpression: String, + implicitColumnExpression: String, + }), +); - metricTables: { - type: { - [MetricsDataType.Gauge]: String, - [MetricsDataType.Histogram]: String, - [MetricsDataType.Sum]: String, - [MetricsDataType.Summary]: String, - [MetricsDataType.ExponentialHistogram]: String, - }, - default: undefined, +export const MetricSource = Source.discriminator< + Extract +>( + SourceKind.Metric, + new mongoose.Schema>({ + metricTables: { + type: { + [MetricsDataType.Gauge]: String, + [MetricsDataType.Histogram]: String, + [MetricsDataType.Sum]: String, + [MetricsDataType.Summary]: String, + [MetricsDataType.ExponentialHistogram]: String, }, + default: undefined, }, - { - toJSON: { virtuals: true }, - timestamps: true, - }, - ), + timestampValueExpression: String, + resourceAttributesExpression: String, + logSourceId: String, + }), +); + +export const SessionSource = Source.discriminator< + Extract +>( + SourceKind.Session, + new mongoose.Schema>({ + traceSourceId: String, + }), ); diff --git a/packages/api/src/routers/api/__tests__/sources.test.ts b/packages/api/src/routers/api/__tests__/sources.test.ts index 871aa6558..7e62c1f9a 100644 --- a/packages/api/src/routers/api/__tests__/sources.test.ts +++ b/packages/api/src/routers/api/__tests__/sources.test.ts @@ -1,10 +1,10 @@ -import { SourceKind, TSourceUnion } from '@hyperdx/common-utils/dist/types'; +import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types'; import { Types } from 'mongoose'; import { getLoggedInAgent, getServer } from '@/fixtures'; -import { Source } from '@/models/source'; +import { LogSource, Source } from '@/models/source'; -const MOCK_SOURCE: Omit, 'id'> = { +const MOCK_SOURCE: Omit, 'id'> = { kind: SourceKind.Log, name: 'Test Source', connection: new Types.ObjectId().toString(), @@ -35,7 +35,7 @@ describe('sources router', () => { const { agent, team } = await getLoggedInAgent(server); // Create test source - await Source.create({ + await LogSource.create({ ...MOCK_SOURCE, team: team._id, }); @@ -93,7 +93,7 @@ describe('sources router', () => { const { agent, team } = await getLoggedInAgent(server); // Create test source - const source = await Source.create({ + const source = await LogSource.create({ ...MOCK_SOURCE, team: team._id, }); @@ -129,7 +129,7 @@ describe('sources router', () => { const { agent, team } = await getLoggedInAgent(server); // Create test source - const source = await Source.create({ + const source = await LogSource.create({ ...MOCK_SOURCE, team: team._id, }); diff --git a/packages/api/src/routers/api/sources.ts b/packages/api/src/routers/api/sources.ts index 840a1d97c..bc68b8216 100644 --- a/packages/api/src/routers/api/sources.ts +++ b/packages/api/src/routers/api/sources.ts @@ -1,6 +1,7 @@ import { + SourceKind, SourceSchema, - sourceSchemaWithout, + SourceSchemaNoId, } from '@hyperdx/common-utils/dist/types'; import express from 'express'; import { z } from 'zod'; @@ -23,14 +24,26 @@ router.get('/', async (req, res, next) => { const sources = await getSources(teamId.toString()); - return res.json(sources.map(s => s.toJSON({ getters: true }))); + const out = sources.map(s => { + // Typescript gets confused about calling toJSON on the union type, but + // breaking it out into a switch statement keeps TS happy + switch (s.kind) { + case SourceKind.Log: + return s.toJSON({ getters: true }); + case SourceKind.Trace: + return s.toJSON({ getters: true }); + case SourceKind.Metric: + return s.toJSON({ getters: true }); + case SourceKind.Session: + return s.toJSON({ getters: true }); + } + }); + return res.json(out); } catch (e) { next(e); } }); -const SourceSchemaNoId = sourceSchemaWithout({ id: true }); - router.post( '/', validateRequest({ @@ -40,11 +53,10 @@ router.post( try { const { teamId } = getNonNullUserWithTeam(req); - // TODO: HDX-1768 Eliminate type assertion const source = await createSource(teamId.toString(), { ...req.body, team: teamId, - } as any); + }); res.json(source); } catch (e) { @@ -65,11 +77,10 @@ router.put( try { const { teamId } = getNonNullUserWithTeam(req); - // TODO: HDX-1768 Eliminate type assertion const source = await updateSource(teamId.toString(), req.params.id, { ...req.body, team: teamId, - } as any); + }); if (!source) { res.status(404).send('Source not found'); diff --git a/packages/api/src/routers/external-api/__tests__/charts.test.ts b/packages/api/src/routers/external-api/__tests__/charts.test.ts index f996a1e8e..3b4684772 100644 --- a/packages/api/src/routers/external-api/__tests__/charts.test.ts +++ b/packages/api/src/routers/external-api/__tests__/charts.test.ts @@ -1,5 +1,8 @@ -import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; -import { SourceKind } from '@hyperdx/common-utils/dist/types'; +import { + SourceKind, + type TLogSource, + type TMetricSource, +} from '@hyperdx/common-utils/dist/types'; import { MetricsDataType } from '@hyperdx/common-utils/dist/types'; import { ObjectId } from 'mongodb'; import request from 'supertest'; @@ -16,7 +19,7 @@ import { getServer, } from '../../../fixtures'; import Connection from '../../../models/connection'; -import { ISource, Source } from '../../../models/source'; +import { LogSource, MetricSource } from '../../../models/source'; // Default time range for tests (1 hour) const DEFAULT_END_TIME = Date.now(); @@ -77,8 +80,8 @@ describe('External API v2 Charts', () => { let team: any; let user: any; let connection: any; - let logSource: ISource; - let metricSource: ISource; + let logSource: TLogSource; + let metricSource: TMetricSource; beforeAll(async () => { await server.start(); @@ -98,19 +101,19 @@ describe('External API v2 Charts', () => { password: config.CLICKHOUSE_PASSWORD, }); - logSource = await Source.create({ - kind: SourceKind.Log, + logSource = await LogSource.create({ team: team._id, + name: 'Logs', + kind: SourceKind.Log, from: { databaseName: DEFAULT_DATABASE, tableName: DEFAULT_LOGS_TABLE, }, timestampValueExpression: 'Timestamp', connection: connection._id, - name: 'Logs', }); - metricSource = await Source.create({ + metricSource = await MetricSource.create({ kind: SourceKind.Metric, team: team._id, from: { diff --git a/packages/api/src/tasks/__tests__/checkAlerts.test.ts b/packages/api/src/tasks/__tests__/checkAlerts.test.ts index d32c05aa8..6303f627c 100644 --- a/packages/api/src/tasks/__tests__/checkAlerts.test.ts +++ b/packages/api/src/tasks/__tests__/checkAlerts.test.ts @@ -1,6 +1,5 @@ import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; -import { AlertState } from '@hyperdx/common-utils/dist/types'; -import { create } from 'lodash'; +import { AlertState, SourceKind } from '@hyperdx/common-utils/dist/types'; import mongoose from 'mongoose'; import ms from 'ms'; @@ -20,7 +19,7 @@ import AlertHistory from '@/models/alertHistory'; import Connection from '@/models/connection'; import Dashboard from '@/models/dashboard'; import { SavedSearch } from '@/models/savedSearch'; -import { Source } from '@/models/source'; +import { LogSource, MetricSource } from '@/models/source'; import Webhook from '@/models/webhook'; import * as checkAlert from '@/tasks/checkAlerts'; import { @@ -28,7 +27,7 @@ import { getPreviousAlertHistories, processAlert, } from '@/tasks/checkAlerts'; -import { AlertDetails, AlertTaskType, loadProvider } from '@/tasks/providers'; +import { AlertTaskType, loadProvider } from '@/tasks/providers'; import { AlertMessageTemplateDefaultView, buildAlertMessageTemplateHdxLink, @@ -134,9 +133,9 @@ describe('checkAlerts', () => { interval: '1m', }, source: { - id: 'fake-source-id' as any, - kind: 'log' as any, - team: 'team-123' as any, + id: 'fake-source-id', + kind: SourceKind.Log, + team: 'team-123', from: { databaseName: 'default', tableName: 'otel_logs', @@ -144,7 +143,7 @@ describe('checkAlerts', () => { timestampValueExpression: 'Timestamp', connection: 'connection-123' as any, name: 'Logs', - }, + } as any, savedSearch: { _id: 'fake-saved-search-id' as any, team: 'team-123' as any, @@ -680,7 +679,7 @@ describe('checkAlerts', () => { username: config.CLICKHOUSE_USER, password: config.CLICKHOUSE_PASSWORD, }); - const source = await Source.create({ + const source = await LogSource.create({ kind: 'log', team: team._id, from: { @@ -913,7 +912,7 @@ describe('checkAlerts', () => { username: config.CLICKHOUSE_USER, password: config.CLICKHOUSE_PASSWORD, }); - const source = await Source.create({ + const source = await LogSource.create({ kind: 'log', team: team._id, from: { @@ -1153,7 +1152,7 @@ describe('checkAlerts', () => { username: config.CLICKHOUSE_USER, password: config.CLICKHOUSE_PASSWORD, }); - const source = await Source.create({ + const source = await LogSource.create({ kind: 'log', team: team._id, from: { @@ -1369,7 +1368,7 @@ describe('checkAlerts', () => { username: config.CLICKHOUSE_USER, password: config.CLICKHOUSE_PASSWORD, }); - const source = await Source.create({ + const source = await MetricSource.create({ kind: 'metric', team: team._id, from: { diff --git a/packages/api/src/tasks/__tests__/singleInvocationAlert.test.ts b/packages/api/src/tasks/__tests__/singleInvocationAlert.test.ts index e88501690..90423270c 100644 --- a/packages/api/src/tasks/__tests__/singleInvocationAlert.test.ts +++ b/packages/api/src/tasks/__tests__/singleInvocationAlert.test.ts @@ -12,14 +12,10 @@ import AlertHistory from '@/models/alertHistory'; import Connection from '@/models/connection'; import Dashboard from '@/models/dashboard'; import { SavedSearch } from '@/models/savedSearch'; -import { Source } from '@/models/source'; +import { LogSource } from '@/models/source'; import Webhook from '@/models/webhook'; import { getPreviousAlertHistories, processAlert } from '@/tasks/checkAlerts'; import { AlertTaskType, loadProvider } from '@/tasks/providers'; -import { - AlertMessageTemplateDefaultView, - buildAlertMessageTemplateTitle, -} from '@/tasks/template'; import * as slack from '@/utils/slack'; describe('Single Invocation Alert Test', () => { @@ -104,7 +100,7 @@ describe('Single Invocation Alert Test', () => { }); // Create source - const source = await Source.create({ + const source = await LogSource.create({ kind: 'log', team: team._id, from: { @@ -293,7 +289,7 @@ describe('Single Invocation Alert Test', () => { }); // Create source - const source = await Source.create({ + const source = await LogSource.create({ kind: 'log', team: team._id, from: { diff --git a/packages/api/src/tasks/checkAlerts.ts b/packages/api/src/tasks/checkAlerts.ts index 65c7928b4..ee46407eb 100644 --- a/packages/api/src/tasks/checkAlerts.ts +++ b/packages/api/src/tasks/checkAlerts.ts @@ -8,6 +8,7 @@ import { getMetadata, Metadata } from '@hyperdx/common-utils/dist/metadata'; import { ChartConfigWithOptDateRange, DisplayType, + SourceKind, } from '@hyperdx/common-utils/dist/types'; import * as fns from 'date-fns'; import { chunk, isString } from 'lodash'; @@ -190,7 +191,10 @@ export const processAlert = async ( where: savedSearch.where, whereLanguage: savedSearch.whereLanguage, groupBy: alert.groupBy, - implicitColumnExpression: source.implicitColumnExpression, + implicitColumnExpression: + source.kind === SourceKind.Trace || source.kind === SourceKind.Log + ? source.implicitColumnExpression + : undefined, timestampValueExpression: source.timestampValueExpression, }; } else if (details.taskType === AlertTaskType.TILE) { @@ -206,8 +210,12 @@ export const processAlert = async ( from: source.from, granularity: `${windowSizeInMins} minute`, groupBy: tile.config.groupBy, - implicitColumnExpression: source.implicitColumnExpression, - metricTables: source.metricTables, + implicitColumnExpression: + source.kind === SourceKind.Trace || source.kind === SourceKind.Log + ? source.implicitColumnExpression + : undefined, + metricTables: + source.kind === SourceKind.Metric ? source.metricTables : undefined, select: tile.config.select, timestampValueExpression: source.timestampValueExpression, where: tile.config.where, diff --git a/packages/api/src/tasks/providers/__tests__/default.test.ts b/packages/api/src/tasks/providers/__tests__/default.test.ts index 30b3f1dcd..63ec3653a 100644 --- a/packages/api/src/tasks/providers/__tests__/default.test.ts +++ b/packages/api/src/tasks/providers/__tests__/default.test.ts @@ -7,7 +7,7 @@ import Alert, { AlertSource, AlertThresholdType } from '@/models/alert'; import Connection from '@/models/connection'; import Dashboard from '@/models/dashboard'; import { SavedSearch } from '@/models/savedSearch'; -import { Source } from '@/models/source'; +import { LogSource } from '@/models/source'; import { AlertProvider, AlertTaskType, @@ -57,7 +57,7 @@ describe('DefaultAlertProvider', () => { }); // Create source - const source = await Source.create({ + const source = await LogSource.create({ team: team._id, name: 'Test Source', kind: 'log', @@ -125,7 +125,7 @@ describe('DefaultAlertProvider', () => { }); // Create source - const source = await Source.create({ + const source = await LogSource.create({ team: team._id, name: 'Test Source', kind: 'log', @@ -250,7 +250,7 @@ describe('DefaultAlertProvider', () => { }); // Create source - const source = await Source.create({ + const source = await LogSource.create({ team: team._id, name: 'Test Source', kind: 'log', @@ -348,7 +348,7 @@ describe('DefaultAlertProvider', () => { }); // Create sources for each connection - const source1 = await Source.create({ + const source1 = await LogSource.create({ team: team._id, name: 'Source 1', kind: 'log', @@ -360,7 +360,7 @@ describe('DefaultAlertProvider', () => { connection: connection1._id, }); - const source2 = await Source.create({ + const source2 = await LogSource.create({ team: team._id, name: 'Source 2', kind: 'log', @@ -525,7 +525,7 @@ describe('DefaultAlertProvider', () => { const team = await createTeam({ name: 'Test Team' }); // Create source with non-existent connection - const source = await Source.create({ + const source = await LogSource.create({ team: team._id, name: 'Test Source', kind: 'log', @@ -599,7 +599,7 @@ describe('DefaultAlertProvider', () => { }); // Create source - const source = await Source.create({ + const source = await LogSource.create({ team: team._id, name: 'Test Source', kind: 'log', @@ -661,7 +661,7 @@ describe('DefaultAlertProvider', () => { }); // Create source - const source = await Source.create({ + const source = await LogSource.create({ team: team._id, name: 'Test Source', kind: 'log', diff --git a/packages/api/src/tasks/template.ts b/packages/api/src/tasks/template.ts index ae61d92d0..5ac5e2fe3 100644 --- a/packages/api/src/tasks/template.ts +++ b/packages/api/src/tasks/template.ts @@ -5,6 +5,7 @@ import { renderChartConfig } from '@hyperdx/common-utils/dist/renderChartConfig' import { ChartConfigWithOptDateRange, DisplayType, + SourceKind, WebhookService, } from '@hyperdx/common-utils/dist/types'; import { _useTry, formatDate } from '@hyperdx/common-utils/dist/utils'; @@ -452,10 +453,17 @@ export const renderAlertTemplate = async ({ displayType: DisplayType.Search, dateRange: [startTime, endTime], from: source.from, - select: savedSearch.select || source.defaultTableSelectExpression || '', // remove alert body if there is no select and defaultTableSelectExpression + select: + savedSearch.select || + ('defaultTableSelectExpression' in source && + source.defaultTableSelectExpression) || + '', // remove alert body if there is no select and defaultTableSelectExpression where: savedSearch.where, whereLanguage: savedSearch.whereLanguage, - implicitColumnExpression: source.implicitColumnExpression, + implicitColumnExpression: + source.kind === SourceKind.Trace || source.kind === SourceKind.Log + ? source.implicitColumnExpression + : undefined, timestampValueExpression: source.timestampValueExpression, orderBy: savedSearch.orderBy, limit: { diff --git a/packages/app/src/ChartUtils.tsx b/packages/app/src/ChartUtils.tsx index e6da9e970..abbfeb9b1 100644 --- a/packages/app/src/ChartUtils.tsx +++ b/packages/app/src/ChartUtils.tsx @@ -16,7 +16,10 @@ import { SavedChartConfig, SourceKind, SQLInterval, - TSource, + type TLogSource, + type TMetricSource, + type TSource, + type TTraceSource, } from '@hyperdx/common-utils/dist/types'; import { SegmentedControl, Select as MSelect } from '@mantine/core'; @@ -518,7 +521,9 @@ export function formatResponseForTimeChart({ groupColumns[0].name === (source.kind === SourceKind.Log ? source.severityTextExpression - : source.statusCodeExpression) + : source.kind === SourceKind.Trace + ? source.statusCodeExpression + : undefined) ) { color = logLevelColor(row[groupColumns[0].name]); } @@ -646,7 +651,7 @@ export const mapV1AggFnToV2 = (aggFn?: AggFn): AggFnV2 | undefined => { }; export const convertV1GroupByToV2 = ( - metricSource: TSource, + metricSource: TMetricSource, groupBy: string[], ): string => { return groupBy @@ -672,9 +677,9 @@ export const convertV1ChartConfigToV2 = ( sortOrder?: SortOrder; }, source: { - log?: TSource; - metric?: TSource; - trace?: TSource; + log?: TLogSource; + metric?: TMetricSource; + trace?: TTraceSource; }, ): ChartConfigWithDateRange => { const { diff --git a/packages/app/src/DBDashboardPage.tsx b/packages/app/src/DBDashboardPage.tsx index 7c501fa6d..f3f0ba2da 100644 --- a/packages/app/src/DBDashboardPage.tsx +++ b/packages/app/src/DBDashboardPage.tsx @@ -18,7 +18,7 @@ import RGL, { WidthProvider } from 'react-grid-layout'; import { Controller, useForm } from 'react-hook-form'; import { useHotkeys } from 'react-hotkeys-hook'; import { TableConnection } from '@hyperdx/common-utils/dist/metadata'; -import { AlertState } from '@hyperdx/common-utils/dist/types'; +import { AlertState, SourceKind } from '@hyperdx/common-utils/dist/types'; import { ChartConfigWithDateRange, DisplayType, @@ -182,14 +182,18 @@ const Tile = forwardRef( connection: source.connection, dateRange, granularity, - timestampValueExpression: source.timestampValueExpression, + timestampValueExpression: source.timestampValueExpression ?? '', from: { databaseName: source.from?.databaseName || 'default', tableName: tableName || '', }, - implicitColumnExpression: source.implicitColumnExpression, + ...('implicitColumnExpression' in source + ? { implicitColumnExpression: source.implicitColumnExpression } + : {}), filters, - metricTables: source.metricTables, + ...('metricTables' in source + ? { metricTables: source.metricTables } + : {}), }); } } @@ -362,7 +366,9 @@ const Tile = forwardRef( dateRange, select: queriedConfig.select || - source?.defaultTableSelectExpression || + (source && 'defaultTableSelectExpression' in source + ? source.defaultTableSelectExpression + : undefined) || '', groupBy: undefined, granularity: undefined, @@ -1063,13 +1069,16 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) { > + Add New Tile - {rowId && rowSidePanelSource && ( - - )} + {rowId && + rowSidePanelSource && + (rowSidePanelSource.kind === SourceKind.Log || + rowSidePanelSource.kind === SourceKind.Trace) && ( + + )} ); } diff --git a/packages/app/src/DBSearchPage.tsx b/packages/app/src/DBSearchPage.tsx index e0e51cc27..02dd6c385 100644 --- a/packages/app/src/DBSearchPage.tsx +++ b/packages/app/src/DBSearchPage.tsx @@ -137,6 +137,8 @@ const SearchConfigSchema = z.object({ type SearchConfigFromSchema = z.infer; +const ALLOWED_SOURCE_KINDS = [SourceKind.Log, SourceKind.Trace]; + // Helper function to get the default source id export function getDefaultSourceId( sources: { id: string }[] | undefined, @@ -477,9 +479,16 @@ function useSearchedConfigToChartConfig({ filters, orderBy, }: SearchConfig) { - const { data: sourceObj, isLoading } = useSource({ + const { data: logSource, isLoading: isLogSourceLoading } = useSource({ + id: source, + kind: SourceKind.Log, + }); + const { data: traceSource, isLoading: isTraceSourceLoading } = useSource({ id: source, + kind: SourceKind.Trace, }); + const sourceObj = logSource ?? traceSource; + const isLoading = isLogSourceLoading || isTraceSourceLoading; const defaultOrderBy = useDefaultOrderBy(source); return useMemo(() => { @@ -488,7 +497,8 @@ function useSearchedConfigToChartConfig({ data: { select: select || (sourceObj.defaultTableSelectExpression ?? ''), from: sourceObj.from, - ...(sourceObj.tableFilterExpression != null + ...('tableFilterExpression' in sourceObj && + sourceObj.tableFilterExpression != null ? { filters: [ { @@ -611,9 +621,15 @@ function DBSearchPage() { 'hdx-last-selected-source-id', '', ); - const { data: searchedSource } = useSource({ + const { data: logSearchedSource } = useSource({ + id: searchedConfig.source, + kind: SourceKind.Log, + }); + const { data: traceSearchedSource } = useSource({ id: searchedConfig.source, + kind: SourceKind.Trace, }); + const searchedSource = logSearchedSource ?? traceSearchedSource; const [analysisMode, setAnalysisMode] = useQueryState( 'mode', @@ -685,7 +701,15 @@ function DBSearchPage() { const inputSource = watch('source'); // const { data: inputSourceObj } = useSource({ id: inputSource }); const { data: inputSourceObjs } = useSources(); - const inputSourceObj = inputSourceObjs?.find(s => s.id === inputSource); + const { data: logSource } = useSource({ + id: inputSource, + kind: SourceKind.Log, + }); + const { data: traceSource } = useSource({ + id: inputSource, + kind: SourceKind.Trace, + }); + const inputSourceObj = logSource ?? traceSource; const defaultOrderBy = useDefaultOrderBy(inputSource); const [rowId, setRowId] = useQueryState('rowWhere'); @@ -843,10 +867,12 @@ function DBSearchPage() { // Save the selected source ID to localStorage setLastSelectedSourceId(newInputSourceObj.id); - setValue( - 'select', - newInputSourceObj?.defaultTableSelectExpression ?? '', - ); + if (newInputSourceObj.kind === SourceKind.Log) { + setValue( + 'select', + newInputSourceObj.defaultTableSelectExpression ?? '', + ); + } // Clear all search filters searchFilters.clearAllFilters(); } @@ -1221,7 +1247,7 @@ function DBSearchPage() { control={control} name="source" onCreate={openNewSourceModal} - allowedSourceKinds={[SourceKind.Log, SourceKind.Trace]} + allowedSourceKinds={ALLOWED_SOURCE_KINDS} /> @@ -1516,7 +1542,10 @@ function DBSearchPage() { setAnalysisMode={setAnalysisMode} chartConfig={filtersChartConfig} sourceId={inputSourceObj?.id} - showDelta={!!searchedSource?.durationExpression} + showDelta={ + searchedSource?.kind === SourceKind.Trace && + !!searchedSource?.durationExpression + } {...searchFilters} /> @@ -1568,8 +1597,9 @@ function DBSearchPage() { dateRange: searchedTimeRange, }} bodyValueExpression={ - searchedSource?.bodyExpression ?? - chartConfig.implicitColumnExpression ?? + ((searchedSource?.kind === SourceKind.Log && + searchedSource?.bodyExpression) || + chartConfig.implicitColumnExpression) ?? '' } totalCountConfig={histogramTimeChartConfig} @@ -1577,59 +1607,61 @@ function DBSearchPage() { /> )} - {analysisMode === 'delta' && searchedSource != null && ( - -
- { - setOutlierSqlCondition( - [ - `${searchedSource.durationExpression} >= ${yMin} * 1e${(searchedSource.durationPrecision ?? 9) - 3}`, - `${searchedSource.durationExpression} <= ${yMax} * 1e${(searchedSource.durationPrecision ?? 9) - 3}`, - `${getFirstTimestampValueExpression(chartConfig.timestampValueExpression)} >= ${xMin}`, - `${getFirstTimestampValueExpression(chartConfig.timestampValueExpression)} <= ${xMax}`, - ].join(' AND '), - ); - }} - /> -
- {outlierSqlCondition ? ( - - ) : ( - -
- - Please highlight an outlier range in the heatmap to - view the delta chart. - -
-
- )} -
- )} + {analysisMode === 'delta' && + searchedSource != null && + searchedSource.kind === SourceKind.Trace && ( + +
+ { + setOutlierSqlCondition( + [ + `${searchedSource.durationExpression} >= ${yMin} * 1e${(searchedSource.durationPrecision ?? 9) - 3}`, + `${searchedSource.durationExpression} <= ${yMax} * 1e${(searchedSource.durationPrecision ?? 9) - 3}`, + `${getFirstTimestampValueExpression(chartConfig.timestampValueExpression)} >= ${xMin}`, + `${getFirstTimestampValueExpression(chartConfig.timestampValueExpression)} <= ${xMax}`, + ].join(' AND '), + ); + }} + /> +
+ {outlierSqlCondition ? ( + + ) : ( + +
+ + Please highlight an outlier range in the heatmap to + view the delta chart. + +
+
+ )} +
+ )}
{analysisMode === 'results' && chartConfig && diff --git a/packages/app/src/KubernetesDashboardPage.tsx b/packages/app/src/KubernetesDashboardPage.tsx index 7eac5bc8d..ece1c402f 100644 --- a/packages/app/src/KubernetesDashboardPage.tsx +++ b/packages/app/src/KubernetesDashboardPage.tsx @@ -13,9 +13,8 @@ import { import { useForm } from 'react-hook-form'; import { StringParam, useQueryParam, withDefault } from 'use-query-params'; import { - SearchConditionLanguage, SourceKind, - TSource, + type TMetricSource, } from '@hyperdx/common-utils/dist/types'; import { Anchor, @@ -146,7 +145,7 @@ export const InfraPodsStatusTable = ({ where, }: { dateRange: [Date, Date]; - metricSource: TSource; + metricSource: TMetricSource; where: string; }) => { const [phaseFilter, setPhaseFilter] = React.useState('running'); @@ -439,7 +438,7 @@ const NodesTable = ({ where, dateRange, }: { - metricSource: TSource; + metricSource: TMetricSource; where: string; dateRange: [Date, Date]; }) => { @@ -608,7 +607,7 @@ const NamespacesTable = ({ where, }: { dateRange: [Date, Date]; - metricSource: TSource; + metricSource: TMetricSource; where: string; }) => { const groupBy = ['k8s.namespace.name']; @@ -772,13 +771,14 @@ function KubernetesDashboardPage() { const connection = _connection ?? connections?.[0]?.id ?? ''; // TODO: Let users select log + metric sources - const { data: sources, isLoading: isLoadingSources } = useSources(); - const logSource = sources?.find( - s => s.kind === SourceKind.Log && s.connection === connection, - ); - const metricSource = sources?.find( - s => s.kind === SourceKind.Metric && s.connection === connection, - ); + const { data: logSource } = useSource({ + connection: connection, + kind: SourceKind.Log, + }); + const { data: metricSource } = useSource({ + connection: connection, + kind: SourceKind.Metric, + }); const { control, watch } = useForm({ values: { @@ -869,13 +869,16 @@ function KubernetesDashboardPage() { logSource={logSource} /> )} - {rowId && rowSidePanelSource && ( - - )} + {rowId && + rowSidePanelSource && + (rowSidePanelSource.kind === SourceKind.Log || + rowSidePanelSource.kind === SourceKind.Trace) && ( + + )} diff --git a/packages/app/src/NamespaceDetailsSidePanel.tsx b/packages/app/src/NamespaceDetailsSidePanel.tsx index ace7e0184..63a4b69d2 100644 --- a/packages/app/src/NamespaceDetailsSidePanel.tsx +++ b/packages/app/src/NamespaceDetailsSidePanel.tsx @@ -3,7 +3,10 @@ import Link from 'next/link'; import Drawer from 'react-modern-drawer'; import { StringParam, useQueryParam, withDefault } from 'use-query-params'; import { tcFromSource } from '@hyperdx/common-utils/dist/metadata'; -import { TSource } from '@hyperdx/common-utils/dist/types'; +import type { + TLogSource, + TMetricSource, +} from '@hyperdx/common-utils/dist/types'; import { Anchor, Badge, @@ -61,7 +64,7 @@ const NamespaceDetails = ({ }: { name: string; dateRange: [Date, Date]; - metricSource?: TSource; + metricSource?: TMetricSource; }) => { const where = `${metricSource?.resourceAttributesExpression}.k8s.namespace.name:"${name}"`; const groupBy = ['k8s.namespace.name']; @@ -141,7 +144,7 @@ function NamespaceLogs({ where, }: { dateRange: [Date, Date]; - logSource: TSource; + logSource: TLogSource; where: string; }) { const [resultType, setResultType] = React.useState<'all' | 'error'>('all'); @@ -230,8 +233,8 @@ export default function NamespaceDetailsSidePanel({ metricSource, logSource, }: { - metricSource: TSource; - logSource: TSource; + metricSource: TMetricSource; + logSource: TLogSource; }) { const [namespaceName, setNamespaceName] = useQueryParam( 'namespaceName', diff --git a/packages/app/src/NodeDetailsSidePanel.tsx b/packages/app/src/NodeDetailsSidePanel.tsx index 9fa2288a3..f3876f522 100644 --- a/packages/app/src/NodeDetailsSidePanel.tsx +++ b/packages/app/src/NodeDetailsSidePanel.tsx @@ -3,9 +3,9 @@ import Link from 'next/link'; import Drawer from 'react-modern-drawer'; import { StringParam, useQueryParam, withDefault } from 'use-query-params'; import { tcFromSource } from '@hyperdx/common-utils/dist/metadata'; -import { - SearchConditionLanguage, - TSource, +import type { + TLogSource, + TMetricSource, } from '@hyperdx/common-utils/dist/types'; import { Anchor, @@ -65,7 +65,7 @@ const NodeDetails = ({ }: { name: string; dateRange: [Date, Date]; - metricSource: TSource; + metricSource: TMetricSource; }) => { const where = `${metricSource.resourceAttributesExpression}.k8s.node.name:"${name}"`; const groupBy = ['k8s.node.name']; @@ -160,7 +160,7 @@ function NodeLogs({ where, }: { dateRange: [Date, Date]; - logSource: TSource; + logSource: TLogSource; where: string; }) { const [resultType, setResultType] = React.useState<'all' | 'error'>('all'); @@ -249,8 +249,8 @@ export default function NodeDetailsSidePanel({ metricSource, logSource, }: { - metricSource: TSource; - logSource: TSource; + metricSource: TMetricSource; + logSource: TLogSource; }) { const [nodeName, setNodeName] = useQueryParam( 'nodeName', diff --git a/packages/app/src/PodDetailsSidePanel.tsx b/packages/app/src/PodDetailsSidePanel.tsx index 60eac8a6a..c2a134f0a 100644 --- a/packages/app/src/PodDetailsSidePanel.tsx +++ b/packages/app/src/PodDetailsSidePanel.tsx @@ -3,7 +3,10 @@ import Link from 'next/link'; import Drawer from 'react-modern-drawer'; import { StringParam, useQueryParam, withDefault } from 'use-query-params'; import { tcFromSource } from '@hyperdx/common-utils/dist/metadata'; -import { TSource } from '@hyperdx/common-utils/dist/types'; +import type { + TLogSource, + TMetricSource, +} from '@hyperdx/common-utils/dist/types'; import { Anchor, Box, @@ -59,7 +62,7 @@ const PodDetails = ({ podName, }: { dateRange: [Date, Date]; - logSource: TSource; + logSource: TLogSource; podName: string; }) => { const { data: logsData } = useV2LogBatch<{ @@ -137,7 +140,7 @@ function PodLogs({ onRowClick, }: { dateRange: [Date, Date]; - logSource: TSource; + logSource: TLogSource; where: string; rowId: string | null; onRowClick: (rowId: string) => void; @@ -224,8 +227,8 @@ export default function PodDetailsSidePanel({ logSource, metricSource, }: { - logSource: TSource; - metricSource: TSource; + logSource: TLogSource; + metricSource: TMetricSource; }) { const [podName, setPodName] = useQueryParam( 'podName', diff --git a/packages/app/src/ServicesDashboardPage.tsx b/packages/app/src/ServicesDashboardPage.tsx index d8f262b28..8556ac655 100644 --- a/packages/app/src/ServicesDashboardPage.tsx +++ b/packages/app/src/ServicesDashboardPage.tsx @@ -12,7 +12,7 @@ import { DisplayType, Filter, SourceKind, - TSource, + type TTraceSource, } from '@hyperdx/common-utils/dist/types'; import { Box, @@ -59,7 +59,7 @@ type AppliedConfig = { }; function getScopedFilters( - source: TSource, + source: TTraceSource, appliedConfig: AppliedConfig, includeIsSpanKindServer = true, ): Filter[] { @@ -90,12 +90,8 @@ function ServiceSelectControlled({ size?: string; onCreate?: () => void; } & UseControllerProps) { - const { data: source } = useSource({ id: sourceId }); - const { data: jsonColumns = [] } = useJsonColumns({ - databaseName: source?.from?.databaseName || '', - tableName: source?.from?.tableName || '', - connectionId: source?.connection || '', - }); + const { data: source } = useSource({ id: sourceId, kind: SourceKind.Trace }); + const { data: jsonColumns = [] } = useJsonColumns(tcFromSource(source)); const expressions = getExpressions(source, jsonColumns); const queriedConfig = { @@ -154,16 +150,12 @@ export function EndpointLatencyChart({ appliedConfig = {}, extraFilters = [], }: { - source: TSource; + source: TTraceSource; dateRange: [Date, Date]; appliedConfig?: AppliedConfig; extraFilters?: Filter[]; }) { - const { data: jsonColumns = [] } = useJsonColumns({ - databaseName: source?.from?.databaseName || '', - tableName: source?.from?.tableName || '', - connectionId: source?.connection || '', - }); + const { data: jsonColumns = [] } = useJsonColumns(tcFromSource(source)); const expressions = getExpressions(source, jsonColumns); const [latencyChartType, setLatencyChartType] = useState< 'line' | 'histogram' @@ -269,12 +261,11 @@ function HttpTab({ searchedTimeRange: [Date, Date]; appliedConfig: AppliedConfig; }) { - const { data: source } = useSource({ id: appliedConfig.source }); - const { data: jsonColumns = [] } = useJsonColumns({ - databaseName: source?.from?.databaseName || '', - tableName: source?.from?.tableName || '', - connectionId: source?.connection || '', + const { data: source } = useSource({ + id: appliedConfig.source, + kind: ALLOWED_SOURCE_KIND, }); + const { data: jsonColumns = [] } = useJsonColumns(tcFromSource(source)); const expressions = getExpressions(source, jsonColumns); const [reqChartType, setReqChartType] = useQueryState( @@ -546,12 +537,11 @@ function DatabaseTab({ searchedTimeRange: [Date, Date]; appliedConfig: AppliedConfig; }) { - const { data: source } = useSource({ id: appliedConfig.source }); - const { data: jsonColumns = [] } = useJsonColumns({ - databaseName: source?.from?.databaseName || '', - tableName: source?.from?.tableName || '', - connectionId: source?.connection || '', + const { data: source } = useSource({ + id: appliedConfig.source, + kind: ALLOWED_SOURCE_KIND, }); + const { data: jsonColumns = [] } = useJsonColumns(tcFromSource(source)); const expressions = getExpressions(source, jsonColumns); const [chartType, setChartType] = useState<'table' | 'list'>('list'); @@ -800,7 +790,10 @@ function ErrorsTab({ searchedTimeRange: [Date, Date]; appliedConfig: AppliedConfig; }) { - const { data: source } = useSource({ id: appliedConfig.source }); + const { data: source } = useSource({ + id: appliedConfig.source, + kind: SourceKind.Trace, + }); const { data: jsonColumns = [] } = useJsonColumns({ databaseName: source?.from?.databaseName || '', tableName: source?.from?.tableName || '', @@ -849,6 +842,7 @@ function ErrorsTab({ ); } +const ALLOWED_SOURCE_KIND = SourceKind.Trace; // TODO: This is a hack to set the default time range const defaultTimeRange = parseTimeQuery('Past 1h', false) as [Date, Date]; @@ -883,6 +877,7 @@ function ServicesDashboardPage() { const sourceId = watch('source'); const { data: source } = useSource({ id: watch('source'), + kind: ALLOWED_SOURCE_KIND, }); useEffect(() => { @@ -943,7 +938,7 @@ function ServicesDashboardPage() { string; generateChartUrl?: (config: { diff --git a/packages/app/src/SessionsPage.tsx b/packages/app/src/SessionsPage.tsx index 2aae44420..1cb63e33d 100644 --- a/packages/app/src/SessionsPage.tsx +++ b/packages/app/src/SessionsPage.tsx @@ -253,10 +253,12 @@ export default function SessionsPage() { const sourceId = watch('source'); const { data: sessionSource, isPending: isSessionSourceLoading } = useSource({ id: watch('source'), + kind: SourceKind.Session, }); const { data: traceTrace } = useSource({ id: sessionSource?.traceSourceId, + kind: SourceKind.Trace, }); // Get all sources and select the first session type source by default diff --git a/packages/app/src/__tests__/serviceDashboard.test.ts b/packages/app/src/__tests__/serviceDashboard.test.ts index 827d944e0..a31384a19 100644 --- a/packages/app/src/__tests__/serviceDashboard.test.ts +++ b/packages/app/src/__tests__/serviceDashboard.test.ts @@ -1,4 +1,4 @@ -import type { TSource } from '@hyperdx/common-utils/dist/types'; +import type { TTraceSource } from '@hyperdx/common-utils/dist/types'; import { SourceKind } from '@hyperdx/common-utils/dist/types'; import { @@ -11,7 +11,7 @@ function removeAllWhitespace(str: string) { } describe('Service Dashboard', () => { - const mockSource: TSource = { + const mockSource: TTraceSource = { id: 'test-source', name: 'Test Source', kind: SourceKind.Trace, @@ -27,7 +27,9 @@ describe('Service Dashboard', () => { serviceNameExpression: 'ServiceName', spanNameExpression: 'SpanName', spanKindExpression: 'SpanKind', - severityTextExpression: 'StatusCode', + statusCodeExpression: 'StatusCode', + spanIdExpression: 'SpanId', + parentSpanIdExpression: 'ParentSpanId', }; describe('getExpressions', () => { diff --git a/packages/app/src/__tests__/utils.test.ts b/packages/app/src/__tests__/utils.test.ts index 059415a8f..7a2399366 100644 --- a/packages/app/src/__tests__/utils.test.ts +++ b/packages/app/src/__tests__/utils.test.ts @@ -1,4 +1,4 @@ -import { TSource } from '@hyperdx/common-utils/dist/types'; +import { TMetricSource } from '@hyperdx/common-utils/dist/types'; import { act, renderHook } from '@testing-library/react'; import { MetricsDataType, NumberFormat } from '../types'; @@ -65,14 +65,14 @@ describe('getMetricTableName', () => { }); it('returns the default table name when metricType is null', () => { - const source = createSourceWithMetrics() as unknown as TSource; + const source = createSourceWithMetrics() as unknown as TMetricSource; expect(getMetricTableName(source)).toBe(''); expect(getMetricTableName(source, undefined)).toBe(''); }); it('returns the specific metric table when metricType is provided', () => { - const source = createSourceWithMetrics() as unknown as TSource; + const source = createSourceWithMetrics() as unknown as TMetricSource; expect(getMetricTableName(source, 'gauge' as MetricsDataType)).toBe( 'gauge_table', @@ -83,7 +83,7 @@ describe('getMetricTableName', () => { }); it('handles case insensitivity for metric types', () => { - const source = createSourceWithMetrics() as unknown as TSource; + const source = createSourceWithMetrics() as unknown as TMetricSource; expect(getMetricTableName(source, 'GAUGE' as MetricsDataType)).toBe( 'gauge_table', @@ -99,7 +99,7 @@ describe('getMetricTableName', () => { metricTables: { gauge: 'gauge_table', }, - } as unknown as TSource; + } as unknown as TMetricSource; expect( getMetricTableName(source, 'histogram' as MetricsDataType), @@ -107,7 +107,7 @@ describe('getMetricTableName', () => { }); it('handles sources without metricTables property', () => { - const source = createBaseSource() as unknown as TSource; + const source = createBaseSource() as unknown as TMetricSource; expect(getMetricTableName(source)).toBe(''); expect( diff --git a/packages/app/src/components/AlertPreviewChart.tsx b/packages/app/src/components/AlertPreviewChart.tsx index 36ae7085d..592a1bd31 100644 --- a/packages/app/src/components/AlertPreviewChart.tsx +++ b/packages/app/src/components/AlertPreviewChart.tsx @@ -3,6 +3,7 @@ import { AlertInterval, SearchCondition, SearchConditionLanguage, + SourceKind, } from '@hyperdx/common-utils/dist/types'; import { TSource } from '@hyperdx/common-utils/dist/types'; import { Paper } from '@mantine/core'; @@ -62,7 +63,10 @@ export const AlertPreviewChart = ({ whereLanguage: whereLanguage || undefined, dateRange: intervalToDateRange(interval), granularity: intervalToGranularity(interval), - implicitColumnExpression: source.implicitColumnExpression, + implicitColumnExpression: + source.kind === SourceKind.Trace || source.kind === SourceKind.Log + ? source.implicitColumnExpression + : undefined, groupBy, with: aliasWith, select: [ @@ -73,7 +77,10 @@ export const AlertPreviewChart = ({ valueExpression: '', }, ], - timestampValueExpression: source.timestampValueExpression, + timestampValueExpression: + source.kind !== SourceKind.Session + ? source.timestampValueExpression + : '', from: source.from, connection: source.connection, }} diff --git a/packages/app/src/components/ContextSidePanel.tsx b/packages/app/src/components/ContextSidePanel.tsx index b8b39bb50..0d1f9e6cc 100644 --- a/packages/app/src/components/ContextSidePanel.tsx +++ b/packages/app/src/components/ContextSidePanel.tsx @@ -6,7 +6,9 @@ import { useForm } from 'react-hook-form'; import { tcFromSource } from '@hyperdx/common-utils/dist/metadata'; import { ChartConfigWithDateRange, - TSource, + SourceKind, + type TLogSource, + type TTraceSource, } from '@hyperdx/common-utils/dist/types'; import { Badge, Flex, Group, SegmentedControl } from '@mantine/core'; import { useDebouncedValue } from '@mantine/hooks'; @@ -34,7 +36,7 @@ enum ContextBy { } interface ContextSubpanelProps { - source: TSource; + source: TLogSource | TTraceSource; dbSqlRowTableConfig: ChartConfigWithDateRange | undefined; rowData: Record; rowId: string | undefined; @@ -101,9 +103,15 @@ export default function ContextSubpanel({ setContextRowSource, } = useNestedPanelState(isNested); - const { data: contextRowSidePanelSource } = useSource({ + const { data: logSource } = useSource({ id: contextRowSource || '', + kind: SourceKind.Log, }); + const { data: traceSource } = useSource({ + id: contextRowSource || '', + kind: SourceKind.Trace, + }); + const contextRowSidePanelSource = logSource || traceSource; const handleContextSidePanelClose = useCallback(() => { setContextRowId(null); diff --git a/packages/app/src/components/DBEditTimeChartForm.tsx b/packages/app/src/components/DBEditTimeChartForm.tsx index 133c1b305..228826bd9 100644 --- a/packages/app/src/components/DBEditTimeChartForm.tsx +++ b/packages/app/src/components/DBEditTimeChartForm.tsx @@ -458,13 +458,20 @@ export default function EditTimeChartForm({ const newConfig = { ...form, from: tableSource.from, - timestampValueExpression: tableSource.timestampValueExpression, + timestampValueExpression: tableSource.timestampValueExpression || '', dateRange, connection: tableSource.connection, - implicitColumnExpression: tableSource.implicitColumnExpression, - metricTables: tableSource.metricTables, + ...(tableSource.kind === SourceKind.Log || + tableSource.kind === SourceKind.Trace + ? { implicitColumnExpression: tableSource.implicitColumnExpression } + : {}), + ...(tableSource.kind === SourceKind.Metric + ? { metricTables: tableSource.metricTables } + : {}), select: isSelectEmpty - ? tableSource.defaultTableSelectExpression || '' + ? ('defaultTableSelectExpression' in tableSource && + tableSource.defaultTableSelectExpression) || + '' : form.select, }; setQueriedConfig( @@ -532,27 +539,31 @@ export default function EditTimeChartForm({ const sampleEventsConfig = useMemo( () => tableSource != null && queriedConfig != null && queryReady - ? { + ? ({ ...queriedConfig, orderBy: [ { ordering: 'DESC' as const, valueExpression: getFirstTimestampValueExpression( - tableSource.timestampValueExpression, + tableSource.timestampValueExpression || '', ), }, ], dateRange, - timestampValueExpression: tableSource.timestampValueExpression, + timestampValueExpression: + tableSource.timestampValueExpression ?? '', connection: tableSource.connection, from: tableSource.from, limit: { limit: 200 }, - select: tableSource?.defaultTableSelectExpression || '', + select: + ('defaultTableSelectExpression' in tableSource + ? tableSource.defaultTableSelectExpression + : undefined) || '', filters: seriesToFilters(queriedConfig.select), filtersLogicalOperator: 'OR' as const, groupBy: undefined, granularity: undefined, - } + } satisfies ChartConfigWithDateRange) : null, [queriedConfig, tableSource, dateRange, queryReady], ); @@ -764,9 +775,16 @@ export default function EditTimeChartForm({ control={control} name="select" placeholder={ - tableSource?.defaultTableSelectExpression || 'SELECT Columns' + (tableSource && + 'defaultTableSelectExpression' in tableSource && + tableSource.defaultTableSelectExpression) || + 'SELECT Columns' + } + defaultValue={ + tableSource && 'defaultTableSelectExpression' in tableSource + ? tableSource.defaultTableSelectExpression + : undefined } - defaultValue={tableSource?.defaultTableSelectExpression} onSubmit={onSubmit} label="SELECT" /> @@ -986,18 +1004,20 @@ export default function EditTimeChartForm({ { ordering: 'DESC' as const, valueExpression: getFirstTimestampValueExpression( - tableSource.timestampValueExpression, + tableSource.timestampValueExpression ?? '', ), }, ], dateRange, - timestampValueExpression: tableSource.timestampValueExpression, + timestampValueExpression: + tableSource.timestampValueExpression ?? '', connection: tableSource.connection, from: tableSource.from, limit: { limit: 200 }, select: queriedConfig.select || - tableSource?.defaultTableSelectExpression || + ('defaultTableSelectExpression' in tableSource && + tableSource.defaultTableSelectExpression) || '', groupBy: undefined, granularity: undefined, diff --git a/packages/app/src/components/DBInfraPanel.tsx b/packages/app/src/components/DBInfraPanel.tsx index ad7b0f3c2..c94c99112 100644 --- a/packages/app/src/components/DBInfraPanel.tsx +++ b/packages/app/src/components/DBInfraPanel.tsx @@ -1,6 +1,10 @@ import { useMemo, useState } from 'react'; import { add, min, sub } from 'date-fns'; -import { TSource } from '@hyperdx/common-utils/dist/types'; +import { + SourceKind, + type TLogSource, + type TMetricSource, +} from '@hyperdx/common-utils/dist/types'; import { Box, Card, @@ -33,7 +37,7 @@ const InfraSubpanelGroup = ({ where, }: { fieldPrefix: string; - metricSource: TSource; + metricSource: TMetricSource; timestamp: any; title: string; where: string; @@ -210,9 +214,12 @@ export default ({ }: { rowData?: Record; rowId: string | undefined | null; - source: TSource; + source: TLogSource; }) => { - const { data: metricSource } = useSource({ id: source.metricSourceId }); + const { data: metricSource } = useSource({ + id: source.metricSourceId, + kind: SourceKind.Metric, + }); const podUid = rowData?.__hdx_resource_attributes['k8s.pod.uid']; const nodeName = rowData?.__hdx_resource_attributes['k8s.node.name']; diff --git a/packages/app/src/components/DBRowDataPanel.tsx b/packages/app/src/components/DBRowDataPanel.tsx index 4f418276d..92c19c0cb 100644 --- a/packages/app/src/components/DBRowDataPanel.tsx +++ b/packages/app/src/components/DBRowDataPanel.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react'; import type { ResponseJSON } from '@hyperdx/common-utils/dist/clickhouse'; -import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types'; +import { SourceKind, type TSource } from '@hyperdx/common-utils/dist/types'; import { Box } from '@mantine/core'; import { useQueriedChartConfig } from '@/hooks/useChartConfig'; @@ -17,11 +17,15 @@ export function useRowData({ }) { const eventBodyExpr = getEventBody(source); - const searchedTraceIdExpr = source.traceIdExpression; - const searchedSpanIdExpr = source.spanIdExpression; + const searchedTraceIdExpr = + 'traceIdExpression' in source ? source.traceIdExpression : undefined; + const searchedSpanIdExpr = + 'spanIdExpression' in source ? source.spanIdExpression : undefined; const severityTextExpr = - source.severityTextExpression || source.statusCodeExpression; + (source.kind === SourceKind.Log && source.severityTextExpression) || + (source.kind === SourceKind.Trace && source.statusCodeExpression) || + undefined; return useQueriedChartConfig( { @@ -66,26 +70,26 @@ export function useRowData({ }, ] : []), - ...(source.serviceNameExpression + ...('serviceNameExpression' in source ? [ { - valueExpression: source.serviceNameExpression, + valueExpression: source.serviceNameExpression ?? '', alias: '__hdx_service_name', }, ] : []), - ...(source.resourceAttributesExpression + ...('resourceAttributesExpression' in source ? [ { - valueExpression: source.resourceAttributesExpression, + valueExpression: source.resourceAttributesExpression ?? '', alias: '__hdx_resource_attributes', }, ] : []), - ...(source.eventAttributesExpression + ...('eventAttributesExpression' in source ? [ { - valueExpression: source.eventAttributesExpression, + valueExpression: source.eventAttributesExpression ?? '', alias: '__hdx_event_attributes', }, ] diff --git a/packages/app/src/components/DBRowOverviewPanel.tsx b/packages/app/src/components/DBRowOverviewPanel.tsx index 0fbb4d593..850fc395b 100644 --- a/packages/app/src/components/DBRowOverviewPanel.tsx +++ b/packages/app/src/components/DBRowOverviewPanel.tsx @@ -2,7 +2,10 @@ import { useCallback, useContext, useMemo } from 'react'; import { flatten } from 'flat'; import isString from 'lodash/isString'; import pickBy from 'lodash/pickBy'; -import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types'; +import type { + TLogSource, + TTraceSource, +} from '@hyperdx/common-utils/dist/types'; import { Accordion, Box, Divider, Flex, Text } from '@mantine/core'; import { getEventBody } from '@/source'; @@ -22,7 +25,7 @@ export function RowOverviewPanel({ rowId, hideHeader = false, }: { - source: TSource; + source: TLogSource | TTraceSource; rowId: string | undefined | null; hideHeader?: boolean; }) { diff --git a/packages/app/src/components/DBRowSidePanel.tsx b/packages/app/src/components/DBRowSidePanel.tsx index b5bd6028a..d13e16d92 100644 --- a/packages/app/src/components/DBRowSidePanel.tsx +++ b/packages/app/src/components/DBRowSidePanel.tsx @@ -13,7 +13,11 @@ import { parseAsStringEnum, useQueryState } from 'nuqs'; import { ErrorBoundary } from 'react-error-boundary'; import { useHotkeys } from 'react-hotkeys-hook'; import Drawer from 'react-modern-drawer'; -import { TSource } from '@hyperdx/common-utils/dist/types'; +import { + SourceKind, + type TLogSource, + type TTraceSource, +} from '@hyperdx/common-utils/dist/types'; import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types'; import { Box, Stack } from '@mantine/core'; import { useClickOutside } from '@mantine/hooks'; @@ -72,7 +76,7 @@ enum Tab { } type DBRowSidePanelProps = { - source: TSource; + source: TLogSource | TTraceSource; rowId: string | undefined; onClose: () => void; isNestedPanel?: boolean; @@ -208,9 +212,9 @@ const DBRowSidePanel = ({ const traceId = normalizedRow?.['__hdx_trace_id']; const childSourceId = - source.kind === 'log' + source.kind === SourceKind.Log ? source.traceSourceId - : source.kind === 'trace' + : source.kind === SourceKind.Trace ? source.logSourceId : undefined; @@ -429,11 +433,13 @@ const DBRowSidePanel = ({ )} > - + {source.kind === SourceKind.Log && ( + + )} )} diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index 32bc313d6..8f58ec8bc 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -31,7 +31,9 @@ import { import { ChartConfigWithDateRange, SelectList, - TSource, + SourceKind, + type TLogSource, + type TTraceSource, } from '@hyperdx/common-utils/dist/types'; import { splitAndTrimWithBracket } from '@hyperdx/common-utils/dist/utils'; import { @@ -335,7 +337,7 @@ export const RawLogTable = memo( loadingDate?: Date; config?: ChartConfigWithDateRange; onChildModalOpen?: (open: boolean) => void; - source?: TSource; + source?: TTraceSource | TLogSource; onExpandedRowsChange?: (hasExpandedRows: boolean) => void; collapseAllRows?: boolean; showExpandButton?: boolean; @@ -1185,13 +1187,21 @@ function DBSqlRowTableComponent({ } }, [isError, onError, error]); - const { data: source } = useSource({ id: sourceId }); + const { data: logSource } = useSource({ id: sourceId, kind: SourceKind.Log }); + const { data: traceSource } = useSource({ + id: sourceId, + kind: SourceKind.Trace, + }); + const source = logSource ?? traceSource; const patternColumn = columns[columns.length - 1]; const groupedPatterns = useGroupedPatterns({ config, samples: 10_000, bodyValueExpression: patternColumn ?? '', - severityTextExpression: source?.severityTextExpression ?? '', + severityTextExpression: + (source && 'severityTextExpression' in source + ? source.severityTextExpression + : '') ?? '', totalCount: undefined, enabled: denoiseResults, }); diff --git a/packages/app/src/components/DBSessionPanel.tsx b/packages/app/src/components/DBSessionPanel.tsx index 073aa57fe..cd9c45bfd 100644 --- a/packages/app/src/components/DBSessionPanel.tsx +++ b/packages/app/src/components/DBSessionPanel.tsx @@ -1,5 +1,6 @@ import { useMemo } from 'react'; import Link from 'next/link'; +import { SourceKind } from '@hyperdx/common-utils/dist/types'; import { Loader } from '@mantine/core'; import SessionSubpanel from '@/SessionSubpanel'; @@ -18,46 +19,48 @@ export const useSessionId = ({ dateRange: [Date, Date]; enabled?: boolean; }) => { - // trace source - const { data: source } = useSource({ id: sourceId }); + const { data: traceSource } = useSource({ + id: sourceId, + kind: SourceKind.Trace, + }); const config = useMemo(() => { - if (!source) { + if (!traceSource) { return; } return { select: [ { - valueExpression: `${source.timestampValueExpression}`, + valueExpression: `${traceSource.timestampValueExpression}`, alias: 'Timestamp', }, { - valueExpression: `${source.resourceAttributesExpression}['rum.sessionId']`, + valueExpression: `${traceSource.resourceAttributesExpression}['rum.sessionId']`, alias: 'rumSessionId', }, { - valueExpression: `${source.resourceAttributesExpression}['service.name']`, + valueExpression: `${traceSource.resourceAttributesExpression}['service.name']`, alias: 'serviceName', }, { - valueExpression: `${source.parentSpanIdExpression}`, + valueExpression: `${traceSource.parentSpanIdExpression}`, alias: 'parentSpanId', }, ], - from: source.from, - timestampValueExpression: source.timestampValueExpression, + from: traceSource.from, + timestampValueExpression: traceSource.timestampValueExpression, limit: { limit: 10000 }, - connection: source.connection, - where: `${source.traceIdExpression} = '${traceId}'`, + connection: traceSource.connection, + where: `${traceSource.traceIdExpression} = '${traceId}'`, whereLanguage: 'sql' as const, }; - }, [source, traceId]); + }, [traceSource, traceId]); const { data } = useEventsData({ config: config!, // ok to force unwrap, the query will be disabled if source is null dateRangeStartInclusive: true, dateRange, - enabled: enabled && !!source, + enabled: enabled && !!traceSource, }); const result = useMemo(() => { @@ -99,9 +102,13 @@ export const DBSessionPanel = ({ serviceName: string; setSubDrawerOpen: (open: boolean) => void; }) => { - const { data: traceSource } = useSource({ id: traceSourceId }); + const { data: traceSource } = useSource({ + id: traceSourceId, + kind: SourceKind.Trace, + }); const { data: sessionSource, isLoading: isSessionSourceLoading } = useSource({ id: traceSource?.sessionSourceId, + kind: SourceKind.Session, }); if (!traceSource || (!sessionSource && isSessionSourceLoading)) { diff --git a/packages/app/src/components/DBTimeChart.tsx b/packages/app/src/components/DBTimeChart.tsx index 12f69b16a..6759c1104 100644 --- a/packages/app/src/components/DBTimeChart.tsx +++ b/packages/app/src/components/DBTimeChart.tsx @@ -7,6 +7,7 @@ import { isMetricChartConfig } from '@hyperdx/common-utils/dist/renderChartConfi import { ChartConfigWithDateRange, DisplayType, + SourceKind, } from '@hyperdx/common-utils/dist/types'; import { Box, Button, Code, Collapse, Text } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; @@ -142,7 +143,11 @@ function DBTimeChartComponent({ return null; } const isMetricChart = isMetricChartConfig(config); - if (isMetricChart && source?.logSourceId == null) { + if ( + isMetricChart && + source?.kind === SourceKind.Metric && + source?.logSourceId == null + ) { notifications.show({ color: 'yellow', message: 'No log source is associated with the selected metric source.', @@ -164,7 +169,10 @@ function DBTimeChartComponent({ whereLanguage = config.select[0].aggConditionLanguage ?? 'lucene'; } return new URLSearchParams({ - source: (isMetricChart ? source?.logSourceId : source?.id) ?? '', + source: + (source?.kind === SourceKind.Metric + ? source?.logSourceId + : source?.id) ?? '', where: where, whereLanguage: whereLanguage, filters: JSON.stringify(config.filters), diff --git a/packages/app/src/components/DBTracePanel.tsx b/packages/app/src/components/DBTracePanel.tsx index c5eaac026..3953fb131 100644 --- a/packages/app/src/components/DBTracePanel.tsx +++ b/packages/app/src/components/DBTracePanel.tsx @@ -99,14 +99,20 @@ export default function DBTracePanel({ setValue: traceIdSetValue, } = useForm<{ traceIdExpression: string }>({ defaultValues: { - traceIdExpression: parentSourceData?.traceIdExpression ?? '', + traceIdExpression: + (parentSourceData?.kind === SourceKind.Log && + parentSourceData?.traceIdExpression) || + '', }, }); useEffect(() => { - if (parentSourceData?.traceIdExpression) { + if ( + parentSourceData?.kind === SourceKind.Log && + parentSourceData?.traceIdExpression + ) { traceIdSetValue('traceIdExpression', parentSourceData.traceIdExpression); } - }, [parentSourceData?.traceIdExpression, traceIdSetValue]); + }, [parentSourceData, traceIdSetValue]); const [showTraceIdInput, setShowTraceIdInput] = useState(false); @@ -124,8 +130,10 @@ export default function DBTracePanel({ - {parentSourceData?.traceIdExpression}:{' '} - {traceId || 'No trace id found for event'} + {parentSourceData?.kind === SourceKind.Log + ? parentSourceData?.traceIdExpression + : undefined}{' '} + : {traceId || 'No trace id found for event'} {traceId != null && (