-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathpatch-layer.ts
More file actions
271 lines (248 loc) · 9.83 KB
/
patch-layer.ts
File metadata and controls
271 lines (248 loc) · 9.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
/**
* Platform-portable Express tracing integration.
*
* @module
*
* This Sentry integration is a derivative work based on the OpenTelemetry
* Express instrumentation.
*
* <https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-express>
*
* Extended under the terms of the Apache 2.0 license linked below:
*
* ----
*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DEBUG_BUILD } from '../../debug-build';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes';
import { SPAN_STATUS_ERROR, startSpanManual, withActiveSpan } from '../../tracing';
import { debug } from '../../utils/debug-logger';
import type { SpanAttributes } from '../../types-hoist/span';
import { getActiveSpan } from '../../utils/spanUtils';
import { getStoredLayers, storeLayer } from './request-layer-store';
import {
type ExpressRequest,
type ExpressResponse,
type ExpressIntegrationOptions,
type ExpressLayer,
ATTR_HTTP_ROUTE,
ATTR_EXPRESS_TYPE,
ATTR_EXPRESS_NAME,
ExpressLayerType_ROUTER,
} from './types';
import {
asErrorAndMessage,
getActualMatchedRoute,
getConstructedRoute,
getLayerMetadata,
isLayerIgnored,
} from './utils';
import { getIsolationScope } from '../../currentScopes';
import { getDefaultIsolationScope } from '../../defaultScopes';
import { getOriginalFunction, markFunctionWrapped } from '../../utils/object';
import { setSDKProcessingMetadata } from './set-sdk-processing-metadata';
export type ExpressPatchLayerOptions = Pick<
ExpressIntegrationOptions,
'onRouteResolved' | 'ignoreLayers' | 'ignoreLayersType'
>;
export function patchLayer(
getOptions: () => ExpressPatchLayerOptions,
maybeLayer?: ExpressLayer,
layerPath?: string,
): void {
if (!maybeLayer?.handle) {
return;
}
const layer = maybeLayer;
const layerHandleOriginal = layer.handle;
// avoid patching multiple times the same layer
if (getOriginalFunction(layerHandleOriginal)) {
return;
}
if (layerHandleOriginal.length === 4) {
// todo: instrument error handlers
return;
}
function layerHandlePatched(
this: ExpressLayer,
req: ExpressRequest,
res: ExpressResponse,
//oxlint-disable-next-line no-explicit-any
...otherArgs: any[]
) {
const options = getOptions();
// Set normalizedRequest here because expressRequestHandler middleware
// (registered via setupExpressErrorHandler) is added after routes and
// therefore never runs for successful requests — route handlers typically
// send a response without calling next(). It would be safe to set this
// multiple times, since the data is identical, but more performant not to.
setSDKProcessingMetadata(req);
// Only create spans when there's an active parent span
// Without a parent span, this request is being ignored, so skip it
const parentSpan = getActiveSpan();
if (!parentSpan) {
return layerHandleOriginal.apply(this, [req, res, ...otherArgs]);
}
if (layerPath) {
storeLayer(req, layerPath);
}
const storedLayers = getStoredLayers(req);
const isLayerPathStored = !!layerPath;
const constructedRoute = getConstructedRoute(req);
const actualMatchedRoute = getActualMatchedRoute(req, constructedRoute);
options.onRouteResolved?.(actualMatchedRoute);
const metadata = getLayerMetadata(constructedRoute, layer, layerPath);
const name = metadata.attributes[ATTR_EXPRESS_NAME];
const type = metadata.attributes[ATTR_EXPRESS_TYPE];
const attributes: SpanAttributes = Object.assign(metadata.attributes, {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.express',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.express`,
});
if (actualMatchedRoute) {
attributes[ATTR_HTTP_ROUTE] = actualMatchedRoute;
}
// verify against the config if the layer should be ignored
if (isLayerIgnored(metadata.attributes[ATTR_EXPRESS_NAME], type, options)) {
// XXX: the isLayerPathStored guard here is *not* present in the
// original @opentelemetry/instrumentation-express impl, but was
// suggested by the Sentry code review bot. It appears to correctly
// prevent improper layer calculation in the case where there's a
// middleware without a layerPath argument. It's unclear whether
// that's possible, or if any existing code depends on that "bug".
if (isLayerPathStored) {
storedLayers.pop();
}
return layerHandleOriginal.apply(this, [req, res, ...otherArgs]);
}
const currentScope = getIsolationScope();
if (currentScope !== getDefaultIsolationScope()) {
if (type === 'request_handler') {
// type cast b/c Otel unfortunately types info.request as any :(
const method = req.method ? req.method.toUpperCase() : 'GET';
currentScope.setTransactionName(`${method} ${constructedRoute}`);
}
} else {
DEBUG_BUILD && debug.warn('Isolation scope is still default isolation scope - skipping setting transactionName');
}
return startSpanManual({ name, attributes }, span => {
let spanHasEnded = false;
// TODO: Fix router spans (getRouterPath does not work properly) to
// have useful names before removing this branch
if (metadata.attributes[ATTR_EXPRESS_TYPE] === ExpressLayerType_ROUTER) {
span.end();
spanHasEnded = true;
}
// listener for response.on('finish')
const onResponseFinish = () => {
if (!spanHasEnded) {
spanHasEnded = true;
span.end();
}
};
// verify we have a callback
for (let i = 0; i < otherArgs.length; i++) {
const callback = otherArgs[i] as Function;
if (typeof callback !== 'function') {
continue;
}
//oxlint-disable-next-line no-explicit-any
otherArgs[i] = function (...args: any[]) {
// express considers anything but an empty value, "route" or "router"
// passed to its callback to be an error
const maybeError = args[0];
const isError = !!maybeError && maybeError !== 'route' && maybeError !== 'router';
if (!spanHasEnded && isError) {
const [_, message] = asErrorAndMessage(maybeError);
// intentionally do not record the exception here, because
// the error handler we assign does that, provided the user
// correctly calls setupExpressErrorHandler.
// TODO: A future enhancement can automatically attach
// the error handler if we detect that it has not been added.
span.setStatus({
code: SPAN_STATUS_ERROR,
message,
});
}
if (!spanHasEnded) {
spanHasEnded = true;
res.removeListener('finish', onResponseFinish);
span.end();
}
if (!(req.route && isError) && isLayerPathStored) {
storedLayers.pop();
}
// execute the callback back in the parent's scope, so that
// we bubble up each level as next() is called.
return withActiveSpan(parentSpan, () => callback.apply(this, args));
};
break;
}
try {
return layerHandleOriginal.apply(this, [req, res, ...otherArgs]);
} catch (anyError) {
const [_, message] = asErrorAndMessage(anyError);
// intentionally do not record the exception here, because
// the error handler we assign does that, provided the user
// correctly calls setupExpressErrorHandler.
// TODO: A future enhancement can automatically attach
// the error handler if we detect that it has not been added.
span.setStatus({
code: SPAN_STATUS_ERROR,
message,
});
throw anyError;
/* v8 ignore next - it sees the block end at the throw */
} finally {
// At this point if the callback wasn't called, that means
// either the layer is asynchronous (so it will call the
// callback later on) or that the layer directly ends the
// http response, so we'll hook into the "finish" event to
// handle the later case.
if (!spanHasEnded) {
res.once('finish', onResponseFinish);
}
}
});
}
// `handle` isn't just a regular function in some cases. It also contains
// some properties holding metadata and state so we need to proxy them
// through through patched function. Use a for-in to also pick up properties
// that other libraries might add to the prototype before we instrument.
// ref: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/1950
// ref: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2271
// oxlint-disable-next-line guard-for-in
for (const key in layerHandleOriginal as Function & Record<string, unknown>) {
// skip standard function prototype fields that both have
if (key in layerHandlePatched) {
continue;
}
Object.defineProperty(layerHandlePatched, key, {
get() {
return layerHandleOriginal[key];
},
set(value) {
layerHandleOriginal[key] = value;
},
});
}
markFunctionWrapped(layerHandlePatched, layerHandleOriginal);
Object.defineProperty(layer, 'handle', {
enumerable: true,
configurable: true,
writable: true,
value: layerHandlePatched,
});
}