refactor: Update telemetry exception view layout components

This commit is contained in:
Simon Larsen 2024-08-26 08:58:45 +01:00
parent a3856588bb
commit cc7696f481
No known key found for this signature in database
GPG Key ID: 96C5DCA24769DBCA
18 changed files with 146 additions and 59 deletions

View File

@ -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<string, any> {
return this.getColumnValue("attributes") as Record<string, any>;
}
public set attributes(v: Record<string, any>) {
this.setColumnValue("attributes", v);
}
}

View File

@ -16,8 +16,8 @@ export enum SpanKind {
}
export enum SpanEventType {
Exception = "Exception",
Event = "Event",
Exception = "exception",
Event = "event",
}
export enum SpanStatus {

View File

@ -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;
}

View File

@ -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<void>;
liveCheck: () => Promise<void>;
@ -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);

View File

@ -256,5 +256,5 @@ export const AccountsClientUrl: URL = new URL(
AccountsRoute,
);
export const DisableTelemetry: boolean = process.env["DISABLE_TELEMETRY"] === "true";
export const DisableTelemetry: boolean =
process.env["DISABLE_TELEMETRY"] === "true";

View File

@ -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"`,
);

View File

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1724659071843 implements MigrationInterface {
public name = 'MigrationName1724659071843'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "TelemetryException" ADD "occuranceCount" integer NOT NULL DEFAULT '1'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "TelemetryException" DROP COLUMN "occuranceCount"`);
}
}

View File

@ -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
];

View File

@ -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<TBaseModel extends BaseModel> extends BaseService {
public modelType!: { new (): TBaseModel };
@ -427,10 +426,10 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
}
private async sanitizeCreateOrUpdate(
data: TBaseModel | QueryDeepPartialEntity<TBaseModel>,
data: TBaseModel | PartialEntity<TBaseModel>,
props: DatabaseCommonInteractionProps,
isUpdate: boolean = false,
): Promise<TBaseModel | QueryDeepPartialEntity<TBaseModel>> {
): Promise<TBaseModel | PartialEntity<TBaseModel>> {
data = this.checkMaxLengthOfFields(data as TBaseModel);
const columns: Columns = this.model.getTableColumns();
@ -1274,12 +1273,12 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
beforeUpdateBy.props,
);
const data: QueryDeepPartialEntity<TBaseModel> =
const data: PartialEntity<TBaseModel> =
(await this.sanitizeCreateOrUpdate(
beforeUpdateBy.data,
updateBy.props,
true,
)) as QueryDeepPartialEntity<TBaseModel>;
)) as PartialEntity<TBaseModel>;
if (!(updateBy.skip instanceof PositiveNumber)) {
updateBy.skip = new PositiveNumber(updateBy.skip);

View File

@ -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,

View File

@ -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<TBaseModel extends BaseModel> {
id: ObjectID;
data: QueryDeepPartialEntity<TBaseModel>;

View File

@ -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;
}
}

View File

@ -1,11 +1,16 @@
/**
* Make all properties in T optional. Deep version.
*/
type QueryDeepPartialEntity<T> = {
[P in keyof T]?: (T[P] extends Array<infer U> ? Array<QueryDeepPartialEntity<U>> : T[P] extends ReadonlyArray<infer U> ? ReadonlyArray<QueryDeepPartialEntity<U>> : QueryDeepPartialEntity<T[P]>) | (() => string) | null;
[P in keyof T]?:
| (T[P] extends Array<infer U>
? Array<QueryDeepPartialEntity<U>>
: T[P] extends ReadonlyArray<infer U>
? ReadonlyArray<QueryDeepPartialEntity<U>>
: QueryDeepPartialEntity<T[P]>)
| (() => string)
| null;
};
export default QueryDeepPartialEntity;

View File

@ -210,4 +210,4 @@ const getOpenTelemetryExporterOtlpHeaders: GetOpenTelemetryExporterOtlpHeadersFu
export const OpenTelemetryExporterOtlpHeaders: Dictionary<string> =
getOpenTelemetryExporterOtlpHeaders();
export const DisableTelemetry: boolean = env("DISABLE_TELEMETRY") === "true";
export const DisableTelemetry: boolean = env("DISABLE_TELEMETRY") === "true";

View File

@ -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;
}

View File

@ -419,7 +419,8 @@ const TelemetryRoutes: FunctionComponent<ComponentProps> = (
<PageRoute
path={RouteUtil.getLastPathForKey(
PageMap.TELEMETRY_SERVICES_VIEW_EXCEPTIONS_UNRESOLVED, 2
PageMap.TELEMETRY_SERVICES_VIEW_EXCEPTIONS_UNRESOLVED,
2,
)}
element={
<Suspense fallback={Loader}>
@ -437,7 +438,8 @@ const TelemetryRoutes: FunctionComponent<ComponentProps> = (
<PageRoute
path={RouteUtil.getLastPathForKey(
PageMap.TELEMETRY_SERVICES_VIEW_EXCEPTIONS_RESOLVED,2
PageMap.TELEMETRY_SERVICES_VIEW_EXCEPTIONS_RESOLVED,
2,
)}
element={
<Suspense fallback={Loader}>
@ -455,7 +457,8 @@ const TelemetryRoutes: FunctionComponent<ComponentProps> = (
<PageRoute
path={RouteUtil.getLastPathForKey(
PageMap.TELEMETRY_SERVICES_VIEW_EXCEPTIONS_ARCHIVED,2
PageMap.TELEMETRY_SERVICES_VIEW_EXCEPTIONS_ARCHIVED,
2,
)}
element={
<Suspense fallback={Loader}>

View File

@ -108,7 +108,7 @@ export function getTelemetryBreadcrumbs(path: string): Array<Link> | undefined {
PageMap.TELEMETRY_SERVICES_VIEW_EXCEPTIONS,
["Project", "Telemetry", "Services", "View Service", "Exceptions"],
),
...BuildBreadcrumbLinksByTitles(
PageMap.TELEMETRY_SERVICES_VIEW_EXCEPTIONS_UNRESOLVED,
[

View File

@ -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);
}
}
}