diff --git a/Common/Models/AnalyticsModels/ExceptionInstance.ts b/Common/Models/AnalyticsModels/ExceptionInstance.ts index 724e023bc2..97218ef578 100644 --- a/Common/Models/AnalyticsModels/ExceptionInstance.ts +++ b/Common/Models/AnalyticsModels/ExceptionInstance.ts @@ -297,6 +297,31 @@ export default class ExceptionInstance extends AnalyticsBaseModel { update: [], }, }), + + + new AnalyticsTableColumn({ + key: "attributes", + title: "Attributes", + description: "Attributes", + required: true, + defaultValue: {}, + type: TableColumnType.JSON, + accessControl: { + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadTelemetryException, + ], + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateTelemetryException, + ], + update: [], + }, + }), ], sortKeys: ["projectId", "serviceId", "fingerprint", "time"], primaryKeys: ["projectId", "serviceId", "fingerprint"], @@ -390,4 +415,12 @@ export default class ExceptionInstance extends AnalyticsBaseModel { public set fingerprint(v: string | undefined) { this.setColumnValue("fingerprint", v); } + + public get attributes(): Record { + return this.getColumnValue("attributes") as Record; + } + + public set attributes(v: Record) { + this.setColumnValue("attributes", v); + } } diff --git a/Common/Models/AnalyticsModels/Span.ts b/Common/Models/AnalyticsModels/Span.ts index 4fce909689..7b36c75dd1 100644 --- a/Common/Models/AnalyticsModels/Span.ts +++ b/Common/Models/AnalyticsModels/Span.ts @@ -16,8 +16,8 @@ export enum SpanKind { } export enum SpanEventType { - Exception = "Exception", - Event = "Event", + Exception = "exception", + Event = "event", } export enum SpanStatus { diff --git a/Common/Models/DatabaseModels/TelemetryException.ts b/Common/Models/DatabaseModels/TelemetryException.ts index d1f93f1661..19c1456dac 100644 --- a/Common/Models/DatabaseModels/TelemetryException.ts +++ b/Common/Models/DatabaseModels/TelemetryException.ts @@ -938,4 +938,38 @@ export default class TelemetryException extends DatabaseBaseModel { default: false, }) public isArchived?: boolean = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.CreateTelemetryException, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadTelemetryException, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.EditTelemetryException, + ], + }) + @TableColumn({ + title: "Occurances", + description: "Number of times this exception has occured", + isDefaultValueColumn: true, + required: true, + type: TableColumnType.Number, + }) + @Column({ + type: ColumnType.Number, + nullable: false, + unique: false, + default: 1, + }) + public occuranceCount?: number = undefined; + } diff --git a/Common/Server/API/StatusAPI.ts b/Common/Server/API/StatusAPI.ts index 708e940d3a..51a90f8d43 100644 --- a/Common/Server/API/StatusAPI.ts +++ b/Common/Server/API/StatusAPI.ts @@ -1,4 +1,4 @@ -import { SpanStatusCode } from "@opentelemetry/api"; + import LocalCache from "../Infrastructure/LocalCache"; import Express, { ExpressRequest, @@ -7,12 +7,11 @@ import Express, { } from "../Utils/Express"; import logger from "../Utils/Logger"; import Response from "../Utils/Response"; -import Telemetry, { Span } from "../Utils/Telemetry"; +import Telemetry, { Span, SpanStatusCode } from "../Utils/Telemetry"; import Exception from "Common/Types/Exception/Exception"; import ServerException from "Common/Types/Exception/ServerException"; import BadDataException from "../../Types/Exception/BadDataException"; - export interface StatusAPIOptions { readyCheck: () => Promise; liveCheck: () => Promise; @@ -95,16 +94,14 @@ export default class StatusAPI { router.get( "/status/live", async (req: ExpressRequest, res: ExpressResponse) => { - const span: Span = Telemetry.startSpan({ name: "status.live", attributes: { - "status": "live" - } + status: "live", + }, }); try { - logger.debug("Live check"); await options.readyCheck(); logger.info("Live check: ok"); @@ -115,16 +112,14 @@ export default class StatusAPI { Response.sendJsonObjectResponse(req, res, { status: "ok", }); - } catch (e) { - // record exception span.recordException(e as Exception); // set span status span.setStatus({ - code: SpanStatusCode.OK, - message: "Live check failed" + code: SpanStatusCode.ERROR, + message: "Live check failed", }); this.stausLiveFailed.add(1); diff --git a/Common/Server/EnvironmentConfig.ts b/Common/Server/EnvironmentConfig.ts index abd92f20b6..1bab7f2cf8 100644 --- a/Common/Server/EnvironmentConfig.ts +++ b/Common/Server/EnvironmentConfig.ts @@ -256,5 +256,5 @@ export const AccountsClientUrl: URL = new URL( AccountsRoute, ); - -export const DisableTelemetry: boolean = process.env["DISABLE_TELEMETRY"] === "true"; \ No newline at end of file +export const DisableTelemetry: boolean = + process.env["DISABLE_TELEMETRY"] === "true"; diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1724613666632-MigrationName.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1724613666632-MigrationName.ts index 8a233048ae..d81f364e6d 100644 --- a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1724613666632-MigrationName.ts +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1724613666632-MigrationName.ts @@ -25,12 +25,7 @@ export class MigrationName1724613666632 implements MigrationInterface { await queryRunner.query( `ALTER TABLE "TelemetryException" ADD "isArchived" boolean NOT NULL DEFAULT false`, ); - await queryRunner.query( - `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`, - ); - await queryRunner.query( - `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`, - ); + await queryRunner.query( `ALTER TABLE "TelemetryException" ADD CONSTRAINT "FK_3def22373f0cb84e16cb355b5e5" FOREIGN KEY ("markedAsArchivedByUserId") REFERENCES "User"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`, ); @@ -40,12 +35,6 @@ export class MigrationName1724613666632 implements MigrationInterface { await queryRunner.query( `ALTER TABLE "TelemetryException" DROP CONSTRAINT "FK_3def22373f0cb84e16cb355b5e5"`, ); - await queryRunner.query( - `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`, - ); - await queryRunner.query( - `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`, - ); await queryRunner.query( `ALTER TABLE "TelemetryException" DROP COLUMN "isArchived"`, ); diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1724659071843-MigrationName.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1724659071843-MigrationName.ts new file mode 100644 index 0000000000..691bad02e3 --- /dev/null +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1724659071843-MigrationName.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class MigrationName1724659071843 implements MigrationInterface { + public name = 'MigrationName1724659071843' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "TelemetryException" ADD "occuranceCount" integer NOT NULL DEFAULT '1'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "TelemetryException" DROP COLUMN "occuranceCount"`); + } + +} diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts index d36aa11a25..481a80d134 100644 --- a/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts @@ -43,6 +43,7 @@ import { MigrationName1723828588502 } from "./1723828588502-MigrationName"; import { MigrationName1724078044172 } from "./1724078044172-MigrationName"; import { MigrationName1724610006927 } from "./1724610006927-MigrationName"; import { MigrationName1724613666632 } from "./1724613666632-MigrationName"; +import { MigrationName1724659071843 } from "./1724659071843-MigrationName"; export default [ InitialMigration, @@ -90,4 +91,5 @@ export default [ MigrationName1724078044172, MigrationName1724610006927, MigrationName1724613666632, + MigrationName1724659071843 ]; diff --git a/Common/Server/Services/DatabaseService.ts b/Common/Server/Services/DatabaseService.ts index 84917b08cb..14f032eb47 100644 --- a/Common/Server/Services/DatabaseService.ts +++ b/Common/Server/Services/DatabaseService.ts @@ -60,7 +60,6 @@ import API from "Common/Utils/API"; import Slug from "Common/Utils/Slug"; import { DataSource, Repository, SelectQueryBuilder } from "typeorm"; import { FindWhere } from "../../Types/BaseDatabase/Query"; -import QueryDeepPartialEntity from "../../Types/Database/PartialEntity"; class DatabaseService extends BaseService { public modelType!: { new (): TBaseModel }; @@ -427,10 +426,10 @@ class DatabaseService extends BaseService { } private async sanitizeCreateOrUpdate( - data: TBaseModel | QueryDeepPartialEntity, + data: TBaseModel | PartialEntity, props: DatabaseCommonInteractionProps, isUpdate: boolean = false, - ): Promise> { + ): Promise> { data = this.checkMaxLengthOfFields(data as TBaseModel); const columns: Columns = this.model.getTableColumns(); @@ -1274,12 +1273,12 @@ class DatabaseService extends BaseService { beforeUpdateBy.props, ); - const data: QueryDeepPartialEntity = + const data: PartialEntity = (await this.sanitizeCreateOrUpdate( beforeUpdateBy.data, updateBy.props, true, - )) as QueryDeepPartialEntity; + )) as PartialEntity; if (!(updateBy.skip instanceof PositiveNumber)) { updateBy.skip = new PositiveNumber(updateBy.skip); diff --git a/Common/Server/Types/Database/Permissions/Index.ts b/Common/Server/Types/Database/Permissions/Index.ts index 7795215b9d..385779667d 100644 --- a/Common/Server/Types/Database/Permissions/Index.ts +++ b/Common/Server/Types/Database/Permissions/Index.ts @@ -8,7 +8,6 @@ import UpdatePermission from "./UpdatePermission"; import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel"; import DatabaseCommonInteractionProps from "Common/Types/BaseDatabase/DatabaseCommonInteractionProps"; - export default class ModelPermission { public static async checkDeletePermissionByModel< TBaseModel extends BaseModel, diff --git a/Common/Server/Types/Database/UpdateByID.ts b/Common/Server/Types/Database/UpdateByID.ts index 4166098346..aed7ef0476 100644 --- a/Common/Server/Types/Database/UpdateByID.ts +++ b/Common/Server/Types/Database/UpdateByID.ts @@ -3,7 +3,6 @@ import DatabaseCommonInteractionProps from "Common/Types/BaseDatabase/DatabaseCo import ObjectID from "Common/Types/ObjectID"; import QueryDeepPartialEntity from "../../../Types/Database/PartialEntity"; - export default interface UpdateBy { id: ObjectID; data: QueryDeepPartialEntity; diff --git a/Common/Server/Utils/Telemetry.ts b/Common/Server/Utils/Telemetry.ts index 8b2f1ea50d..278cd344a1 100644 --- a/Common/Server/Utils/Telemetry.ts +++ b/Common/Server/Utils/Telemetry.ts @@ -29,8 +29,14 @@ import { DisableTelemetry } from "../EnvironmentConfig"; // Enable this line to see debug logs // diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG); -export type Span = opentelemetry.api.Span -export type SpanStatus = opentelemetry.api.SpanStatus +export type Span = opentelemetry.api.Span; +export type SpanStatus = opentelemetry.api.SpanStatus; + +export enum SpanStatusCode { + UNSET = 0, + OK = 1, + ERROR = 2 +} export default class Telemetry { public static sdk: opentelemetry.NodeSDK | null = null; @@ -104,9 +110,10 @@ export default class Telemetry { }); } - public static init(data: { serviceName: string }): opentelemetry.NodeSDK | null { - - if(DisableTelemetry){ + public static init(data: { + serviceName: string; + }): opentelemetry.NodeSDK | null { + if (DisableTelemetry) { return null; } @@ -290,7 +297,8 @@ export default class Telemetry { } public static getTracer(): opentelemetry.api.Tracer { - const tracer: opentelemetry.api.Tracer = OpenTelemetryAPI.trace.getTracer("default"); + const tracer: opentelemetry.api.Tracer = + OpenTelemetryAPI.trace.getTracer("default"); return tracer; } @@ -303,5 +311,4 @@ export default class Telemetry { const span: Span = this.getTracer().startSpan(name, attributes); return span; } - } diff --git a/Common/Types/Database/PartialEntity.ts b/Common/Types/Database/PartialEntity.ts index 41a23a21ce..e3ea224009 100644 --- a/Common/Types/Database/PartialEntity.ts +++ b/Common/Types/Database/PartialEntity.ts @@ -1,11 +1,16 @@ - /** * Make all properties in T optional. Deep version. */ - type QueryDeepPartialEntity = { - [P in keyof T]?: (T[P] extends Array ? Array> : T[P] extends ReadonlyArray ? ReadonlyArray> : QueryDeepPartialEntity) | (() => string) | null; + [P in keyof T]?: + | (T[P] extends Array + ? Array> + : T[P] extends ReadonlyArray + ? ReadonlyArray> + : QueryDeepPartialEntity) + | (() => string) + | null; }; export default QueryDeepPartialEntity; diff --git a/Common/UI/Config.ts b/Common/UI/Config.ts index a80ae7042c..8be79f6992 100644 --- a/Common/UI/Config.ts +++ b/Common/UI/Config.ts @@ -210,4 +210,4 @@ const getOpenTelemetryExporterOtlpHeaders: GetOpenTelemetryExporterOtlpHeadersFu export const OpenTelemetryExporterOtlpHeaders: Dictionary = getOpenTelemetryExporterOtlpHeaders(); -export const DisableTelemetry: boolean = env("DISABLE_TELEMETRY") === "true"; +export const DisableTelemetry: boolean = env("DISABLE_TELEMETRY") === "true"; diff --git a/Common/UI/Utils/Telemetry.ts b/Common/UI/Utils/Telemetry.ts index 0c7f168f83..badcd08fe6 100644 --- a/Common/UI/Utils/Telemetry.ts +++ b/Common/UI/Utils/Telemetry.ts @@ -19,9 +19,7 @@ import URL from "Common/Types/API/URL"; export default class Telemetry { public static init(data: { serviceName: string }): void { - - - if(DisableTelemetry){ + if (DisableTelemetry) { return; } diff --git a/Dashboard/src/Routes/TelemetryRoutes.tsx b/Dashboard/src/Routes/TelemetryRoutes.tsx index 386332a680..a4f9a045d7 100644 --- a/Dashboard/src/Routes/TelemetryRoutes.tsx +++ b/Dashboard/src/Routes/TelemetryRoutes.tsx @@ -419,7 +419,8 @@ const TelemetryRoutes: FunctionComponent = ( @@ -437,7 +438,8 @@ const TelemetryRoutes: FunctionComponent = ( @@ -455,7 +457,8 @@ const TelemetryRoutes: FunctionComponent = ( diff --git a/Dashboard/src/Utils/Breadcrumbs/TelemetryBreadcrumbs.ts b/Dashboard/src/Utils/Breadcrumbs/TelemetryBreadcrumbs.ts index d3a1ee8ebe..4440c009da 100644 --- a/Dashboard/src/Utils/Breadcrumbs/TelemetryBreadcrumbs.ts +++ b/Dashboard/src/Utils/Breadcrumbs/TelemetryBreadcrumbs.ts @@ -108,7 +108,7 @@ export function getTelemetryBreadcrumbs(path: string): Array | undefined { PageMap.TELEMETRY_SERVICES_VIEW_EXCEPTIONS, ["Project", "Telemetry", "Services", "View Service", "Exceptions"], ), - + ...BuildBreadcrumbLinksByTitles( PageMap.TELEMETRY_SERVICES_VIEW_EXCEPTIONS_UNRESOLVED, [ diff --git a/Ingestor/API/OTelIngest.ts b/Ingestor/API/OTelIngest.ts index 54a1abfb42..94ac2449f0 100644 --- a/Ingestor/API/OTelIngest.ts +++ b/Ingestor/API/OTelIngest.ts @@ -308,12 +308,22 @@ router.post( exception.traceId = dbSpan.traceId; exception.time = eventTime; exception.timeUnixNano = eventTimeUnixNano; - exception.message = eventAttributes["message"] as string; - exception.stackTrace = eventAttributes[ - "stacktrace" - ] as string; - exception.exceptionType = eventAttributes["type"] as string; - exception.escaped = eventAttributes["escaped"] as boolean; + exception.message = (eventAttributes["exception.message"] as string) || ""; + exception.stackTrace = (eventAttributes["exception.stacktrace"] as string) || ""; + exception.exceptionType = (eventAttributes["exception.type"] as string) || ""; + exception.escaped = (eventAttributes["exception.escaped"] as boolean) || false; + const exceptionAttributes: JSONObject = { + ...eventAttributes, + }; + + for(const keys of Object.keys(exceptionAttributes)) { + // delete all keys that start with exception to avoid duplicate keys because we already saved it. + if(keys.startsWith("exception.")) { + delete exceptionAttributes[keys]; + } + } + + exception.attributes = exceptionAttributes; exception.fingerprint = ExceptionUtil.getFingerprint(exception); @@ -322,7 +332,7 @@ router.post( // save exception status // maybe this can be improved instead of doing a lot of db calls. - // await ExceptionUtil.saveOrUpdateTelemetryException(exception); + await ExceptionUtil.saveOrUpdateTelemetryException(exception); } } }