Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Merge staged otel work #2873

Merged
merged 13 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
632 changes: 631 additions & 1 deletion THIRD_PARTY_NOTICES.md

Large diffs are not rendered by default.

45 changes: 31 additions & 14 deletions api.js
Original file line number Diff line number Diff line change
Expand Up @@ -961,19 +961,21 @@ API.prototype.startWebTransaction = function startWebTransaction(url, handle) {

const shim = this.shim
const tracer = this.agent.tracer
const parent = tracer.getTransaction()
const parentTx = tracer.getTransaction()

assignCLMSymbol(shim, handle)
return tracer.transactionNestProxy('web', function startWebSegment() {
const tx = tracer.getTransaction()
const context = tracer.getContext()
const tx = context?.transaction
const parent = context?.segment

if (!tx) {
return handle.apply(this, arguments)
}

if (tx === parent) {
if (tx === parentTx) {
logger.debug('not creating nested transaction %s using transaction %s', url, tx.id)
return tracer.addSegment(url, null, null, true, handle)
return tracer.addSegment(url, null, parent, true, handle)
}

logger.debug(
Expand All @@ -985,10 +987,16 @@ API.prototype.startWebTransaction = function startWebTransaction(url, handle) {
tx.nameState.setName(NAMES.CUSTOM, null, NAMES.ACTION_DELIMITER, url)
tx.url = url
tx.applyUserNamingRules(tx.url)
tx.baseSegment = tracer.createSegment(url, recordWeb)
tx.baseSegment = tracer.createSegment({
name: url,
recorder: recordWeb,
transaction: tx,
parent
})
const newContext = context.enterSegment({ transaction: tx, segment: tx.baseSegment })
tx.baseSegment.start()

const boundHandle = tracer.bindFunction(handle, tx.baseSegment)
const boundHandle = tracer.bindFunction(handle, newContext)
maybeAddCLMAttributes(handle, tx.baseSegment)
let returnResult = boundHandle.call(this)
if (returnResult && shim.isPromise(returnResult)) {
Expand Down Expand Up @@ -1061,19 +1069,21 @@ function startBackgroundTransaction(name, group, handle) {
const tracer = this.agent.tracer
const shim = this.shim
const txName = group + '/' + name
const parent = tracer.getTransaction()
const parentTx = tracer.getTransaction()

assignCLMSymbol(shim, handle)
return tracer.transactionNestProxy('bg', function startBackgroundSegment() {
const tx = tracer.getTransaction()
const context = tracer.getContext()
const tx = context?.transaction
const parent = context?.segment

if (!tx) {
return handle.apply(this, arguments)
}

if (tx === parent) {
if (tx === parentTx) {
logger.debug('not creating nested transaction %s using transaction %s', txName, tx.id)
return tracer.addSegment(txName, null, null, true, handle)
return tracer.addSegment(txName, null, parent, true, handle)
}

logger.debug(
Expand All @@ -1085,11 +1095,17 @@ function startBackgroundTransaction(name, group, handle) {
)

tx._partialName = txName
tx.baseSegment = tracer.createSegment(name, recordBackground)
tx.baseSegment = tracer.createSegment({
name,
recorder: recordBackground,
transaction: tx,
parent
})
const newContext = context.enterSegment({ transaction: tx, segment: tx.baseSegment })
tx.baseSegment.partialName = group
tx.baseSegment.start()

const boundHandle = tracer.bindFunction(handle, tx.baseSegment)
const boundHandle = tracer.bindFunction(handle, newContext)
maybeAddCLMAttributes(handle, tx.baseSegment)
let returnResult = boundHandle.call(this)
if (returnResult && shim.isPromise(returnResult)) {
Expand Down Expand Up @@ -1525,12 +1541,13 @@ API.prototype.getTraceMetadata = function getTraceMetadata() {
const metadata = {}

const segment = this.agent.tracer.getSegment()
if (!segment) {
const transaction = this.agent.tracer.getTransaction()
if (!(segment || transaction)) {
logger.debug('No transaction found when calling API#getTraceMetadata')
} else if (!this.agent.config.distributed_tracing.enabled) {
logger.debug('Distributed tracing disabled when calling API#getTraceMetadata')
} else {
metadata.traceId = segment.transaction.traceId
metadata.traceId = transaction.traceId

const spanId = segment.getSpanId()
if (spanId) {
Expand Down
45 changes: 29 additions & 16 deletions lib/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ const TxSegmentNormalizer = require('./metrics/normalizer/tx_segment')
const uninstrumented = require('./uninstrumented')
const util = require('util')
const createSpanEventAggregator = require('./spans/create-span-event-aggregator')
const createContextManager = require('./context-manager/create-context-manager')
const {
maybeAddDatabaseAttributes,
maybeAddExternalAttributes,
Expand Down Expand Up @@ -258,9 +257,8 @@ function Agent(config) {

this.errors = new ErrorCollector(config, errorTraceAggregator, errorEventAggregator, this.metrics)

this._contextManager = createContextManager(this.config)
// Transaction tracing.
this.tracer = new Tracer(this, this._contextManager)
this.tracer = new Tracer(this)
this.traces = new TransactionTraceAggregator(
{
periodMs: DEFAULT_HARVEST_INTERVAL_MS,
Expand Down Expand Up @@ -294,13 +292,7 @@ function Agent(config) {
// Set up all the configuration events the agent needs to listen for.
this._listenForConfigChanges()

// Entity tracking metrics.
this.totalActiveSegments = 0
this.segmentsCreatedInHarvest = 0
this.segmentsClearedInHarvest = 0
// Used by shutdown code as well as entity tracking stats
this.activeTransactions = 0

this.initCounters()
this.llm = {}

// Finally, add listeners for the agent's own events.
Expand Down Expand Up @@ -599,11 +591,33 @@ Agent.prototype._generateEntityStatsAndClear = function _generateHarvestMetrics(
)
}

// Reset the counters.
this.resetCounters()
}

Agent.prototype.initCounters = function initCounters() {
// Entity tracking metrics.
this.totalActiveSegments = 0
this.segmentsCreatedInHarvest = 0
this.segmentsClearedInHarvest = 0
// Used by shutdown code as well as entity tracking stats
this.activeTransactions = 0
}

Agent.prototype.incrementCounters = function incrementCounters() {
++this.totalActiveSegments
++this.segmentsCreatedInHarvest
}

Agent.prototype.decrementCounters = function decrementCounters(transaction) {
--this.activeTransactions
this.totalActiveSegments -= transaction.numSegments
this.segmentsClearedInHarvest += transaction.numSegments
}

Agent.prototype.resetCounters = function resetCounters() {
this.segmentsCreatedInHarvest = 0
this.segmentsClearedInHarvest = 0
}
/**
* Public interface for passing configuration data from the collector
* on to the configuration, in an effort to keep them at least somewhat
Expand Down Expand Up @@ -755,9 +769,7 @@ Agent.prototype._transactionFinished = function _transactionFinished(transaction
logger.debug('Ignoring %s (%s).', transaction.name, transaction.id)
}

--this.activeTransactions
this.totalActiveSegments -= transaction.numSegments
this.segmentsClearedInHarvest += transaction.numSegments
this.decrementCounters(transaction)
}

Agent.prototype.setLambdaArn = function setLambdaArn(arn) {
Expand Down Expand Up @@ -839,12 +851,13 @@ Agent.prototype._listenForConfigChanges = function _listenForConfigChanges() {
*/
Agent.prototype.getLinkingMetadata = function getLinkingMetadata(excludeServiceLinks = false) {
const segment = this.tracer.getSegment()
const transaction = this.tracer.getTransaction()
const config = this.config

const linkingMetadata = {}

if (config.distributed_tracing.enabled && segment) {
linkingMetadata['trace.id'] = segment.transaction.traceId
if (config.distributed_tracing.enabled && segment && transaction) {
linkingMetadata['trace.id'] = transaction.traceId
const spanId = segment.getSpanId()
if (spanId) {
linkingMetadata['span.id'] = spanId
Expand Down
10 changes: 3 additions & 7 deletions lib/context-manager/async-local-context-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
'use strict'

const { AsyncLocalStorage } = require('async_hooks')
const Context = require('./context')

/**
* Class for managing state in the agent.
Expand All @@ -17,12 +18,7 @@ const { AsyncLocalStorage } = require('async_hooks')
* @class
*/
class AsyncLocalContextManager {
/**
* @param {object} config New Relic config instance
*/
constructor(config) {
this._config = config

constructor() {
this._asyncLocalStorage = new AsyncLocalStorage()
}

Expand All @@ -32,7 +28,7 @@ class AsyncLocalContextManager {
* @returns {object} The current active context.
*/
getContext() {
return this._asyncLocalStorage.getStore() || null
return this._asyncLocalStorage.getStore() || new Context()
}

/**
Expand Down
29 changes: 29 additions & 0 deletions lib/context-manager/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'

module.exports = class Context {
constructor(transaction, segment) {
this._transaction = transaction
this._segment = segment
}

get segment() {
return this._segment
}

get transaction() {
return this._transaction
}

enterSegment({ segment, transaction = this.transaction }) {
return new this.constructor(transaction, segment)
}

enterTransaction(transaction) {
return new this.constructor(transaction, transaction.trace.root)
}
}
29 changes: 0 additions & 29 deletions lib/context-manager/create-context-manager.js

This file was deleted.

102 changes: 102 additions & 0 deletions lib/db/query-parsers/elasticsearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'
const logger = require('../../logger').child({ component: 'elasticsearch_query_parser' })
const { isNotEmpty } = require('../../util/objects')

/**
* Parses the parameters sent to elasticsearch for collection,
* method, and query
*
* @param {object} params Query object received by the datashim.
* Required properties: path {string}, method {string}.
* Optional properties: querystring {string}, body {object}, and
* bulkBody {object}
* @returns {object} consisting of collection {string}, operation {string},
* and query {string}
*/
function queryParser(params) {
params = JSON.parse(params)
const { collection, operation } = parsePath(params.path, params.method)

// the substance of the query may be in querystring or in body.
let queryParam = {}
if (isNotEmpty(params.querystring)) {
queryParam = params.querystring
}
// let body or bulkBody override querystring, as some requests have both
if (isNotEmpty(params.body)) {
queryParam = params.body
} else if (Array.isArray(params.bulkBody) && params.bulkBody.length) {
queryParam = params.bulkBody
}
// The helper interface provides a simpler API:

const query = JSON.stringify(queryParam)

return {
collection,
operation,
query
}
}

/**
* Convenience function for parsing the params.path sent to the queryParser
* for normalized collection and operation
*
* @param {string} pathString params.path supplied to the query parser
* @param {string} method http method called by @elastic/elasticsearch
* @returns {object} consisting of collection {string} and operation {string}
*/
function parsePath(pathString, method) {
let collection
let operation
const defaultCollection = 'any'
const actions = {
GET: 'get',
PUT: 'create',
POST: 'create',
DELETE: 'delete',
HEAD: 'exists'
}
const suffix = actions[method]

try {
const path = pathString.split('/')
if (method === 'PUT' && path.length === 2) {
collection = path?.[1] || defaultCollection
operation = 'index.create'
return { collection, operation }
}
path.forEach((segment, idx) => {
const prev = idx - 1
let opname
if (segment === '_search') {
collection = path?.[prev] || defaultCollection
operation = 'search'
} else if (segment[0] === '_') {
opname = segment.substring(1)
collection = path?.[prev] || defaultCollection
operation = `${opname}.${suffix}`
}
})
if (!operation && !collection) {
// likely creating an index--no underscore segments
collection = path?.[1] || defaultCollection
operation = `index.${suffix}`
}
} catch (e) {
logger.warn('Failed to parse path for operation and collection. Using defaults')
logger.warn(e)
collection = defaultCollection
operation = 'unknown'
}

return { collection, operation }
}

module.exports = { queryParser, parsePath }
Loading
Loading