1 | import { AppError } from "./error.js";
|
2 | import { isNil } from "./lodash.js";
|
3 | import { _compasSentryExport } from "./sentry.js";
|
4 |
|
5 | /**
|
6 | * @typedef {object} InsightEventSpan
|
7 | * @property {string} name
|
8 | * @property {number} [duration]
|
9 | * @property {number} startTime
|
10 | * @property {number} [stopTime]
|
11 | * @property {number} [abortedTime]
|
12 | * @property {InsightEventSpan[]} children
|
13 | */
|
14 |
|
15 | /**
|
16 | * The insight event is a tool for tracking the duration of (async) functions manually.
|
17 | * By utilizing the insight event, you can gain access to a task or request-specific
|
18 | * logger and obtain insights into the execution time of your functions.
|
19 | *
|
20 | * How to use the Insight Event:
|
21 | *
|
22 | * Start by retrieving a root event. It can be created by calling {@link newEvent}
|
23 | * and passing it a logger. When you use the {@link getApp} from @compas/store,
|
24 | * it automatically adds a root event to `ctx.event`.
|
25 | * In your tests you can use {@link newTestEvent}.
|
26 | *
|
27 | * You could pass the event object down through your (async) functions as an argument.
|
28 | * This allows the insight event to associate the event with the specific task or
|
29 | * request.
|
30 | *
|
31 | * Finally, you should stop the event for correct logging by calling {@link eventStop}.
|
32 | * When the root event is stopped via {@link eventStop} it calculates the duration
|
33 | * by subtracting the start time from the end time. the event can log the start
|
34 | * and end times of the function execution if necessary.
|
35 | *
|
36 | * @example
|
37 | * async function userList(event) {
|
38 | * eventStart(event, "user.list");
|
39 | *
|
40 | * const totol = await userCount(newEventFromEvent(event));
|
41 | * const users = await queryUser({}).exec(sql);
|
42 | *
|
43 | * eventStop(event);
|
44 | *
|
45 | * return { total, users };
|
46 | * }
|
47 | *
|
48 | * // Logs something like:
|
49 | * // {
|
50 | * // name: "user.list",
|
51 | * // duration: 25,
|
52 | * // startTime: 1685000000000
|
53 | * // stopTime: 1685000000025
|
54 | * // children: [
|
55 | * // {
|
56 | * // name: "user.count",
|
57 | * // duration: 5,
|
58 | * // startTime: 1685000000010
|
59 | * // stopTime: 1685000000015
|
60 | * // }
|
61 | * // ]
|
62 | * // }
|
63 | * @typedef {object} InsightEvent
|
64 | * @property {import("@compas/stdlib").Logger} log
|
65 | * @property {AbortSignal} [signal]
|
66 | * @property {InsightEvent} [rootEvent]
|
67 | * @property {string} [name]
|
68 | * @property {InsightEventSpan} span
|
69 | * @property {import("@sentry/node").Span} [_compasSentrySpan]
|
70 | */
|
71 |
|
72 | /**
|
73 | *
|
74 | * @param {import("./logger.js").Logger} logger
|
75 | * @param {AbortSignal|undefined} [signal]
|
76 | * @returns {InsightEvent}
|
77 | */
|
78 | function InsightEventConstructor(logger, signal) {
|
79 | return {
|
80 | log: logger,
|
81 | signal,
|
82 | root: undefined,
|
83 | name: undefined,
|
84 | span: {
|
85 | // @ts-expect-error
|
86 | name: undefined,
|
87 |
|
88 | duration: undefined,
|
89 |
|
90 | // @ts-expect-error
|
91 | startTime: undefined,
|
92 |
|
93 | stopTime: undefined,
|
94 | abortedTime: undefined,
|
95 | children: [],
|
96 | },
|
97 |
|
98 | _compasSentrySpan: undefined,
|
99 | };
|
100 | }
|
101 |
|
102 | /**
|
103 | * Create a new event from a logger
|
104 | *
|
105 | * @since 0.1.0
|
106 | *
|
107 | * @param {import("./logger.js").Logger} logger Logger should have a
|
108 | * context, like the default `ctx.log`
|
109 | * @param {AbortSignal|undefined} [signal]
|
110 | * @returns {InsightEvent}
|
111 | */
|
112 | export function newEvent(logger, signal) {
|
113 | return InsightEventConstructor(logger, signal);
|
114 | }
|
115 |
|
116 | /**
|
117 | * Create a 'child' event, reuses the logger, adds it als a child to the passed event
|
118 | *
|
119 | * @since 0.1.0
|
120 | *
|
121 | * @param {InsightEvent} event
|
122 | * @returns {InsightEvent}
|
123 | */
|
124 | export function newEventFromEvent(event) {
|
125 | if (event.signal?.aborted) {
|
126 | event.span.abortedTime = Date.now();
|
127 |
|
128 | if (event._compasSentrySpan) {
|
129 | event._compasSentrySpan.end();
|
130 | }
|
131 |
|
132 | throw AppError.serverError({
|
133 | message: "Operation aborted",
|
134 | span: getEventRoot(event).span,
|
135 | });
|
136 | }
|
137 |
|
138 | const newEvent = InsightEventConstructor(event.log, event.signal);
|
139 |
|
140 | // Add ot parent
|
141 | event.span.children.push(newEvent.span);
|
142 |
|
143 | // Set root
|
144 | newEvent.rootEvent = event.rootEvent ?? event;
|
145 |
|
146 | return newEvent;
|
147 | }
|
148 |
|
149 | /**
|
150 | * Track event start times
|
151 | *
|
152 | * @since 0.1.0
|
153 | *
|
154 | * @param {InsightEvent} event
|
155 | * @param {string} name
|
156 | * @returns {void}
|
157 | */
|
158 | export function eventStart(event, name) {
|
159 | event.name = name;
|
160 | event.span.name = name;
|
161 | event.span.startTime = Date.now();
|
162 |
|
163 | if (typeof _compasSentryExport?.startInactiveSpan === "function") {
|
164 | event._compasSentrySpan = _compasSentryExport.startInactiveSpan({
|
165 | op: "event",
|
166 | name: name,
|
167 | description: name,
|
168 | });
|
169 | }
|
170 |
|
171 | if (event.signal?.aborted) {
|
172 | event.span.abortedTime = Date.now();
|
173 |
|
174 | if (event._compasSentrySpan) {
|
175 | event._compasSentrySpan.end();
|
176 | }
|
177 |
|
178 | throw AppError.serverError({
|
179 | message: "Operation aborted",
|
180 | span: getEventRoot(event).span,
|
181 | });
|
182 | }
|
183 | }
|
184 |
|
185 | /**
|
186 | * Rename an event
|
187 | *
|
188 | * @since 0.1.0
|
189 | *
|
190 | * @param {InsightEvent} event
|
191 | * @param {string} name
|
192 | * @returns {void}
|
193 | */
|
194 | export function eventRename(event, name) {
|
195 | event.name = name;
|
196 | event.span.name = name;
|
197 |
|
198 | if (event._compasSentrySpan) {
|
199 | event._compasSentrySpan.description = name;
|
200 | event._compasSentrySpan.updateName(name);
|
201 | }
|
202 |
|
203 | if (event.signal?.aborted) {
|
204 | event.span.abortedTime = Date.now();
|
205 |
|
206 | if (event._compasSentrySpan) {
|
207 | event._compasSentrySpan.end();
|
208 | }
|
209 |
|
210 | throw AppError.serverError({
|
211 | message: "Operation aborted",
|
212 | span: getEventRoot(event).span,
|
213 | });
|
214 | }
|
215 | }
|
216 |
|
217 | /**
|
218 | * Track event end times and log if necessary
|
219 | *
|
220 | * @since 0.1.0
|
221 | *
|
222 | * @param {InsightEvent} event
|
223 | * @returns {void}
|
224 | */
|
225 | export function eventStop(event) {
|
226 | event.span.stopTime = Date.now();
|
227 |
|
228 | if (event.span.startTime && event.span.stopTime) {
|
229 | event.span.duration = event.span.stopTime - event.span.startTime;
|
230 | }
|
231 |
|
232 | if (event._compasSentrySpan) {
|
233 | event._compasSentrySpan.end();
|
234 | }
|
235 |
|
236 | if (isNil(event.rootEvent)) {
|
237 | event.log.info({
|
238 | type: "event_span",
|
239 | aborted: !!event.signal?.aborted,
|
240 | span: event.span,
|
241 | });
|
242 | }
|
243 | }
|
244 |
|
245 | /**
|
246 | * Get the root event from the provided event
|
247 | *
|
248 | * @param {InsightEvent} event
|
249 | * @returns {InsightEvent}
|
250 | */
|
251 | function getEventRoot(event) {
|
252 | return isNil(event.rootEvent) ? event : event.rootEvent;
|
253 | }
|