From 9ec5b4540822c3212f80487e8a9da6a7e1c33fd9 Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Thu, 17 Sep 2026 11:52:53 +0200 Subject: [PATCH 1/5] feat(otlp): added support for mapping Instana spans refs https://jsw.ibm.com/browse/INSTA-115765 --- .../otlpExporter/traces/mappers/constants.js | 3 +- .../mappers/instanaInstrumentationMappings.js | 34 ++- .../instanaInstrumentationMappings_test.js | 194 ++++++++++++++++++ 3 files changed, 223 insertions(+), 8 deletions(-) diff --git a/packages/core/src/otlpExporter/traces/mappers/constants.js b/packages/core/src/otlpExporter/traces/mappers/constants.js index e801892890..f2e418c34b 100644 --- a/packages/core/src/otlpExporter/traces/mappers/constants.js +++ b/packages/core/src/otlpExporter/traces/mappers/constants.js @@ -36,7 +36,8 @@ exports.INSTRUMENTATION_TYPES = { KINESIS: 'kinesis', AZSTORAGE: 'azstorage', AWS_LAMBDA_INVOKE: 'aws.lambda.invoke', - AWS_LAMBDA_ENTRY: 'lambda' + AWS_LAMBDA_ENTRY: 'lambda', + SDK: 'sdk' }; /** diff --git a/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js b/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js index c74582d67b..27cf86be2e 100644 --- a/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js +++ b/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js @@ -61,7 +61,7 @@ const OTLP = /** @type {any} */ (ctx.semConv); /** * @typedef {Object} InstrumentationMapping * @property {SpanNameFunction} [spanName] - * @property {AttributeMapping[]} [spanAttributes] + * @property {AttributeMapping[] | ((spanData: Record) => SpanAttribute[])} [spanAttributes] */ /** @@ -448,6 +448,20 @@ const instrumentationMappings = { { otlp: OTLP.network.PEER_NAME, instana: 'hostname' }, { otlp: OTLP.network.PEER_PORT, instana: 'port' } ] + }, + + // SDK spans are created via the Instana SDK API by the user. + // There are no official OTel semantic conventions for these spans. + // Tags from sdk.custom.tags are expanded directly as flat attributes (no prefix). + [INSTRUMENTATION_TYPES.SDK]: { + spanName: data => data.name, + spanAttributes: spanData => { + const tags = spanData?.custom?.tags; + if (!tags || typeof tags !== 'object') return []; + return Object.keys(tags) + .filter(k => tags[k] !== null && tags[k] !== undefined) + .map(k => ({ key: k, value: formatOTLPValue(tags[k]) })); + } } }; @@ -537,15 +551,21 @@ module.exports = { const handler = instrumentationMappings[spanType]?.spanAttributes; const spanData = span.data[spanType]; - if (!Array.isArray(handler) || !spanData) { + if (!handler || !spanData) { continue; } - for (let j = 0; j < handler.length; j++) { - const attribute = applyMapping(handler[j], spanData); - - if (attribute) { - attributes.push(attribute); + if (typeof handler === 'function') { + const expanded = handler(spanData); + for (let j = 0; j < expanded.length; j++) { + attributes.push(expanded[j]); + } + } else { + for (let j = 0; j < handler.length; j++) { + const attribute = applyMapping(handler[j], spanData); + if (attribute) { + attributes.push(attribute); + } } } } diff --git a/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js b/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js index 7776707407..162ef7097b 100644 --- a/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js +++ b/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js @@ -708,4 +708,198 @@ describe('otlpExporter/traces/mappers/instanaInstrumentationMappings', () => { }); }); }); + + describe('SDK spans', () => { + describe('spanName', () => { + it('should use sdk.name as span name', () => { + const span = { + n: 'sdk', + data: { + sdk: { + name: 'my-operation', + type: 'entry' + } + } + }; + + const result = spanName(span); + expect(result).to.equal('my-operation'); + }); + + }); + + describe('spanAttributes', () => { + it('should return empty attributes when no custom tags', () => { + const span = { + n: 'sdk', + data: { + sdk: { + name: 'bare-operation', + type: 'exit' + } + } + }; + + const result = spanAttributes(span); + expect(result).to.have.lengthOf(0); + }); + + it('should expand sdk.custom.tags directly as flat attributes (no prefix)', () => { + const span = { + n: 'sdk', + data: { + sdk: { + name: 'my-operation', + type: 'exit', + custom: { + tags: { + userId: '42', + region: 'eu-west-1' + } + } + } + } + }; + + const result = spanAttributes(span); + expect(result).to.deep.include({ key: 'userId', value: { stringValue: '42' } }); + expect(result).to.deep.include({ key: 'region', value: { stringValue: 'eu-west-1' } }); + }); + + it('should expand numeric and boolean tag values correctly', () => { + const span = { + n: 'sdk', + data: { + sdk: { + name: 'my-operation', + type: 'intermediate', + custom: { + tags: { + retryCount: 3, + success: false + } + } + } + } + }; + + const result = spanAttributes(span); + expect(result).to.deep.include({ key: 'retryCount', value: { intValue: 3 } }); + expect(result).to.deep.include({ key: 'success', value: { boolValue: false } }); + }); + + it('should not include tags with null or undefined values', () => { + const span = { + n: 'sdk', + data: { + sdk: { + name: 'my-operation', + type: 'entry', + custom: { + tags: { + present: 'yes', + missing: null, + absent: undefined + } + } + } + } + }; + + const result = spanAttributes(span); + const keys = result.map(a => a.key); + expect(keys).to.include('present'); + expect(keys).to.not.include('missing'); + expect(keys).to.not.include('absent'); + }); + }); + + it('should merge tags from start and complete into flat attributes', () => { + // Simulates: startExitSpan('op', { path: '/tmp/file', encoding: 'UTF-8' }) + // completeExitSpan(null, { success: true }) + // -> sdk.js deepMerges both into sdk.custom.tags + const span = { + n: 'sdk', + data: { + sdk: { + name: 'file-access', + type: 'exit', + custom: { + tags: { + path: '/tmp/file', + encoding: 'UTF-8', + success: true + } + } + } + } + }; + + const result = spanAttributes(span); + expect(result).to.deep.include({ key: 'path', value: { stringValue: '/tmp/file' } }); + expect(result).to.deep.include({ key: 'encoding', value: { stringValue: 'UTF-8' } }); + expect(result).to.deep.include({ key: 'success', value: { boolValue: true } }); + }); + + it('should expose error message tag when set via completeSpan(error)', () => { + // Simulates: completeExitSpan(new Error('Boom!')) + // -> sdk.js writes error.message into sdk.custom.tags.message + const span = { + n: 'sdk', + ec: 1, + data: { + sdk: { + name: 'file-access', + type: 'exit', + custom: { + tags: { + message: 'Boom!' + } + } + } + } + }; + + const result = spanAttributes(span); + expect(result).to.deep.include({ key: 'message', value: { stringValue: 'Boom!' } }); + }); + + describe('spanStatus', () => { + it('should return UNSET status for a successful SDK span', () => { + const span = { + n: 'sdk', + data: { + sdk: { + name: 'my-operation', + type: 'entry' + } + } + }; + + const result = spanStatus(span); + expect(result).to.deep.equal({ code: OTLP_STATUS_CODES.UNSET }); + }); + + it('should return ERROR status for an SDK span with ec=1', () => { + const span = { + n: 'sdk', + ec: 1, + data: { + sdk: { + name: 'my-operation', + type: 'entry', + custom: { + tags: { + message: 'something went wrong' + } + } + } + } + }; + + const result = spanStatus(span); + expect(result.code).to.equal(OTLP_STATUS_CODES.ERROR); + }); + }); + }); }); From 5b0071b3d7dae37a715bbe7a67c6c048392da0df Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Thu, 17 Sep 2026 11:54:14 +0200 Subject: [PATCH 2/5] chore: lint --- .../traces/mappers/instanaInstrumentationMappings_test.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js b/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js index 162ef7097b..59c132179c 100644 --- a/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js +++ b/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js @@ -725,7 +725,6 @@ describe('otlpExporter/traces/mappers/instanaInstrumentationMappings', () => { const result = spanName(span); expect(result).to.equal('my-operation'); }); - }); describe('spanAttributes', () => { @@ -836,9 +835,9 @@ describe('otlpExporter/traces/mappers/instanaInstrumentationMappings', () => { }; const result = spanAttributes(span); - expect(result).to.deep.include({ key: 'path', value: { stringValue: '/tmp/file' } }); + expect(result).to.deep.include({ key: 'path', value: { stringValue: '/tmp/file' } }); expect(result).to.deep.include({ key: 'encoding', value: { stringValue: 'UTF-8' } }); - expect(result).to.deep.include({ key: 'success', value: { boolValue: true } }); + expect(result).to.deep.include({ key: 'success', value: { boolValue: true } }); }); it('should expose error message tag when set via completeSpan(error)', () => { From 5035e0f5546d228f1e75f6fce7621d8b93f55a7f Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Thu, 17 Sep 2026 11:58:37 +0200 Subject: [PATCH 3/5] test: extended test --- .../instanaInstrumentationMappings_test.js | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js b/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js index 59c132179c..381a03c1af 100644 --- a/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js +++ b/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js @@ -497,6 +497,39 @@ describe('otlpExporter/traces/mappers/instanaInstrumentationMappings', () => { expect(getAttr('graphql.document')).to.be.undefined; expect(getAttr('error.type')).to.be.undefined; }); + + it('should map both http and sdk data keys from a single span', () => { + const span = { + n: 'node.http.server', + data: { + http: { + operation: 'GET', + path: '/api/orders', + status: 200 + }, + sdk: { + custom: { + tags: { + 'order.id': '42', + 'user.id': 'u-99' + } + } + } + } + }; + + const result = spanAttributes(span); + const getAttr = key => result.find(a => a.key === key); + + // http data keys + expect(getAttr('http.method').value).to.deep.equal({ stringValue: 'GET' }); + expect(getAttr('http.target').value).to.deep.equal({ stringValue: '/api/orders' }); + expect(getAttr('http.status_code').value).to.deep.equal({ intValue: 200 }); + + // sdk data keys (custom tags expanded as flat attributes) + expect(getAttr('order.id').value).to.deep.equal({ stringValue: '42' }); + expect(getAttr('user.id').value).to.deep.equal({ stringValue: 'u-99' }); + }); }); describe('spanStatus', () => { From 1d79003377a108a6f0e53c4af2d46c3ef7368c29 Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Thu, 17 Sep 2026 12:32:15 +0200 Subject: [PATCH 4/5] test: extended assert --- .../traces/mappers/instanaInstrumentationMappings_test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js b/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js index 381a03c1af..d4b875ab73 100644 --- a/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js +++ b/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js @@ -930,7 +930,10 @@ describe('otlpExporter/traces/mappers/instanaInstrumentationMappings', () => { }; const result = spanStatus(span); - expect(result.code).to.equal(OTLP_STATUS_CODES.ERROR); + expect(result).to.deep.equal({ + code: OTLP_STATUS_CODES.ERROR, + message: 'sdk failed' + }); }); }); }); From e7366527a0e2f7dba82a2464e38683cde5e0d687 Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Fri, 18 Sep 2026 08:46:22 +0200 Subject: [PATCH 5/5] chore: extended type creation --- .../mappers/instanaInstrumentationMappings.js | 11 ++++++++-- .../instanaInstrumentationMappings_test.js | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js b/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js index 27cf86be2e..698ed2a176 100644 --- a/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js +++ b/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js @@ -474,11 +474,18 @@ function getSpanType(span) { return null; } - const key = Object.keys(span.data).find( + const keys = Object.keys(span.data).filter( k => k !== INSTRUMENTATION_TYPES.PEER && k !== SPECIAL_SPAN_DATA_TYPES.RESOURCE ); - return key || null; + // CASE: ignore SDK data key if its multiple data keys, because + // we always prefer the other data key such as http + if (keys.length > 1) { + const nonSdk = keys.find(k => k !== INSTRUMENTATION_TYPES.SDK); + if (nonSdk) return nonSdk; + } + + return keys[0] || null; } /** diff --git a/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js b/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js index d4b875ab73..5c38c93e6d 100644 --- a/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js +++ b/packages/core/test/otlpExporter/traces/mappers/instanaInstrumentationMappings_test.js @@ -232,6 +232,26 @@ describe('otlpExporter/traces/mappers/instanaInstrumentationMappings', () => { expect(result).to.equal('custom.span'); }); + it('should use the actual instrumentation type when sdk coexists with another type', () => { + const span = { + n: 'node.http.server', + ec: 1, + data: { + sdk: { custom: { tags: { foo: 'bar' } } }, + http: { operation: 'GET', path: '/api' } + } + }; + + // spanName uses getSpanType internally — 'GET /api' proves type='http', not 'sdk' + expect(spanName(span)).to.equal('GET /api'); + + // spanStatus.message explicitly shows the resolved type: 'http failed', not 'sdk failed' + expect(spanStatus(span)).to.deep.equal({ + code: OTLP_STATUS_CODES.ERROR, + message: 'http failed' + }); + }); + it('should return "unknown" when span has no name or type', () => { const span = { data: {}