mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
## Summary Large refactor changing the TSource type to a true discriminated union. This means that the expected fields for `kind: 'log'` will differ from those for `'trace', 'session', 'metrics'`. This avoids the current laissez faire source type that currently exists, and required extensive changes across the api and app packages. Also includes a nice addition to `useSource` - you can now specify a `kind` field, which will properly infer the type of the returned source. This also makes use of discriminators in mongoose. This does change a bit of the way that we create and update sources. Obvious changes to sources have also been made, namely making `timeValueExpression` required on sources. Care has been taken to avoid requiring a migration. ### How to test locally or on Vercel 1. `yarn dev` 2. Play around with the app, especially around source creation, source edits, and loading existing sources from a previous version ### References - Linear Issue: References HDX-3352 - Related PRs: Ref: HDX-3352
223 lines
6.2 KiB
TypeScript
223 lines
6.2 KiB
TypeScript
import {
|
|
BaseSourceSchema,
|
|
LogSourceSchema,
|
|
MetricsDataType,
|
|
MetricSourceSchema,
|
|
QuerySettings,
|
|
SessionSourceSchema,
|
|
SourceKind,
|
|
TraceSourceSchema,
|
|
} from '@hyperdx/common-utils/dist/types';
|
|
import mongoose, { Schema } from 'mongoose';
|
|
import z from 'zod';
|
|
|
|
import { objectIdSchema } from '@/utils/zod';
|
|
|
|
// ISource is a discriminated union (inherits from TSource) with team added
|
|
// and connection widened to ObjectId | string for Mongoose.
|
|
// Omit and & distribute over the union, preserving the discriminated structure.
|
|
export const ISourceSchema = z.discriminatedUnion('kind', [
|
|
LogSourceSchema.omit({ connection: true }).extend({
|
|
team: objectIdSchema,
|
|
connection: objectIdSchema.or(z.string()),
|
|
}),
|
|
TraceSourceSchema.omit({ connection: true }).extend({
|
|
team: objectIdSchema,
|
|
connection: objectIdSchema.or(z.string()),
|
|
}),
|
|
SessionSourceSchema.omit({ connection: true }).extend({
|
|
team: objectIdSchema,
|
|
connection: objectIdSchema.or(z.string()),
|
|
}),
|
|
MetricSourceSchema.omit({ connection: true }).extend({
|
|
team: objectIdSchema,
|
|
connection: objectIdSchema.or(z.string()),
|
|
}),
|
|
]);
|
|
export type ISource = z.infer<typeof ISourceSchema>;
|
|
export type ISourceInput = z.input<typeof ISourceSchema>;
|
|
|
|
export type SourceDocument = mongoose.HydratedDocument<ISource>;
|
|
|
|
const maxLength =
|
|
(max: number) =>
|
|
<T>({ length }: Array<T>) =>
|
|
length <= max;
|
|
|
|
const QuerySetting = new Schema<QuerySettings[number]>(
|
|
{
|
|
setting: {
|
|
type: String,
|
|
required: true,
|
|
minlength: 1,
|
|
},
|
|
value: {
|
|
type: String,
|
|
required: true,
|
|
minlength: 1,
|
|
},
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
// --------------------------
|
|
// Base schema (common fields shared by all source kinds)
|
|
// --------------------------
|
|
|
|
type MongooseSourceBase = Omit<
|
|
z.infer<typeof BaseSourceSchema>,
|
|
'connection'
|
|
> & {
|
|
team: mongoose.Types.ObjectId;
|
|
connection: mongoose.Types.ObjectId;
|
|
};
|
|
|
|
const sourceBaseSchema = new Schema<MongooseSourceBase>(
|
|
{
|
|
team: {
|
|
type: mongoose.Schema.Types.ObjectId,
|
|
required: true,
|
|
ref: 'Team',
|
|
},
|
|
connection: {
|
|
type: mongoose.Schema.Types.ObjectId,
|
|
required: true,
|
|
ref: 'Connection',
|
|
},
|
|
name: String,
|
|
from: {
|
|
databaseName: String,
|
|
tableName: String,
|
|
},
|
|
timestampValueExpression: String,
|
|
querySettings: {
|
|
type: [QuerySetting],
|
|
validate: {
|
|
validator: maxLength(10),
|
|
message: '{PATH} exceeds the limit of 10',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
discriminatorKey: 'kind',
|
|
toJSON: { virtuals: true },
|
|
timestamps: true,
|
|
},
|
|
);
|
|
|
|
// Model is typed with the base schema type internally. Consumers use ISource
|
|
// (the discriminated union) via the exported type and discriminator models.
|
|
const SourceModel = mongoose.model<MongooseSourceBase>(
|
|
'Source',
|
|
sourceBaseSchema,
|
|
);
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
export const Source = SourceModel as unknown as mongoose.Model<ISource>;
|
|
|
|
// --------------------------
|
|
// Log discriminator
|
|
// --------------------------
|
|
type ILogSource = Extract<ISource, { kind: SourceKind.Log }>;
|
|
export const LogSource = Source.discriminator<ILogSource>(
|
|
SourceKind.Log,
|
|
new Schema<ILogSource>({
|
|
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,
|
|
highlightedTraceAttributeExpressions: {
|
|
type: mongoose.Schema.Types.Array,
|
|
},
|
|
highlightedRowAttributeExpressions: {
|
|
type: mongoose.Schema.Types.Array,
|
|
},
|
|
materializedViews: {
|
|
type: mongoose.Schema.Types.Array,
|
|
},
|
|
orderByExpression: String,
|
|
}),
|
|
);
|
|
|
|
// --------------------------
|
|
// Trace discriminator
|
|
// --------------------------
|
|
type ITraceSource = Extract<ISource, { kind: SourceKind.Trace }>;
|
|
export const TraceSource = Source.discriminator<ITraceSource>(
|
|
SourceKind.Trace,
|
|
new Schema<ITraceSource>({
|
|
defaultTableSelectExpression: 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,
|
|
displayedTimestampValueExpression: String,
|
|
highlightedTraceAttributeExpressions: {
|
|
type: mongoose.Schema.Types.Array,
|
|
},
|
|
highlightedRowAttributeExpressions: {
|
|
type: mongoose.Schema.Types.Array,
|
|
},
|
|
materializedViews: {
|
|
type: mongoose.Schema.Types.Array,
|
|
},
|
|
orderByExpression: String,
|
|
}),
|
|
);
|
|
|
|
// --------------------------
|
|
// Session discriminator
|
|
// --------------------------
|
|
type ISessionSource = Extract<ISource, { kind: SourceKind.Session }>;
|
|
export const SessionSource = Source.discriminator<ISessionSource>(
|
|
SourceKind.Session,
|
|
new Schema<Extract<ISource, { kind: SourceKind.Session }>>({
|
|
traceSourceId: String,
|
|
resourceAttributesExpression: String,
|
|
}),
|
|
);
|
|
|
|
// --------------------------
|
|
// Metric discriminator
|
|
// --------------------------
|
|
type IMetricSource = Extract<ISource, { kind: SourceKind.Metric }>;
|
|
export const MetricSource = Source.discriminator<IMetricSource>(
|
|
SourceKind.Metric,
|
|
new Schema<Extract<ISource, { kind: SourceKind.Metric }>>({
|
|
metricTables: {
|
|
type: {
|
|
[MetricsDataType.Gauge]: String,
|
|
[MetricsDataType.Histogram]: String,
|
|
[MetricsDataType.Sum]: String,
|
|
[MetricsDataType.Summary]: String,
|
|
[MetricsDataType.ExponentialHistogram]: String,
|
|
},
|
|
default: undefined,
|
|
},
|
|
resourceAttributesExpression: String,
|
|
serviceNameExpression: String,
|
|
logSourceId: String,
|
|
}),
|
|
);
|