Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -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'
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const OTLP = /** @type {any} */ (ctx.semConv);
/**
* @typedef {Object} InstrumentationMapping
* @property {SpanNameFunction} [spanName]
* @property {AttributeMapping[]} [spanAttributes]
* @property {AttributeMapping[] | ((spanData: Record<string, any>) => SpanAttribute[])} [spanAttributes]
*/

/**
Expand Down Expand Up @@ -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]) }));
}
}
};

Expand All @@ -460,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;
}

/**
Expand Down Expand Up @@ -537,15 +558,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') {
Comment thread
kirrg001 marked this conversation as resolved.
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);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {}
Expand Down Expand Up @@ -497,6 +517,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', () => {
Expand Down Expand Up @@ -708,4 +761,200 @@ 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).to.deep.equal({
code: OTLP_STATUS_CODES.ERROR,
message: 'sdk failed'
Comment thread
kirrg001 marked this conversation as resolved.
});
});
});
});
});