Skip to content

Commit

Permalink
feat: Added instrumentation for @opensearch-projects/opensearch v2.…
Browse files Browse the repository at this point in the history
…1.0+
  • Loading branch information
bizob2828 committed Dec 19, 2024
1 parent daa3700 commit f8488cf
Show file tree
Hide file tree
Showing 6 changed files with 681 additions and 0 deletions.
161 changes: 161 additions & 0 deletions lib/instrumentation/@opensearch-project/opensearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'

const { QuerySpec } = require('../../shim/specs')
const semver = require('semver')
const logger = require('../../logger').child({ component: 'OpenSearch' })
const { isNotEmpty } = require('../../util/objects')

/**
* Instruments the `@opensearch-project/opensearch` module. This function is
* passed to `onRequire` when instantiating instrumentation.
*
* @param {object} _agent New Relic agent
* @param {object} opensearch resolved module
* @param {string} _moduleName string representation of require/import path
* @param {object} shim New Relic shim
* @returns {void}
*/
module.exports = function initialize(_agent, opensearch, _moduleName, shim) {
const pkgVersion = shim.pkgVersion
if (semver.lt(pkgVersion, '2.1.0')) {
shim &&
shim.logger.debug(
`Opensearch support is for versions 2.1.0 and above. Not instrumenting ${pkgVersion}.`
)
return
}

shim.setDatastore(shim.OPENSEARCH)
shim.setParser(queryParser)

shim.recordQuery(
opensearch.Transport.prototype,
'request',
function wrapQuery(shim, _, __, args) {
const ctx = this
return new QuerySpec({
query: JSON.stringify(args?.[0]),
promise: true,
opaque: true,
inContext: function inContext() {
getConnection.call(ctx, shim)
}
})
}
)
}

/**
* Parses the parameters sent to opensearch 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 @opensearch-project/opensearch
* @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
* opensearch
*
* @param {object} shim The New Relic datastore-shim
* @returns {Function} captureInstanceAttributes method of shim
*/
function getConnection(shim) {
const connectionPool = this.connectionPool.connections[0]
const host = connectionPool.url.host.split(':')
const port = connectionPool.url.port || host?.[1]
return shim.captureInstanceAttributes(host[0], port)
}

module.exports.queryParser = queryParser
module.exports.parsePath = parsePath
module.exports.getConnection = getConnection
1 change: 1 addition & 0 deletions lib/instrumentations.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const InstrumentationDescriptor = require('./instrumentation-descriptor')
module.exports = function instrumentations() {
return {
'@elastic/elasticsearch': { type: InstrumentationDescriptor.TYPE_DATASTORE },
'@opensearch-project/opensearch': { type: InstrumentationDescriptor.TYPE_DATASTORE },
'@grpc/grpc-js': { module: './instrumentation/grpc-js' },
'@hapi/hapi': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
'@hapi/vision': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
Expand Down
1 change: 1 addition & 0 deletions lib/shim/datastore-shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const DATASTORE_NAMES = {
MONGODB: 'MongoDB',
MYSQL: 'MySQL',
NEPTUNE: 'Neptune',
OPENSEARCH: 'OpenSearch',
POSTGRES: 'Postgres',
REDIS: 'Redis',
PRISMA: 'Prisma'
Expand Down
21 changes: 21 additions & 0 deletions test/versioned/opensearch/newrelic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2020 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'

exports.config = {
app_name: ['opensearch test'],
license_key: 'license key here',
utilization: {
detect_aws: false,
detect_pcf: false,
detect_azure: false,
detect_gcp: false,
detect_docker: false
},
logging: {
enabled: true
}
}
Loading

0 comments on commit f8488cf

Please sign in to comment.