UNPKG

6.1 kBJavaScriptView Raw
1import { AppError } from "./error.js";
2import { isNil } from "./lodash.js";
3import { _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 */
78function 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 */
112export 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 */
124export 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 */
158export 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 */
194export 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 */
225export 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 */
251function getEventRoot(event) {
252 return isNil(event.rootEvent) ? event : event.rootEvent;
253}