Skip to content

Commit

Permalink
wip: moved sql parsers to lib/db/query-parsers
Browse files Browse the repository at this point in the history
  • Loading branch information
bizob2828 committed Nov 22, 2024
1 parent cac0902 commit 695c6a7
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 120 deletions.
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 }
29 changes: 29 additions & 0 deletions lib/db/query-parsers/mongodb.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'

/**
* parser used to grab the collection and operation
* from a running query
*
* @param {object} operation mongodb operation
* @returns {object} { operation, collection } parsed operation and collection
*/
function queryParser(operation) {
let collection = this.collectionName || 'unknown'

// cursor methods have collection on namespace.collection
if (this?.namespace?.collection) {
collection = this.namespace.collection
// (un)ordered bulk operations have collection on different key
} else if (this?.s?.collection?.collectionName) {
collection = this.s.collection.collectionName
}

return { operation, collection }
}

module.exports = queryParser
97 changes: 1 addition & 96 deletions lib/instrumentation/@elastic/elasticsearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@

const { QuerySpec } = require('../../shim/specs')
const semver = require('semver')
const logger = require('../../logger').child({ component: 'ElasticSearch' })
const { isNotEmpty } = require('../../util/objects')
const { queryParser } = require('../../db/query-parsers/elasticsearch')

/**
* Instruments the `@elastic/elasticsearch` module. This function is
Expand Down Expand Up @@ -46,98 +45,6 @@ module.exports = function initialize(_agent, elastic, _moduleName, shim) {
})
}

/**
* 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 }
}

/**
* Convenience function for deriving connection information from
* elasticsearch
Expand All @@ -152,6 +59,4 @@ function getConnection(shim) {
return shim.captureInstanceAttributes(host[0], port)
}

module.exports.queryParser = queryParser
module.exports.parsePath = parsePath
module.exports.getConnection = getConnection
22 changes: 1 addition & 21 deletions lib/instrumentation/mongodb/v4-mongo.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,7 @@ const {
instrumentDb,
parseAddress
} = require('./common')

/**
* parser used to grab the collection and operation
* from a running query
*
* @param {object} operation mongodb operation
* @returns {object} { operation, collection } parsed operation and collection
*/
function queryParser(operation) {
let collection = this.collectionName || 'unknown'

// cursor methods have collection on namespace.collection
if (this?.namespace?.collection) {
collection = this.namespace.collection
// (un)ordered bulk operations have collection on different key
} else if (this?.s?.collection?.collectionName) {
collection = this.s.collection.collectionName
}

return { operation, collection }
}
const queryParser = require('../../db/query-parsers/mongodb')

/**
* `commandStarted` handler used to
Expand Down
26 changes: 24 additions & 2 deletions lib/otel/segment-synthesis.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
const { RulesEngine } = require('./rules')
const defaultLogger = require('../logger').child({ component: 'segment-synthesizer' })
const NAMES = require('../metrics/names')
const { SEMATTRS_HTTP_HOST } = require('@opentelemetry/semantic-conventions')
const {
SEMATTRS_HTTP_HOST,
SEMATTRS_DB_SYSTEM,
SEMATTRS_DB_SQL_TABLE,
SEMATTRS_DB_OPERATION
} = require('@opentelemetry/semantic-conventions')

class SegmentSynthesizer {
constructor(agent, { logger = defaultLogger } = {}) {
Expand All @@ -27,9 +32,12 @@ class SegmentSynthesizer {
return
}

if (rule?.type === 'external') {
if (rule.type === 'external') {
return this.createExternalSegment(otelSpan)
} else if (rule.type === 'db') {
return this.createDatabaseSegment(otelSpan)
}

this.logger.debug('Found type: %s, no synthesize rule currently built', rule.type)
}

Expand All @@ -45,6 +53,20 @@ class SegmentSynthesizer {
transaction: context.transaction
})
}

createDatabaseSegment(otelSpan) {
const context = this.agent.tracer.getContext()
const system = otelSpan.attributes[SEMATTRS_DB_SYSTEM] || 'Unknown'
// TODO: looks like most otel instrumentation does not set this, instead provides the query
// we have to parse this
const table = otelSpan.attributes[SEMATTRS_DB_SQL_TABLE] || 'Unknown'
const operation = otelSpan.attributes[SEMATTRS_DB_OPERATION] || otelSpan.name || 'Unknown'
return this.agent.tracer.createSegment({
name: `Datastore/statement/${system}/${table}/${operation}`,
parent: context.segment,
transaction: context.transaction
})
}
}

module.exports = SegmentSynthesizer
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

const test = require('node:test')
const assert = require('node:assert')
const { parsePath, queryParser } = require('../../../lib/instrumentation/@elastic/elasticsearch')
const { parsePath, queryParser } = require('../../../../lib/db/query-parsers/elasticsearch')
const methods = [
{ name: 'GET', expected: 'get' },
{ name: 'PUT', expected: 'create' },
Expand Down

0 comments on commit 695c6a7

Please sign in to comment.