forked from googleapis/cloud-trace-nodejs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathutil.ts
343 lines (317 loc) · 10.9 KB
/
util.ts
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
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
// Copyright 2015 Google LLC
//
// 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
//
// http://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 * as path from 'path';
import * as sourceMapSupport from 'source-map-support';
const {
hexToDec,
decToHex,
}: // eslint-disable-next-line @typescript-eslint/no-var-requires
{[key: string]: (input: string) => string} = require('hex2dec');
export {hexToDec, decToHex};
// This symbol must be exported (for now).
// See: https://github.com/Microsoft/TypeScript/issues/20080
export const kSingleton = Symbol();
/**
* Trace API expects stack frames to be a JSON string with the following
* structure:
* STACK_TRACE := { "stack_frame" : [ FRAMES ] }
* FRAMES := { "class_name" : CLASS_NAME, "file_name" : FILE_NAME,
* "line_number" : LINE_NUMBER, "method_name" : METHOD_NAME }*
*
* While the API doesn't expect a column_number at this point, it does accept,
* and ignore it.
*/
export interface StackFrame {
class_name?: string;
method_name?: string;
file_name?: string;
line_number?: number;
column_number?: number;
}
export interface Constructor<T, ConfigType, LoggerType> {
new (config: ConfigType, logger: LoggerType): T;
prototype: T;
name: string;
}
export const FORCE_NEW = Symbol('force-new');
export type Forceable<T> = T & {[FORCE_NEW]?: boolean};
export interface Component {
enable(): void;
disable(): void;
}
/**
* A class that provides access to a singleton.
* We assume that any such singleton is always constructed with two arguments:
* An arbitrary configuration object and a logger.
* Instances of this type should only be constructed in module scope.
*/
export class Singleton<T, ConfigType, LoggerType> {
// Note: private[symbol] is enforced by clang-format.
private [kSingleton]: T | null = null;
constructor(private implementation: Constructor<T, ConfigType, LoggerType>) {}
create(config: Forceable<ConfigType>, logger: LoggerType): T {
if (!this[kSingleton] || config[FORCE_NEW]) {
const s = this[kSingleton] as Partial<Component>;
if (s && s.disable) {
s.disable();
}
this[kSingleton] = new this.implementation(config, logger);
return this[kSingleton]!;
} else {
throw new Error(`${this.implementation.name} has already been created.`);
}
}
get(): T {
if (!this[kSingleton]) {
throw new Error(`${this.implementation.name} has not yet been created.`);
}
return this[kSingleton]!;
}
exists(): boolean {
return !!this[kSingleton];
}
}
/**
* Returns the last parameter that is not null, undefined, or NaN.
* @param defaultValue The first parameter. This must not be null/undefined/NaN.
* @param otherValues Other parameters, which may be null/undefined/NaN.
*/
export function lastOf<T>(
defaultValue: T,
...otherValues: Array<T | null | undefined>
): T {
for (let i = otherValues.length - 1; i >= 0; i--) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (
otherValues[i] !== null &&
otherValues[i] !== undefined &&
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(typeof otherValues[i] !== 'number' || !isNaN(otherValues[i] as any))
) {
return otherValues[i] as T;
}
}
return defaultValue;
}
/**
* Truncates the provided `string` to be at most `length` bytes
* after utf8 encoding and the appending of '...'.
* We produce the result by iterating over input characters to
* avoid truncating the string potentially producing partial unicode
* characters at the end.
*/
export function truncate(str: string, length: number) {
if (Buffer.byteLength(str, 'utf8') <= length) {
return str;
}
str = str.substr(0, length - 3);
while (Buffer.byteLength(str, 'utf8') > length - 3) {
str = str.substr(0, str.length - 1);
}
return str + '...';
}
// Includes support for npm '@org/name' packages
// Regex: .*?node_modules(?!.*node_modules)\/(@[^\/]*\/[^\/]*|[^\/]*).*
// Tests: https://regex101.com/r/lW2bE3/6
const moduleRegex = new RegExp(
[
'.*?node_modules(?!.*node_modules)\\',
'(@[^\\',
']*\\',
'[^\\',
']*|[^\\',
']*).*',
].join(path.sep)
);
export interface TraceContext {
traceId: string;
spanId: string;
options?: number;
}
/**
* Parse a cookie-style header string to extract traceId, spandId and options
* ex: '123456/667;o=3'
* -> {traceId: '123456', spanId: '667', options: '3'}
* note that we ignore trailing garbage if there is more than one '='
* Returns null if traceId or spanId could not be found.
*
* @param str string representation of the trace headers
* @return object with keys. null if there is a problem.
*/
export function parseContextFromHeader(str: string): TraceContext | null {
if (!str) {
return null;
}
const matches = str.match(/^([0-9a-fA-F]+)(?:\/([0-9]+))(?:;o=(.*))?/);
if (
!matches ||
matches.length !== 4 ||
matches[0] !== str ||
(matches[2] && isNaN(Number(matches[2])))
) {
return null;
}
return {
traceId: matches[1],
spanId: matches[2],
options: isNaN(Number(matches[3])) ? undefined : Number(matches[3]),
};
}
/**
* Generates a trace context header value that can be used
* to follow the associated request through other Google services.
*
* @param traceContext An object with information sufficient for creating a
* serialized trace context.
*/
export function generateTraceContext(traceContext: TraceContext): string {
if (!traceContext) {
return '';
}
let header = `${traceContext.traceId}/${traceContext.spanId}`;
if (typeof traceContext.options !== 'undefined') {
header += `;o=${traceContext.options}`;
}
return header;
}
/**
* Retrieves a package name from the full import path.
* For example:
* './node_modules/bar/index/foo.js' => 'bar'
*
* @param path The full import path.
*/
export function packageNameFromPath(importPath: string) {
const matches = moduleRegex.exec(importPath);
return matches && matches.length > 1 ? matches[1] : null;
}
/**
* Creates a StackFrame object containing a structured stack trace.
* @param numFrames The number of frames to retain.
* @param skipFrames The number of top-most frames to remove.
* @param constructorOpt A function passed to Error.captureStackTrace, which
* causes it to ignore the frames above the top-most call to this function.
*/
export function createStackTrace(
numFrames: number,
skipFrames: number,
constructorOpt?: Function
): StackFrame[] {
// This is a mechanism to get the structured stack trace out of V8.
// prepareStackTrace is called the first time the Error#stack property is
// accessed. The original behavior is to format the stack as an exception
// throw, which is not what we like. We customize it.
//
// See: https://code.google.com/p/v8-wiki/wiki/JavaScriptStackTraceApi
//
if (numFrames === 0) {
return [];
}
const origLimit = Error.stackTraceLimit;
Error.stackTraceLimit = numFrames + skipFrames;
const origPrepare = Error.prepareStackTrace;
Error.prepareStackTrace = (
error: Error,
structured: NodeJS.CallSite[]
): NodeJS.CallSite[] => {
return structured.map(sourceMapSupport.wrapCallSite);
};
const e: {stack?: NodeJS.CallSite[]} = {};
Error.captureStackTrace(e, constructorOpt);
const stackFrames: StackFrame[] = [];
if (e.stack) {
e.stack.forEach((callSite, i) => {
if (i < skipFrames) {
return;
}
// TODO(kjin): Check if callSite getters actually return null or
// undefined. Docs say undefined but we guard it here just in case.
const functionName = callSite.getFunctionName();
const methodName = callSite.getMethodName();
const name =
methodName && functionName
? functionName + ' [as ' + methodName + ']'
: functionName || methodName || '<anonymous function>';
const stackFrame: StackFrame = {
method_name: name,
file_name: callSite.getFileName() || undefined,
line_number: callSite.getLineNumber() || undefined,
column_number: callSite.getColumnNumber() || undefined,
};
stackFrames.push(stackFrame);
});
}
Error.stackTraceLimit = origLimit;
Error.prepareStackTrace = origPrepare;
return stackFrames;
}
/**
* Serialize the given trace context into a Buffer.
* @param traceContext The trace context to serialize.
*/
export function serializeTraceContext(traceContext: TraceContext): Buffer {
// 0 1 2
// 0 1 2345678901234567 8 90123456 7 8
// -------------------------------------
// | | | | | | | |
// -------------------------------------
// ^ ^ ^ ^ ^ ^ ^
// | | | | | | `-- options value (traceContext.options)
// | | | | | `---- options field ID (2)
// | | | | `---------- spanID value (traceConext.spanID)
// | | | `--------------- spanID field ID (1)
// | | `--------------------------- traceID value (traceContext.traceID)
// | `---------------------------------- traceID field ID (0)
// `------------------------------------ version (0)
const result = Buffer.alloc(29, 0);
result.write(traceContext.traceId, 2, 16, 'hex');
result.writeUInt8(1, 18);
// Convert Span ID from decimal to base 16 representation, then left pad into
// a length-16 hex string. (decToHex prepends its output with '0x', so we
// also slice that off.)
const base16SpanId = `0000000000000000${decToHex(traceContext.spanId).slice(
2
)}`.slice(-16);
result.write(base16SpanId, 19, 8, 'hex');
result.writeUInt8(2, 27);
result.writeUInt8(traceContext.options || 0, 28);
return result;
}
/**
* Deseralize the given trace context from binary encoding. If the input is a
* Buffer of incorrect size or unexpected fields, then this function will return
* null.
* @param buffer The trace context to deserialize.
*/
export function deserializeTraceContext(buffer: Buffer): TraceContext | null {
const result: TraceContext = {traceId: '', spanId: ''};
// Length must be 29.
if (buffer.length !== 29) {
return null;
}
// Check version and field numbers.
if (
buffer.readUInt8(0) !== 0 ||
buffer.readUInt8(1) !== 0 ||
buffer.readUInt8(18) !== 1 ||
buffer.readUInt8(27) !== 2
) {
return null;
}
// See serializeTraceContext for byte offsets.
result.traceId = buffer.slice(2, 18).toString('hex');
result.spanId = hexToDec(buffer.slice(19, 27).toString('hex'));
result.options = buffer.readUInt8(28);
return result;
}