Skip to content

Commit

Permalink
feat: Added segment and transaction synthesis for http server spans (n…
Browse files Browse the repository at this point in the history
  • Loading branch information
bizob2828 committed Dec 12, 2024
1 parent 6a3a012 commit 98b349e
Show file tree
Hide file tree
Showing 9 changed files with 319 additions and 104 deletions.
88 changes: 10 additions & 78 deletions lib/otel/segment-synthesis.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,7 @@
'use strict'
const { RulesEngine } = require('./rules')
const defaultLogger = require('../logger').child({ component: 'segment-synthesizer' })
const NAMES = require('../metrics/names')
const {
SEMATTRS_HTTP_HOST,
SEMATTRS_DB_MONGODB_COLLECTION,
SEMATTRS_DB_SYSTEM,
SEMATTRS_DB_SQL_TABLE,
SEMATTRS_DB_OPERATION,
SEMATTRS_DB_STATEMENT,
DbSystemValues
} = require('@opentelemetry/semantic-conventions')
const parseSql = require('../db/query-parsers/sql')
const { DatabaseSegment, HttpExternalSegment, ServerSegment } = require('./segments')

class SegmentSynthesizer {
constructor(agent, { logger = defaultLogger } = {}) {
Expand All @@ -36,74 +26,16 @@ class SegmentSynthesizer {
return
}

if (rule.type === 'external') {
return this.createExternalSegment(otelSpan)
} else if (rule.type === 'db') {
return this.createDatabaseSegment(otelSpan)
switch (rule.type) {
case 'external':
return new HttpExternalSegment(this.agent, otelSpan)
case 'db':
return new DatabaseSegment(this.agent, otelSpan)
case 'server':
return new ServerSegment(this.agent, otelSpan)
default:
this.logger.debug('Found type: %s, no synthesis rule currently built', rule.type)
}

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

// TODO: should we move these to somewhere else and use in the places
// where external segments are created in our agent
createExternalSegment(otelSpan) {
const context = this.agent.tracer.getContext()
const host = otelSpan.attributes[SEMATTRS_HTTP_HOST] || 'Unknown'
const name = NAMES.EXTERNAL.PREFIX + host
return this.agent.tracer.createSegment({
name,
parent: context.segment,
transaction: context.transaction
})
}

parseStatement(otelSpan, system) {
let table = otelSpan.attributes[SEMATTRS_DB_SQL_TABLE]
let operation = otelSpan.attributes[SEMATTRS_DB_OPERATION]
const statement = otelSpan.attributes[SEMATTRS_DB_STATEMENT]
if (statement && !(table || operation)) {
const parsed = parseSql({ sql: statement })
if (parsed.operation && !operation) {
operation = parsed.operation
}

if (parsed.collection && !table) {
table = parsed.collection
}
}
if (system === DbSystemValues.MONGODB) {
table = otelSpan.attributes[SEMATTRS_DB_MONGODB_COLLECTION]
}

if (system === DbSystemValues.REDIS && statement) {
;[operation] = statement.split(' ')
}

table = table || 'Unknown'
operation = operation || 'Unknown'

return { operation, table }
}

// TODO: This probably has some holes
// I did analysis and tried to apply the best logic
// to extract table/operation
createDatabaseSegment(otelSpan) {
const context = this.agent.tracer.getContext()
const system = otelSpan.attributes[SEMATTRS_DB_SYSTEM]
const { operation, table } = this.parseStatement(otelSpan, system)

let name = `Datastore/statement/${system}/${table}/${operation}`
// All segment name shapes are same except redis/memcached
if (system === DbSystemValues.REDIS || system === DbSystemValues.MEMCACHED) {
name = `Datastore/operation/${system}/${operation}`
}
return this.agent.tracer.createSegment({
name,
parent: context.segment,
transaction: context.transaction
})
}
}

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

'use strict'
const {
SEMATTRS_DB_MONGODB_COLLECTION,
SEMATTRS_DB_SYSTEM,
SEMATTRS_DB_SQL_TABLE,
SEMATTRS_DB_OPERATION,
SEMATTRS_DB_STATEMENT,
DbSystemValues
} = require('@opentelemetry/semantic-conventions')
const parseSql = require('../../db/query-parsers/sql')

// TODO: This probably has some holes
// I did analysis and tried to apply the best logic
// to extract table/operation
module.exports = class DatabaseSegment {
constructor(agent, otelSpan) {
const context = agent.tracer.getContext()
const name = this.setName(otelSpan)
const segment = agent.tracer.createSegment({
name,
parent: context.segment,
transaction: context.transaction
})
return { segment, transaction: context.transaction }
}

parseStatement(otelSpan, system) {
let table = otelSpan.attributes[SEMATTRS_DB_SQL_TABLE]
let operation = otelSpan.attributes[SEMATTRS_DB_OPERATION]
const statement = otelSpan.attributes[SEMATTRS_DB_STATEMENT]
if (statement && !(table || operation)) {
const parsed = parseSql({ sql: statement })
if (parsed.operation && !operation) {
operation = parsed.operation
}

if (parsed.collection && !table) {
table = parsed.collection
}
}
if (system === DbSystemValues.MONGODB) {
table = otelSpan.attributes[SEMATTRS_DB_MONGODB_COLLECTION]
}

if (system === DbSystemValues.REDIS && statement) {
;[operation] = statement.split(' ')
}

table = table || 'Unknown'
operation = operation || 'Unknown'

return { operation, table }
}

setName(otelSpan) {
const system = otelSpan.attributes[SEMATTRS_DB_SYSTEM]
const { operation, table } = this.parseStatement(otelSpan, system)
let name = `Datastore/statement/${system}/${table}/${operation}`
// All segment name shapes are same except redis/memcached
if (system === DbSystemValues.REDIS || system === DbSystemValues.MEMCACHED) {
name = `Datastore/operation/${system}/${operation}`
}
return name
}
}
22 changes: 22 additions & 0 deletions lib/otel/segments/http-external.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'
const NAMES = require('../../metrics/names')
const { SEMATTRS_HTTP_HOST } = require('@opentelemetry/semantic-conventions')

module.exports = class HttpExternalSegment {
constructor(agent, otelSpan) {
const context = agent.tracer.getContext()
const host = otelSpan.attributes[SEMATTRS_HTTP_HOST] || 'Unknown'
const name = NAMES.EXTERNAL.PREFIX + host
const segment = agent.tracer.createSegment({
name,
parent: context.segment,
transaction: context.transaction
})
return { segment, transaction: context.transaction }
}
}
15 changes: 15 additions & 0 deletions lib/otel/segments/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'
const HttpExternalSegment = require('./http-external')
const DatabaseSegment = require('./database')
const ServerSegment = require('./server')

module.exports = {
DatabaseSegment,
HttpExternalSegment,
ServerSegment
}
88 changes: 88 additions & 0 deletions lib/otel/segments/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'

const {
SEMATTRS_HTTP_METHOD,
SEMATTRS_HTTP_ROUTE,
SEMATTRS_HTTP_URL,
SEMATTRS_RPC_SYSTEM,
SEMATTRS_RPC_SERVICE,
SEMATTRS_RPC_METHOD
} = require('@opentelemetry/semantic-conventions')
const { DESTINATIONS } = require('../../config/attribute-filter')
const DESTINATION = DESTINATIONS.TRANS_COMMON
const Transaction = require('../../transaction')
const urltils = require('../../util/urltils')
const url = require('url')

module.exports = class ServerSegment {
constructor(agent, otelSpan) {
this.agent = agent
this.transaction = new Transaction(agent)
this.transaction.type = 'web'
this.otelSpan = otelSpan
const rpcSystem = otelSpan.attributes[SEMATTRS_RPC_SYSTEM]
const httpMethod = otelSpan.attributes[SEMATTRS_HTTP_METHOD]
if (rpcSystem) {
this.segment = this.rpcSegment(rpcSystem)
} else if (httpMethod) {
this.segment = this.httpSegment(httpMethod)
} else {
this.segment = this.genericHttpSegment()
}
this.transaction.baseSegment = this.segment
return { segment: this.segment, transaction: this.transaction }
}

rpcSegment(rpcSystem) {
const rpcService = this.otelSpan.attributes[SEMATTRS_RPC_SERVICE] || 'Unknown'
const rpcMethod = this.otelSpan.attributes[SEMATTRS_RPC_METHOD] || 'Unknown'
const name = `WebTransaction/WebFrameworkUri/${rpcSystem}/${rpcService}.${rpcMethod}`
this.transaction.name = name
this.transaction.trace.attributes.addAttribute(DESTINATION, 'request.method', rpcMethod)
this.transaction.trace.attributes.addAttribute(DESTINATION, 'request.uri', name)
this.transaction.url = name
const segment = this.agent.tracer.createSegment({
name,
parent: this.transaction.trace.root,
transaction: this.transaction
})
segment.addAttribute('component', rpcSystem)
return segment
}

// most instrumentation will hit this case
// I find that if the request is in a web framework, the web framework instrumentation
// sets `http.route` and when the span closes it pulls that attribute in
// we'll most likely need to wire up some naming reconciliation
// to handle this use case.
httpSegment(httpMethod) {
const httpRoute = this.otelSpan.attributes[SEMATTRS_HTTP_ROUTE] || 'Unknown'
const httpUrl = this.otelSpan.attributes[SEMATTRS_HTTP_URL] || '/Unknown'
const requestUrl = url.parse(httpUrl, true)
const name = `WebTransaction/Nodejs/${httpMethod}/${httpRoute}`
this.transaction.name = name
this.transaction.url = urltils.obfuscatePath(this.agent.config, requestUrl.pathname)
this.transaction.trace.attributes.addAttribute(DESTINATION, 'request.uri', this.transaction.url)
this.transaction.trace.attributes.addAttribute(DESTINATION, 'request.method', httpMethod)
return this.agent.tracer.createSegment({
name,
parent: this.transaction.trace.root,
transaction: this.transaction
})
}

genericHttpSegment() {
const name = 'WebTransaction/NormalizedUri/*'
this.transaction.name = name
return this.agent.tracer.createSegment({
name,
parent: this.transaction.trace.root,
transaction: this.transaction
})
}
}
4 changes: 4 additions & 0 deletions test/unit/lib/otel/fixtures/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@ const {
} = require('./db-sql')
const createSpan = require('./span')
const createHttpClientSpan = require('./http-client')
const { createRpcServerSpan, createHttpServerSpan, createBaseHttpSpan } = require('./server')

module.exports = {
createBaseHttpSpan,
createDbClientSpan,
createDbStatementSpan,
createHttpClientSpan,
createHttpServerSpan,
createMemcachedDbSpan,
createMongoDbSpan,
createRedisDbSpan,
createRpcServerSpan,
createSpan
}
43 changes: 43 additions & 0 deletions test/unit/lib/otel/fixtures/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'
const {
SEMATTRS_HTTP_METHOD,
SEMATTRS_HTTP_ROUTE,
SEMATTRS_HTTP_URL,
SEMATTRS_RPC_SYSTEM,
SEMATTRS_RPC_SERVICE,
SEMATTRS_RPC_METHOD
} = require('@opentelemetry/semantic-conventions')

const { SpanKind } = require('@opentelemetry/api')
const createSpan = require('./span')

function createRpcServerSpan({ tracer, name = 'test-span' }) {
const span = createSpan({ name, kind: SpanKind.SERVER, tracer })
span.setAttribute(SEMATTRS_RPC_SYSTEM, 'grpc')
span.setAttribute(SEMATTRS_RPC_METHOD, 'findUser')
span.setAttribute(SEMATTRS_RPC_SERVICE, 'TestService')
return span
}

function createHttpServerSpan({ tracer, name = 'test-span' }) {
const span = createSpan({ name, kind: SpanKind.SERVER, tracer })
span.setAttribute(SEMATTRS_HTTP_METHOD, 'PUT')
span.setAttribute(SEMATTRS_HTTP_ROUTE, '/user/:id')
span.setAttribute(SEMATTRS_HTTP_URL, '/user/1')
return span
}

function createBaseHttpSpan({ tracer, name = 'test-span' }) {
return createSpan({ name, kind: SpanKind.SERVER, tracer })
}

module.exports = {
createBaseHttpSpan,
createHttpServerSpan,
createRpcServerSpan
}
4 changes: 2 additions & 2 deletions test/unit/lib/otel/fixtures/span.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ const { Span } = require('@opentelemetry/sdk-trace-base')

module.exports = function createSpan({ parentId, tracer, tx, kind, name }) {
const spanContext = {
traceId: tx.trace.id,
spanId: tx.trace.root.id,
traceId: tx?.trace?.id,
spanId: tx?.trace?.root?.id,
traceFlags: TraceFlags.SAMPLED
}
return new Span(tracer, ROOT_CONTEXT, name, spanContext, kind, parentId)
Expand Down
Loading

0 comments on commit 98b349e

Please sign in to comment.