newrelic 13.2.1 → 13.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NEWS.md +44 -10
- package/api.js +1 -1
- package/lib/instrumentation/@azure/functions.js +8 -46
- package/lib/instrumentation/core/http-outbound.js +26 -10
- package/lib/instrumentation/core/http.js +20 -118
- package/lib/instrumentations.js +0 -14
- package/lib/otel/logs/index.js +80 -0
- package/lib/otel/metrics/index.js +119 -0
- package/lib/otel/setup-signal.js +37 -0
- package/lib/otel/setup.js +15 -25
- package/lib/otel/{attr-reconciler.js → traces/attr-reconciler.js} +2 -2
- package/lib/otel/{exception-mapping.js → traces/exception-mapping.js} +1 -1
- package/lib/otel/traces/index.js +40 -0
- package/lib/otel/{rules.js → traces/rules.js} +1 -1
- package/lib/otel/{segment-synthesis.js → traces/segment-synthesis.js} +5 -4
- package/lib/otel/{segments → traces/segments}/consumer.js +4 -4
- package/lib/otel/{segments → traces/segments}/database.js +6 -6
- package/lib/otel/{segments → traces/segments}/http-external.js +11 -8
- package/lib/otel/traces/segments/index.js +22 -0
- package/lib/otel/{segments → traces/segments}/internal.js +1 -1
- package/lib/otel/{segments → traces/segments}/producer.js +2 -2
- package/lib/otel/{segments → traces/segments}/server.js +4 -4
- package/lib/otel/{segments → traces/segments}/utils.js +1 -1
- package/lib/otel/{span-processor.js → traces/span-processor.js} +13 -19
- package/lib/otel/{transformation-rules.json → traces/transformation-rules.json} +3 -2
- package/lib/otel/{utils.js → traces/utils.js} +2 -1
- package/lib/serverless/aws-lambda.js +7 -68
- package/lib/shim/webframework-shim/middleware.js +1 -1
- package/lib/shimmer.js +1 -1
- package/lib/subscriber-configs.js +2 -1
- package/lib/subscribers/base.js +30 -0
- package/lib/subscribers/elasticsearch/config.js +1 -0
- package/lib/subscribers/mcp-sdk/client-request.js +66 -0
- package/lib/subscribers/mcp-sdk/config.js +13 -75
- package/lib/subscribers/openai/base.js +21 -0
- package/lib/subscribers/openai/chat-responses.js +15 -0
- package/lib/subscribers/openai/chat.js +85 -0
- package/lib/subscribers/openai/client.js +31 -0
- package/lib/subscribers/openai/config.js +85 -0
- package/lib/subscribers/openai/embeddings.js +54 -0
- package/lib/subscribers/openai/utils.js +349 -0
- package/lib/transaction/index.js +157 -56
- package/lib/transaction/trace/segment.js +5 -2
- package/lib/util/urltils.js +158 -192
- package/lib/utilization/common.js +0 -6
- package/package.json +1 -1
- package/lib/instrumentation/openai.js +0 -482
- package/lib/otel/logs/bootstrap-logs.js +0 -84
- package/lib/otel/metrics/bootstrap-metrics.js +0 -117
- package/lib/otel/segments/index.js +0 -22
- package/lib/subscribers/mcp-sdk/client-prompt.js +0 -23
- package/lib/subscribers/mcp-sdk/client-resource.js +0 -24
- package/lib/subscribers/mcp-sdk/client-tool.js +0 -23
- package/lib/subscribers/mcp-sdk/client.js +0 -29
- /package/lib/otel/{constants.js → traces/constants.js} +0 -0
- /package/lib/otel/{sampler.js → traces/sampler.js} +0 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
const {
|
|
8
|
+
LlmChatCompletionMessage,
|
|
9
|
+
LlmChatCompletionSummary,
|
|
10
|
+
LlmEmbedding,
|
|
11
|
+
LlmErrorMessage
|
|
12
|
+
} = require('../../llm-events/openai')
|
|
13
|
+
const { extractLlmContext } = require('../../util/llm-utils')
|
|
14
|
+
|
|
15
|
+
const { AI } = require('../../metrics/names')
|
|
16
|
+
const { OPENAI } = AI
|
|
17
|
+
const { DESTINATIONS } = require('../../config/attribute-filter')
|
|
18
|
+
let TRACKING_METRIC = OPENAI.TRACKING_PREFIX
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parses the response from OpenAI and extracts the message content and role.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} response The OpenAI SDK response object
|
|
24
|
+
* @returns {{ content: string, role: string }} the message object with fields `content` and `role`
|
|
25
|
+
*/
|
|
26
|
+
function getMessageFromResponse(response) {
|
|
27
|
+
let content
|
|
28
|
+
let role
|
|
29
|
+
if (response?.object === 'response') {
|
|
30
|
+
content = response?.output?.[0]?.content?.[0]?.text
|
|
31
|
+
role = response?.output?.[0]?.role
|
|
32
|
+
} else {
|
|
33
|
+
content = response?.choices?.[0]?.message?.content
|
|
34
|
+
role = response?.choices?.[0]?.message?.role
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { content, role }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parses all messages from the OpenAI request object.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} request The OpenAI SDK request object
|
|
44
|
+
* @param logger
|
|
45
|
+
* @returns {Array<object>} an array of message objects with fields `content` and `role`
|
|
46
|
+
*/
|
|
47
|
+
function getMessagesFromRequest(request, logger) {
|
|
48
|
+
// There are a few different ways to pass messages to OpenAI SDK.
|
|
49
|
+
//
|
|
50
|
+
// For langchain and `chat.completions.create`, messages are passed
|
|
51
|
+
// as an array of objects with `content` and `role` properties
|
|
52
|
+
// to the `messages` field of the request.
|
|
53
|
+
//
|
|
54
|
+
// For `responses.create`, messages are passed as an array of objects
|
|
55
|
+
// with `content` and `role` properties OR as a single string (implied
|
|
56
|
+
// to be a user message) to the `input` field of the request.
|
|
57
|
+
let messages = []
|
|
58
|
+
|
|
59
|
+
if (Array.isArray(request?.input)) {
|
|
60
|
+
// Handle array of input messages
|
|
61
|
+
messages = request.input.filter((msg) => msg?.content && msg?.role)
|
|
62
|
+
} else if (typeof request?.input === 'string') {
|
|
63
|
+
// Handle single string input as a user message
|
|
64
|
+
messages = [{ content: request.input, role: 'user' }]
|
|
65
|
+
} else if (Array.isArray(request?.messages)) {
|
|
66
|
+
// Handle array of messages
|
|
67
|
+
messages = request.messages.filter((msg) => msg?.content && msg?.role)
|
|
68
|
+
} else {
|
|
69
|
+
logger.warn('No valid messages found in OpenAI request object.')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return messages
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Enqueues a LLM event to the custom event aggregator
|
|
77
|
+
*
|
|
78
|
+
* @param {object} params input params
|
|
79
|
+
* @param {Agent} params.agent NR agent instance
|
|
80
|
+
* @param {string} params.type LLM event type
|
|
81
|
+
* @param {object} params.msg LLM event
|
|
82
|
+
*/
|
|
83
|
+
function recordEvent({ agent, type, msg }) {
|
|
84
|
+
const llmContext = extractLlmContext(agent)
|
|
85
|
+
|
|
86
|
+
agent.customEventAggregator.add([
|
|
87
|
+
{ type, timestamp: Date.now() },
|
|
88
|
+
Object.assign({}, msg, llmContext)
|
|
89
|
+
])
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Increments the tracking metric and sets the llm attribute on transactions
|
|
94
|
+
*
|
|
95
|
+
* @param {object} params input params
|
|
96
|
+
* @param {Agent} params.agent NR agent instance
|
|
97
|
+
* @param {Transaction} params.transaction active transaction
|
|
98
|
+
* @param params.version
|
|
99
|
+
*/
|
|
100
|
+
function addLlmMeta({ agent, transaction, version }) {
|
|
101
|
+
TRACKING_METRIC = `${TRACKING_METRIC}/${version}`
|
|
102
|
+
agent.metrics.getOrCreateMetric(TRACKING_METRIC).incrementCallCount()
|
|
103
|
+
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generates LlmChatCompletionSummary for a chat completion creation.
|
|
108
|
+
* Also iterates over both input messages and the first response message
|
|
109
|
+
* and creates LlmChatCompletionMessage.
|
|
110
|
+
*
|
|
111
|
+
* Also assigns relevant ids by response id for LlmFeedbackEvent creation
|
|
112
|
+
*
|
|
113
|
+
* @param {object} params input params
|
|
114
|
+
* @param {Agent} params.agent NR agent instance
|
|
115
|
+
* @param {TraceSegment} params.segment active segment from chat completion
|
|
116
|
+
* @param {object} params.request chat completion params
|
|
117
|
+
* @param {object} params.response chat completion response
|
|
118
|
+
* @param {boolean} [params.err] err if it exists
|
|
119
|
+
* @param {Transaction} params.transaction active transaction
|
|
120
|
+
* @param params.headers
|
|
121
|
+
* @param params.logger
|
|
122
|
+
*/
|
|
123
|
+
function recordChatCompletionMessages({
|
|
124
|
+
agent,
|
|
125
|
+
headers,
|
|
126
|
+
logger,
|
|
127
|
+
segment,
|
|
128
|
+
request,
|
|
129
|
+
response,
|
|
130
|
+
err,
|
|
131
|
+
transaction
|
|
132
|
+
}) {
|
|
133
|
+
if (shouldSkipInstrumentation(agent.config, logger) === true) {
|
|
134
|
+
logger.debug('skipping sending of ai data')
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!response) {
|
|
139
|
+
// If we get an error, it is possible that `response = null`.
|
|
140
|
+
// In that case, we define it to be an empty object.
|
|
141
|
+
response = {}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
response.headers = headers
|
|
145
|
+
// explicitly end segment to consistent duration
|
|
146
|
+
// for both LLM events and the segment
|
|
147
|
+
segment.end()
|
|
148
|
+
const completionSummary = new LlmChatCompletionSummary({
|
|
149
|
+
agent,
|
|
150
|
+
segment,
|
|
151
|
+
transaction,
|
|
152
|
+
request,
|
|
153
|
+
response,
|
|
154
|
+
withError: err != null
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Note: langchain still expects a message event even
|
|
158
|
+
// when the response is empty, so we will not filter
|
|
159
|
+
// the response message.
|
|
160
|
+
const messages = [
|
|
161
|
+
...getMessagesFromRequest(request, logger),
|
|
162
|
+
getMessageFromResponse(response)
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
messages.forEach((message, index) => {
|
|
166
|
+
const completionMsg = new LlmChatCompletionMessage({
|
|
167
|
+
agent,
|
|
168
|
+
segment,
|
|
169
|
+
transaction,
|
|
170
|
+
request,
|
|
171
|
+
response,
|
|
172
|
+
index,
|
|
173
|
+
completionId: completionSummary.id,
|
|
174
|
+
message
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
recordEvent({ agent, type: 'LlmChatCompletionMessage', msg: completionMsg })
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
recordEvent({ agent, type: 'LlmChatCompletionSummary', msg: completionSummary })
|
|
181
|
+
|
|
182
|
+
if (err) {
|
|
183
|
+
const llmError = new LlmErrorMessage({ cause: err, summary: completionSummary, response })
|
|
184
|
+
agent.errors.add(transaction, err, llmError)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
delete response.headers
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Checks if we should skip instrumentation.
|
|
192
|
+
* Currently, it checks if `ai_monitoring.enabled` is true
|
|
193
|
+
* and the package version >= 4.0.0
|
|
194
|
+
*
|
|
195
|
+
* @param {object} config agent config
|
|
196
|
+
* @param logger
|
|
197
|
+
* @returns {boolean} flag if instrumentation should be skipped
|
|
198
|
+
*/
|
|
199
|
+
function shouldSkipInstrumentation(config, logger) {
|
|
200
|
+
if (config?.ai_monitoring?.enabled !== true) {
|
|
201
|
+
logger.debug('config.ai_monitoring.enabled is set to false.')
|
|
202
|
+
return true
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* `chat.completions.create` can return a stream once promise resolves.
|
|
208
|
+
* This wraps the iterator which is a generator function.
|
|
209
|
+
* We will call the original iterator, intercept chunks and yield
|
|
210
|
+
* to the original. On complete, we will construct the new message object
|
|
211
|
+
* with what we have seen in the stream and create the chat completion
|
|
212
|
+
* messages.
|
|
213
|
+
* @param {object} params input params
|
|
214
|
+
* @param {Agent} params.agent NR agent instance
|
|
215
|
+
* @param {Shim} params.shim the current shim instance
|
|
216
|
+
* @param {object} params.request chat completion params
|
|
217
|
+
* @param {object} params.response chat completion response
|
|
218
|
+
* @param {TraceSegment} params.segment active segment from chat completion
|
|
219
|
+
* @param {Transaction} params.transaction active transaction
|
|
220
|
+
* @param {Error} params.err error if it exists
|
|
221
|
+
* @param params.headers
|
|
222
|
+
* @param params.logger
|
|
223
|
+
*/
|
|
224
|
+
function instrumentStream({ agent, headers, logger, request, response, segment, transaction, err = null }) {
|
|
225
|
+
if (!agent.config.ai_monitoring.streaming.enabled) {
|
|
226
|
+
logger.warn(
|
|
227
|
+
'`ai_monitoring.streaming.enabled` is set to `false`, stream will not be instrumented.'
|
|
228
|
+
)
|
|
229
|
+
agent.metrics.getOrCreateMetric(AI.STREAMING_DISABLED).incrementCallCount()
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (err) {
|
|
234
|
+
// If there is an error already e.g. APIConnectionError,
|
|
235
|
+
// the iterator will not be called, so we have to
|
|
236
|
+
// record the chat completion messages with the error now.
|
|
237
|
+
segment.touch()
|
|
238
|
+
recordChatCompletionMessages({
|
|
239
|
+
agent,
|
|
240
|
+
headers,
|
|
241
|
+
logger,
|
|
242
|
+
segment,
|
|
243
|
+
transaction,
|
|
244
|
+
request,
|
|
245
|
+
response,
|
|
246
|
+
err
|
|
247
|
+
})
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const orig = response.iterator
|
|
252
|
+
response.iterator = async function * wrappedIterator() {
|
|
253
|
+
let content = ''
|
|
254
|
+
let role = ''
|
|
255
|
+
let chunk
|
|
256
|
+
try {
|
|
257
|
+
const iterator = orig.apply(this, arguments)
|
|
258
|
+
|
|
259
|
+
for await (chunk of iterator) {
|
|
260
|
+
if (chunk.choices?.[0]?.delta?.role) {
|
|
261
|
+
role = chunk.choices[0].delta.role
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
content += chunk.choices?.[0]?.delta?.content ?? ''
|
|
265
|
+
yield chunk
|
|
266
|
+
}
|
|
267
|
+
} catch (streamErr) {
|
|
268
|
+
err = streamErr
|
|
269
|
+
throw err
|
|
270
|
+
} finally {
|
|
271
|
+
if (chunk?.choices) {
|
|
272
|
+
chunk.choices[0].message = { role, content }
|
|
273
|
+
} else if (chunk?.response) {
|
|
274
|
+
chunk = chunk.response
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// update segment duration since we want to extend the time it took to
|
|
278
|
+
// handle the stream
|
|
279
|
+
segment.touch()
|
|
280
|
+
|
|
281
|
+
recordChatCompletionMessages({
|
|
282
|
+
agent,
|
|
283
|
+
headers,
|
|
284
|
+
logger,
|
|
285
|
+
segment,
|
|
286
|
+
transaction,
|
|
287
|
+
request,
|
|
288
|
+
response: chunk,
|
|
289
|
+
err
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function recordEmbeddingMessage({
|
|
296
|
+
agent,
|
|
297
|
+
logger,
|
|
298
|
+
request,
|
|
299
|
+
headers,
|
|
300
|
+
response,
|
|
301
|
+
segment,
|
|
302
|
+
transaction,
|
|
303
|
+
err
|
|
304
|
+
}) {
|
|
305
|
+
if (!response) {
|
|
306
|
+
// If we get an error, it is possible that `response = null`.
|
|
307
|
+
// In that case, we define it to be an empty object.
|
|
308
|
+
response = {}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
segment.end()
|
|
312
|
+
if (shouldSkipInstrumentation(agent.config, logger) === true) {
|
|
313
|
+
// We need this check inside the wrapper because it is possible for monitoring
|
|
314
|
+
// to be disabled at the account level. In such a case, the value is set
|
|
315
|
+
// after the instrumentation has been initialized.
|
|
316
|
+
logger.debug('skipping sending of ai data')
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
response.headers = headers
|
|
321
|
+
// explicitly end segment to get consistent duration
|
|
322
|
+
// for both LLM events and the segment
|
|
323
|
+
|
|
324
|
+
const embedding = new LlmEmbedding({
|
|
325
|
+
agent,
|
|
326
|
+
segment,
|
|
327
|
+
transaction,
|
|
328
|
+
request,
|
|
329
|
+
response,
|
|
330
|
+
withError: err != null
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
recordEvent({ agent, type: 'LlmEmbedding', msg: embedding })
|
|
334
|
+
|
|
335
|
+
if (err) {
|
|
336
|
+
const llmError = new LlmErrorMessage({ cause: err, embedding, response })
|
|
337
|
+
agent.errors.add(transaction, err, llmError)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// cleanup keys on response before returning to user code
|
|
341
|
+
delete response.headers
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
module.exports = {
|
|
345
|
+
addLlmMeta,
|
|
346
|
+
instrumentStream,
|
|
347
|
+
recordChatCompletionMessages,
|
|
348
|
+
recordEmbeddingMessage
|
|
349
|
+
}
|
package/lib/transaction/index.js
CHANGED
|
@@ -16,8 +16,11 @@ const Timer = require('../timer')
|
|
|
16
16
|
const Trace = require('./trace')
|
|
17
17
|
const synthetics = require('../synthetics')
|
|
18
18
|
const urltils = require('../util/urltils')
|
|
19
|
+
const cat = require('../util/cat')
|
|
19
20
|
const TraceContext = require('./tracecontext').TraceContext
|
|
20
21
|
const Logs = require('./logs')
|
|
22
|
+
const headerAttributes = require('../header-attributes')
|
|
23
|
+
const headerProcessing = require('../header-processing')
|
|
21
24
|
const DT_ACCEPT_PAYLOAD_EXCEPTION_METRIC = 'DistributedTrace/AcceptPayload/Exception'
|
|
22
25
|
const DT_ACCEPT_PAYLOAD_PARSE_EXCEPTION_METRIC = 'DistributedTrace/AcceptPayload/ParseException'
|
|
23
26
|
const REQUEST_PARAMS_PATH = 'request.parameters.'
|
|
@@ -119,7 +122,6 @@ function Transaction(agent, traceId) {
|
|
|
119
122
|
this.syntheticsData = null
|
|
120
123
|
this.syntheticsInfoData = null
|
|
121
124
|
this.url = null
|
|
122
|
-
this.parsedUrl = null
|
|
123
125
|
this.verb = null
|
|
124
126
|
this.baseSegment = null
|
|
125
127
|
this.type = TYPES.WEB
|
|
@@ -317,8 +319,9 @@ Transaction.prototype.setPartialName = function setPartialName(name) {
|
|
|
317
319
|
* boolean flag in `ignore`.
|
|
318
320
|
*/
|
|
319
321
|
Transaction.prototype._partialNameFromUri = _partialNameFromUri
|
|
320
|
-
function _partialNameFromUri(
|
|
321
|
-
const scrubbedUrl =
|
|
322
|
+
function _partialNameFromUri() {
|
|
323
|
+
const scrubbedUrl = this.url
|
|
324
|
+
const status = this.statusCode
|
|
322
325
|
|
|
323
326
|
// 0. If there is a name in the name-state stack, use it.
|
|
324
327
|
let partialName = this._partialName
|
|
@@ -386,21 +389,69 @@ Transaction.prototype.isIgnored = function getIgnore() {
|
|
|
386
389
|
}
|
|
387
390
|
|
|
388
391
|
/**
|
|
389
|
-
*
|
|
392
|
+
* Finalize a web transaction. This includes naming the transaction, collecting
|
|
393
|
+
* response headers, and ending the transaction and its base segment.
|
|
394
|
+
*
|
|
395
|
+
* @param {object} params The parameters for finalizing the transaction.
|
|
396
|
+
* @param {number} params.statusCode The response status code.
|
|
397
|
+
* @param {string} [params.statusMessage] The response status message.
|
|
398
|
+
* @param {object} [params.headers] The response headers.
|
|
399
|
+
* @param {Boolean} params.end Whether to end the transaction and baseSegment.
|
|
400
|
+
*/
|
|
401
|
+
Transaction.prototype.finalizeWeb = function finalizeWeb({ statusCode, statusMessage, headers, end }) {
|
|
402
|
+
// Naming must happen before the segment and transaction are ended
|
|
403
|
+
// because metrics recording depends on naming's side effects.
|
|
404
|
+
this.finalizeNameFromWeb(statusCode)
|
|
405
|
+
if (statusCode != null) {
|
|
406
|
+
const responseCode = String(statusCode)
|
|
407
|
+
|
|
408
|
+
if (/^\d+$/.test(responseCode)) {
|
|
409
|
+
this.trace.attributes.addAttribute(
|
|
410
|
+
DESTS.TRANS_COMMON,
|
|
411
|
+
'http.statusCode',
|
|
412
|
+
responseCode
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
this.baseSegment.addSpanAttribute('http.statusCode', responseCode)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (statusMessage !== undefined) {
|
|
420
|
+
this.trace.attributes.addAttribute(
|
|
421
|
+
DESTS.TRANS_COMMON,
|
|
422
|
+
'http.statusText',
|
|
423
|
+
statusMessage
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
this.baseSegment.addSpanAttribute('http.statusText', statusMessage)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (headers) {
|
|
430
|
+
headerAttributes.collectResponseHeaders(headers, this)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (end) {
|
|
434
|
+
// And we are done! End the segment and transaction.
|
|
435
|
+
this.baseSegment.end()
|
|
436
|
+
this.end()
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Derives the transaction's name from `transaction.url` and status code.
|
|
390
442
|
*
|
|
391
443
|
* The transaction's name will be set after this as well as its ignored status
|
|
392
444
|
* based on the derived name.
|
|
393
445
|
*
|
|
394
|
-
* @param {string} requestURL - The URL to derive the request's name and status from.
|
|
395
446
|
* @param {number} statusCode - The response status code.
|
|
396
447
|
*/
|
|
397
|
-
Transaction.prototype.
|
|
448
|
+
Transaction.prototype.finalizeNameFromWeb = finalizeNameFromWeb
|
|
398
449
|
|
|
399
|
-
function
|
|
450
|
+
function finalizeNameFromWeb(statusCode) {
|
|
400
451
|
if (logger.traceEnabled()) {
|
|
401
452
|
logger.trace(
|
|
402
453
|
{
|
|
403
|
-
requestURL,
|
|
454
|
+
requestURL: this.url,
|
|
404
455
|
statusCode,
|
|
405
456
|
transactionId: this.id,
|
|
406
457
|
transactionName: this.name
|
|
@@ -409,10 +460,6 @@ function finalizeNameFromUri(requestURL, statusCode) {
|
|
|
409
460
|
)
|
|
410
461
|
}
|
|
411
462
|
|
|
412
|
-
if (!this.agent.config.url_obfuscation.enabled) {
|
|
413
|
-
this.url = urltils.scrub(requestURL)
|
|
414
|
-
}
|
|
415
|
-
|
|
416
463
|
this.statusCode = statusCode
|
|
417
464
|
this.name = this.getFullName()
|
|
418
465
|
|
|
@@ -426,7 +473,16 @@ function finalizeNameFromUri(requestURL, statusCode) {
|
|
|
426
473
|
this.ignore = this.forceIgnore
|
|
427
474
|
}
|
|
428
475
|
|
|
429
|
-
this.
|
|
476
|
+
const obfuscatedUrl = urltils.obfuscatePath(this.agent.config, this.url)
|
|
477
|
+
this.url = obfuscatedUrl
|
|
478
|
+
// URL is sent as an agent attribute with transaction events
|
|
479
|
+
this.trace.attributes.addAttribute(
|
|
480
|
+
DESTS.TRANS_EVENT | DESTS.ERROR_EVENT,
|
|
481
|
+
'request.uri',
|
|
482
|
+
obfuscatedUrl
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
this?.baseSegment?.markAsWeb(this, obfuscatedUrl)
|
|
430
486
|
|
|
431
487
|
this._copyNameToActiveSpan(this.name)
|
|
432
488
|
|
|
@@ -471,37 +527,6 @@ Transaction.prototype._copyNameToActiveSpan = function _copyNameToActiveSpan(nam
|
|
|
471
527
|
spanContext.addIntrinsicAttribute('transaction.name', name)
|
|
472
528
|
}
|
|
473
529
|
|
|
474
|
-
/**
|
|
475
|
-
* Copies final base segment parameters to trace attributes before reapplying
|
|
476
|
-
* them to the segment.
|
|
477
|
-
*
|
|
478
|
-
* Handles adding query parameters to `request.parameter.*` attributes
|
|
479
|
-
*
|
|
480
|
-
* @param {string} rawURL The URL, as it came in, for parameter extraction.
|
|
481
|
-
*/
|
|
482
|
-
Transaction.prototype._markAsWeb = function _markAsWeb(rawURL) {
|
|
483
|
-
// Because we are assured we have the URL here, lets grab query params.
|
|
484
|
-
const params = urltils.parseParameters(rawURL)
|
|
485
|
-
for (const key in params) {
|
|
486
|
-
if (props.hasOwn(params, key)) {
|
|
487
|
-
this.trace.attributes.addAttribute(DESTS.NONE, REQUEST_PARAMS_PATH + key, params[key])
|
|
488
|
-
|
|
489
|
-
const segment = this.agent.tracer.getSegment()
|
|
490
|
-
|
|
491
|
-
if (!segment) {
|
|
492
|
-
logger.trace(
|
|
493
|
-
'Active segment not available, not adding request.parameters span attribute for %s',
|
|
494
|
-
key
|
|
495
|
-
)
|
|
496
|
-
} else {
|
|
497
|
-
segment.attributes.addAttribute(DESTS.NONE, REQUEST_PARAMS_PATH + key, params[key])
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
this.baseSegment.markAsWeb(this)
|
|
503
|
-
}
|
|
504
|
-
|
|
505
530
|
/**
|
|
506
531
|
* Sets the transaction's name and determines if it will be ignored.
|
|
507
532
|
*
|
|
@@ -514,7 +539,7 @@ Transaction.prototype.finalizeName = function finalizeName(name) {
|
|
|
514
539
|
// If no name is given, and this is a web transaction with a url, then
|
|
515
540
|
// finalize the name using the stored url.
|
|
516
541
|
if (name == null && this.isWeb() && this.url) {
|
|
517
|
-
return this.
|
|
542
|
+
return this.finalizeNameFromWeb(this.statusCode)
|
|
518
543
|
}
|
|
519
544
|
|
|
520
545
|
// this may seem out of place but certain API methods
|
|
@@ -566,7 +591,7 @@ Transaction.prototype.finalizeName = function finalizeName(name) {
|
|
|
566
591
|
*/
|
|
567
592
|
Transaction.prototype.getName = function getName() {
|
|
568
593
|
if (this.isWeb() && this.url) {
|
|
569
|
-
const finalName = this._partialNameFromUri(
|
|
594
|
+
const finalName = this._partialNameFromUri()
|
|
570
595
|
if (finalName.ignore) {
|
|
571
596
|
this.ignore = true
|
|
572
597
|
}
|
|
@@ -1427,18 +1452,16 @@ Transaction.prototype.addRequestParameters = addRequestParameters
|
|
|
1427
1452
|
* @param {Object<string, string>} requestParameters of the request object
|
|
1428
1453
|
*/
|
|
1429
1454
|
function addRequestParameters(requestParameters) {
|
|
1430
|
-
for (const key
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
)
|
|
1455
|
+
for (const [key, value] of Object.entries(requestParameters)) {
|
|
1456
|
+
this.trace.attributes.addAttribute(
|
|
1457
|
+
DESTS.NONE,
|
|
1458
|
+
REQUEST_PARAMS_PATH + key,
|
|
1459
|
+
value
|
|
1460
|
+
)
|
|
1437
1461
|
|
|
1438
|
-
|
|
1462
|
+
const segment = this.baseSegment
|
|
1439
1463
|
|
|
1440
|
-
|
|
1441
|
-
}
|
|
1464
|
+
segment.attributes.addAttribute(DESTS.NONE, REQUEST_PARAMS_PATH + key, value)
|
|
1442
1465
|
}
|
|
1443
1466
|
}
|
|
1444
1467
|
|
|
@@ -1453,4 +1476,82 @@ Transaction.prototype.incrementCounters = function incrementCounters() {
|
|
|
1453
1476
|
this.agent.incrementCounters()
|
|
1454
1477
|
}
|
|
1455
1478
|
|
|
1479
|
+
/**
|
|
1480
|
+
* Initializes a web transaction with request information:
|
|
1481
|
+
* Assigns type(web), verb, url, port attributes on transaction.
|
|
1482
|
+
* Collects request headers as attributes on the transaction.
|
|
1483
|
+
* Applies user naming rules to the transaction.
|
|
1484
|
+
* Calculates queue time if possible.
|
|
1485
|
+
* Assigns synthetics information if present.
|
|
1486
|
+
* Adds distributed trace or cat headers if present.
|
|
1487
|
+
*
|
|
1488
|
+
* @param {object} params - Parameters to initialize the web transaction.
|
|
1489
|
+
* @param {string} params.absoluteUrl - Full URL of the request.
|
|
1490
|
+
* @param {string} params.method - HTTP method of the request.
|
|
1491
|
+
* @param {number} params.port - Port of the request.
|
|
1492
|
+
* @param {object} [params.headers] - HTTP headers of the request.
|
|
1493
|
+
* @param {string} [params.transport] - Transport type that delivered the request.
|
|
1494
|
+
*/
|
|
1495
|
+
Transaction.prototype.initializeWeb = function initializeWeb({ absoluteUrl, method, port, headers = {}, transport }) {
|
|
1496
|
+
this.type = TYPES.WEB
|
|
1497
|
+
headerAttributes.collectRequestHeaders(headers, this)
|
|
1498
|
+
|
|
1499
|
+
if (method != null) {
|
|
1500
|
+
this.trace.attributes.addAttribute(DESTS.TRANS_COMMON, 'request.method', method)
|
|
1501
|
+
this.baseSegment.addSpanAttribute('request.method', method)
|
|
1502
|
+
this.verb = method
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
this.port = port
|
|
1506
|
+
|
|
1507
|
+
// the error tracer needs a URL for tracing, even though naming overwrites
|
|
1508
|
+
try {
|
|
1509
|
+
const parsedUrl = new URL(absoluteUrl)
|
|
1510
|
+
const data = urltils.scrubAndParseParameters(parsedUrl)
|
|
1511
|
+
this.url = data.path
|
|
1512
|
+
this.addRequestParameters(data.parameters)
|
|
1513
|
+
} catch (err) {
|
|
1514
|
+
logger.debug('Could not parse URL %s: %s', absoluteUrl, err.message)
|
|
1515
|
+
this.url = '/unknown'
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// need to set any config-driven names early for RUM
|
|
1519
|
+
logger.trace({ url: this.url, transaction: this.id },
|
|
1520
|
+
'Appplying user naming rules for RUM.')
|
|
1521
|
+
|
|
1522
|
+
this.applyUserNamingRules(this.url)
|
|
1523
|
+
|
|
1524
|
+
const queueTimeStamp = headerProcessing.getQueueTime(logger, headers)
|
|
1525
|
+
if (queueTimeStamp) {
|
|
1526
|
+
this.queueTime = Date.now() - queueTimeStamp
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
synthetics.assignHeadersToTransaction(this.agent.config, this, headers)
|
|
1530
|
+
this.addDtCatHeaders({ headers, transport })
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* Handles accepting upstream DT headers(traceparent/tracestate or newrelic) if present and DT is enabled.
|
|
1535
|
+
* If CAT is enabled, handles accepting CAT headers and assigns to transaction.
|
|
1536
|
+
*
|
|
1537
|
+
* @param {object} params - Parameters for adding DT or CAT headers.
|
|
1538
|
+
* @param {object} params.headers - HTTP headers of the request(must be lower-cased, by default http instrumentation has headers lower case, other libraries must pass this in lowercased).
|
|
1539
|
+
* @param {string} [params.transport] - Transport type that delivered the request.
|
|
1540
|
+
*/
|
|
1541
|
+
Transaction.prototype.addDtCatHeaders = function addDtCatHeaders({ headers, transport }) {
|
|
1542
|
+
if (this.agent.config.distributed_tracing.enabled) {
|
|
1543
|
+
// Node http request headers are automatically lowercase
|
|
1544
|
+
// need to pass in lower case for other instrumentation
|
|
1545
|
+
this.acceptDistributedTraceHeaders(transport, headers)
|
|
1546
|
+
} else if (this.agent.config.cross_application_tracer.enabled) {
|
|
1547
|
+
const { id, transactionId } = cat.extractCatHeaders(headers)
|
|
1548
|
+
const { externalId, externalTransaction } = cat.parseCatData(
|
|
1549
|
+
id,
|
|
1550
|
+
transactionId,
|
|
1551
|
+
this.agent.config.encoding_key
|
|
1552
|
+
)
|
|
1553
|
+
cat.assignCatToTransaction(externalId, externalTransaction, this)
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1456
1557
|
module.exports = Transaction
|
|
@@ -133,13 +133,14 @@ TraceSegment.prototype.setNameFromTransaction = function setNameFromTransaction(
|
|
|
133
133
|
/**
|
|
134
134
|
* Once a transaction is named, the web segment also needs to be updated to
|
|
135
135
|
* match it (which implies this method must be called subsequent to
|
|
136
|
-
* transaction.
|
|
136
|
+
* transaction.finalizeNameFromWeb). To properly name apdex metrics during metric
|
|
137
137
|
* recording, it's also necessary to copy the transaction's partial name. And
|
|
138
138
|
* finally, marking the trace segment as being a web segment copies the
|
|
139
139
|
* segment's parameters onto the transaction.
|
|
140
140
|
* @param {Transaction} transaction The transaction to which this segment will be bound.
|
|
141
|
+
* @param obfuscatedUrl
|
|
141
142
|
*/
|
|
142
|
-
TraceSegment.prototype.markAsWeb = function markAsWeb(transaction) {
|
|
143
|
+
TraceSegment.prototype.markAsWeb = function markAsWeb(transaction, obfuscatedUrl) {
|
|
143
144
|
this.setNameFromTransaction(transaction)
|
|
144
145
|
|
|
145
146
|
const traceAttrs = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE)
|
|
@@ -148,6 +149,8 @@ TraceSegment.prototype.markAsWeb = function markAsWeb(transaction) {
|
|
|
148
149
|
this.addAttribute(key, traceAttrs[key])
|
|
149
150
|
}
|
|
150
151
|
})
|
|
152
|
+
|
|
153
|
+
this.addSpanAttribute('request.uri', obfuscatedUrl)
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
/**
|