Skip to content

Commit b18f5ea

Browse files
committed
[#389] Support Exception Chain Metadata (PExceptionMetaData)
1 parent 7ce1278 commit b18f5ea

File tree

5 files changed

+166
-12
lines changed

5 files changed

+166
-12
lines changed

lib/context/trace/exception-builder.js

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,57 @@
77
'use strict'
88

99
const { nextExceptionId } = require('./id-generator')
10+
const { namedGroupCallSite } = require('../../instrumentation/call-stack')
1011

1112
class Exception {
12-
constructor(className, message, startTime) {
13-
this.className = className
14-
this.message = message
13+
constructor(errorClassName, errorMessage, startTime) {
14+
this.errorClassName = errorClassName
15+
this.errorMessage = errorMessage
1516
this.startTime = startTime
1617
this.exceptionId = nextExceptionId()
1718
this.exceptionDepth = 1
1819
this.frameStack = []
1920
}
2021
}
2122

23+
const nullErrorClassName = 'unknown'
24+
const nullErrorMessage = ''
25+
2226
class ExceptionBuilder {
2327
constructor(error) {
2428
this.error = error
2529
}
2630

2731
build() {
28-
return new Exception(
29-
this.error.name || 'Error',
30-
this.error.message || '',
31-
Date.now()
32-
)
32+
if (!this.error || typeof this.error.stack !== 'string') {
33+
return new Exception(nullErrorClassName, nullErrorMessage, Date.now())
34+
}
35+
36+
const stack = this.error.stack.split(/\r?\n/)
37+
if (stack.length < 1) {
38+
return new Exception(nullErrorClassName, nullErrorMessage, Date.now())
39+
}
40+
41+
const className = this.extractErrorClassName(stack[0])
42+
const message = this.error.message ?? nullErrorMessage
43+
44+
const exception = new Exception(className, message, Date.now())
45+
exception.frameStack = stack.slice(1).map(callSite => {
46+
return namedGroupCallSite(callSite.trim())
47+
})
48+
exception.stack = stack
49+
return exception
3350
}
51+
52+
extractErrorClassName(firstLine) {
53+
if (!firstLine || typeof firstLine !== 'string') {
54+
return nullErrorClassName
55+
}
56+
57+
const match = /^([^:\s]+):/.exec(firstLine)
58+
return match && match[1] ? match[1].trim() : nullErrorClassName
59+
}
60+
3461
}
3562

3663
module.exports = {

lib/context/trace/span-event-builder.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ class SpanEventBuilder {
189189
return this
190190
}
191191

192+
setException(exception) {
193+
this.exception = exception
194+
return this
195+
}
196+
192197
build() {
193198
if (this.stackId === StackId.nullObject) {
194199
return SpanEvent.nullObject

lib/context/trace/span-event-recorder.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const AnnotationKeyUtils = require('../annotation-key-utils')
1414
const sqlMetadataService = require('../../instrumentation/sql/sql-metadata-service')
1515
const stringMetaService = require('../string-meta-service')
1616
const AsyncId = require('../async-id')
17+
const { ExceptionBuilder } = require('./exception-builder')
1718

1819
class SpanEventRecorder {
1920
static nullObject(traceRoot) {
@@ -120,6 +121,8 @@ class SpanEventRecorder {
120121
const shared = this.traceRoot.getShared()
121122
shared.maskErrorCode(1)
122123
}
124+
125+
this.spanEventBuilder.setException(new ExceptionBuilder(error).build())
123126
}
124127

125128
recordSqlInfo(sql, bindString) {

lib/instrumentation/call-stack.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ function arrayOfStackTrace(stackTrace) {
4444
}
4545

4646
function namedGroupLocationFileName(callSites, index) {
47-
const namedGroup = captureLocationFileNameNamedGroup(callSiteOf(callSites, index))
47+
const callSite = callSiteOf(callSites, index)
48+
return namedGroupLocationFileNameCallSite(callSite)
49+
}
50+
51+
function namedGroupLocationFileNameCallSite(callSite) {
52+
const namedGroup = captureLocationFileNameNamedGroup(callSite)
4853
const returnedValue = {}
4954
if (namedGroup) {
5055
returnedValue.fileName = namedGroup.fileName
@@ -102,6 +107,10 @@ const makeLineNumber = (namedGroups) => {
102107

103108
function namedGroupTypeMethod(callSites, index) {
104109
const callSite = callSiteOf(callSites, index)
110+
return namedGroupTypeMethodCallSite(callSite)
111+
}
112+
113+
function namedGroupTypeMethodCallSite(callSite) {
105114
const functionNameMatches = [newFunctionName, asyncFunctionName, typeFunctionNameMethodName]
106115
let group
107116
for (const match of functionNameMatches) {
@@ -114,11 +123,18 @@ function namedGroupTypeMethod(callSites, index) {
114123
if (!group || !group.groups) {
115124
return {}
116125
}
117-
return group.groups
126+
127+
const result = {}
128+
const { type, functionName, methodName } = group.groups
129+
if (type !== undefined) result.type = type
130+
if (functionName !== undefined) result.functionName = functionName
131+
if (methodName !== undefined) result.methodName = methodName
132+
133+
return result
118134
}
119135

120136
const typeFunctionNameMethodName = (callSite) => {
121-
return callSite.match(/at (?<type>\w+(?=\.))?\.?(?<functionName>[^\s]+)(?: \[as (?<methodName>\w+)\])?/)
137+
return callSite.match(/at (?<type>\w+(?=\.))?\.?(?<functionName>[^:/\s(]+)(?: \[as (?<methodName>\w+)\])?/)
122138
}
123139

124140
const newFunctionName = (callSite) => {
@@ -152,9 +168,16 @@ const asyncFunctionName = (callSite) => {
152168
return { 'groups': { 'functionName': functionName } }
153169
}
154170

171+
function namedGroupCallSite(callSite) {
172+
const namedGroupLocation = namedGroupLocationFileNameCallSite(callSite)
173+
const namedGroupTypeMethod = namedGroupTypeMethodCallSite(callSite)
174+
return { ...namedGroupLocation, ...namedGroupTypeMethod }
175+
}
176+
155177
module.exports = {
156178
callSite,
157179
namedGroupLocationFileName,
158180
namedGroupTypeMethod,
159-
makeCloneBuilder
181+
makeCloneBuilder,
182+
namedGroupCallSite
160183
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* Pinpoint Node.js Agent
3+
* Copyright 2020-present NAVER Corp.
4+
* Apache License v2.0
5+
*/
6+
7+
const test = require('tape')
8+
const { ExceptionBuilder } = require('../../../lib/context/trace/exception-builder')
9+
10+
const error = new Error('error case')
11+
error.stack = `Error: error case
12+
at /Users/workspace/pinpoint/pinpoint-node-agent/test/instrumentation/module/express.test.js:110:11
13+
at Layer.handle [as handle_request] (/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/layer.js:95:5)
14+
at next (/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/route.js:149:13)
15+
at Route.dispatch (/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/route.js:119:3)
16+
at InterceptorRunner.run (/Users/workspace/pinpoint/pinpoint-node-agent/lib/instrumentation/interceptor-runner.js:59:38)
17+
at wrapped (/Users/workspace/pinpoint/pinpoint-node-agent/lib/instrumentation/module/express/express-layer-interceptor.js:41:87)
18+
at Layer.handle [as handle_request] (/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/layer.js:95:5)
19+
at /Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/index.js:284:15
20+
at Function.process_params (/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/index.js:346:12)
21+
at next (/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/index.js:280:10)`
22+
23+
test('ExceptionBuilder Test - snapshot-like multiline', (t) => {
24+
t.plan(13)
25+
const actual = new ExceptionBuilder(error).build()
26+
t.equal(actual.errorClassName, 'Error', `Error class name is ${actual.errorClassName}`)
27+
t.equal(actual.errorMessage, 'error case', `Error message is ${actual.errorMessage}`)
28+
t.equal(actual.frameStack.length, 10, `Frame stack length is ${actual.frameStack.length}`)
29+
30+
t.deepEqual(actual.frameStack[0], {
31+
location: '/Users/workspace/pinpoint/pinpoint-node-agent/test/instrumentation/module/express.test.js',
32+
fileName: 'express.test.js',
33+
lineNumber: 110,
34+
}, 'at /Users/workspace/pinpoint/pinpoint-node-agent/test/instrumentation/module/express.test.js:110:11')
35+
t.deepEqual(actual.frameStack[1], {
36+
location: '/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/layer.js',
37+
fileName: 'layer.js',
38+
lineNumber: 95,
39+
type: 'Layer',
40+
functionName: 'handle',
41+
methodName: 'handle_request'
42+
}, 'at Layer.handle [as handle_request] (/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/layer.js:95:5)')
43+
t.deepEqual(actual.frameStack[2], {
44+
location: '/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/route.js',
45+
fileName: 'route.js',
46+
lineNumber: 149,
47+
functionName: 'next'
48+
}, 'at next (/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/route.js:149:13)')
49+
t.deepEqual(actual.frameStack[3], {
50+
location: '/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/route.js',
51+
fileName: 'route.js',
52+
lineNumber: 119,
53+
type: 'Route',
54+
functionName: 'dispatch'
55+
}, 'at Route.dispatch (/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/route.js:119:3)')
56+
t.deepEqual(actual.frameStack[4], {
57+
location: '/Users/workspace/pinpoint/pinpoint-node-agent/lib/instrumentation/interceptor-runner.js',
58+
fileName: 'interceptor-runner.js',
59+
lineNumber: 59,
60+
type: 'InterceptorRunner',
61+
functionName: 'run'
62+
}, 'at InterceptorRunner.run (/Users/workspace/pinpoint/pinpoint-node-agent/lib/instrumentation/interceptor-runner.js:59:38)')
63+
t.deepEqual(actual.frameStack[5], {
64+
location: '/Users/workspace/pinpoint/pinpoint-node-agent/lib/instrumentation/module/express/express-layer-interceptor.js',
65+
fileName: 'express-layer-interceptor.js',
66+
lineNumber: 41,
67+
functionName: 'wrapped'
68+
}, 'at wrapped (/Users/workspace/pinpoint/pinpoint-node-agent/lib/instrumentation/module/express/express-layer-interceptor.js:41:87)')
69+
t.deepEqual(actual.frameStack[6], {
70+
location: '/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/layer.js',
71+
fileName: 'layer.js',
72+
lineNumber: 95,
73+
type: 'Layer',
74+
functionName: 'handle',
75+
methodName: 'handle_request'
76+
}, 'at Layer.handle [as handle_request] (/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/layer.js:95:5)')
77+
t.deepEqual(actual.frameStack[7], {
78+
location: '/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/index.js',
79+
fileName: 'index.js',
80+
lineNumber: 284,
81+
}, 'at /Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/index.js:284:15')
82+
t.deepEqual(actual.frameStack[8], {
83+
location: '/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/index.js',
84+
fileName: 'index.js',
85+
lineNumber: 346,
86+
functionName: 'process_params',
87+
type: 'Function',
88+
}, 'at Function.process_params (/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/index.js:346:12)')
89+
t.deepEqual(actual.frameStack[9], {
90+
location: '/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/index.js',
91+
fileName: 'index.js',
92+
lineNumber: 280,
93+
functionName: 'next'
94+
}, 'at next (/Users/workspace/pinpoint/pinpoint-node-agent/node_modules/express/lib/router/index.js:280:10)')
95+
96+
})

0 commit comments

Comments
 (0)