Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: RD-14190-Redis-and-Mongo-reduced-instrumentation #554

Merged
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ This setting is independent from `LUMIGO_DEBUG`, that is, `LUMIGO_DEBUG` does no
* `LUMIGO_FILTER_HTTP_ENDPOINTS_REGEX='["regex1", "regex2"]'`: This option enables the filtering of client and server endpoints through regular expression searches. Fine-tune your settings via the following environment variables, which work in conjunction with `LUMIGO_FILTER_HTTP_ENDPOINTS_REGEX` for a specific span type:
* `LUMIGO_FILTER_HTTP_ENDPOINTS_REGEX_SERVER` applies the regular expression search exclusively to server spans. Searching is performed against the following attributes on a span: `url.path` and `http.target`.
* `LUMIGO_FILTER_HTTP_ENDPOINTS_REGEX_CLIENT` applies the regular expression search exclusively to client spans. Searching is performed against the following attributes on a span: `url.full` and `http.url`.
* `LUMIGO_REDUCED_MONGO_INSTRUMENTATION=true`: Reduces the amount of data collected by the MongoDB [instrumentation](https://www.npmjs.com/package/@opentelemetry/instrumentation-mongodb), such as not collecting the `db.operation` attribute `isMaster`. By default, the MongoDB instrumentation reduces the amount of data collected.
* `LUMIGO_REDUCED_REDIS_INSTRUMENTATION=true`: Reduces the amount of data collected by the Redis [instrumentation](https://www.npmjs.com/package/@opentelemetry/instrumentation-redis-4), such as not collecting the `db.statement` attribute `INFO`. By default, the Redis instrumentation reduces the amount of data collected.
eugene-lumigo marked this conversation as resolved.
Show resolved Hide resolved

For more information check out [Filtering http endpoints](#filtering-http-endpoints).

Expand Down
4 changes: 2 additions & 2 deletions src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ import { dirname, join } from 'path';
import { logger } from './logging';
import { ProcessEnvironmentDetector } from './resources/detectors/ProcessEnvironmentDetector';
import { LumigoSpanProcessor } from './resources/spanProcessor';
import { getLumigoSampler } from './samplers/lumigoSampler';
import { getCombinedSampler } from './samplers/combinedSampler';
import { LumigoLogRecordProcessor } from './processors/LumigoLogRecordProcessor';

const lumigoTraceEndpoint = process.env.LUMIGO_ENDPOINT || DEFAULT_LUMIGO_TRACES_ENDPOINT;
Expand Down Expand Up @@ -209,7 +209,7 @@ export const init = async (): Promise<LumigoSdkInitialization> => {
.merge(await new ProcessEnvironmentDetector().detect());

const tracerProvider = new NodeTracerProvider({
sampler: getLumigoSampler(),
sampler: getCombinedSampler(),
resource,
spanLimits: {
attributeValueLengthLimit: getSpanAttributeMaxLength(),
Expand Down
74 changes: 74 additions & 0 deletions src/samplers/combinedSampler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { CombinedSampler, getCombinedSampler } from './combinedSampler';
import { LumigoSampler } from './lumigoSampler';
import { MongodbSampler } from './mongodbSampler';
import { Context, SpanKind } from '@opentelemetry/api';
import { SamplingDecision } from '@opentelemetry/sdk-trace-base';

describe('CombinedSampler', () => {
let lumigoSampler: LumigoSampler;
let mongodbSampler: MongodbSampler;
let combinedSampler: CombinedSampler;

beforeEach(() => {
lumigoSampler = new LumigoSampler();
mongodbSampler = new MongodbSampler();
combinedSampler = new CombinedSampler(lumigoSampler, mongodbSampler);
});

it('should return NOT_RECORD if any sampler returns NOT_RECORD', () => {
jest
.spyOn(lumigoSampler, 'shouldSample')
.mockReturnValue({ decision: SamplingDecision.NOT_RECORD });
jest
.spyOn(mongodbSampler, 'shouldSample')
.mockReturnValue({ decision: SamplingDecision.RECORD_AND_SAMPLED });

const result = combinedSampler.shouldSample(
{} as Context,
'traceId',
'spanName',
SpanKind.CLIENT,
{},
[]
);
expect(result.decision).toBe(SamplingDecision.NOT_RECORD);
});

it('should return RECORD_AND_SAMPLED if all samplers return RECORD_AND_SAMPLED', () => {
jest
.spyOn(lumigoSampler, 'shouldSample')
.mockReturnValue({ decision: SamplingDecision.RECORD_AND_SAMPLED });
jest
.spyOn(mongodbSampler, 'shouldSample')
.mockReturnValue({ decision: SamplingDecision.RECORD_AND_SAMPLED });

const result = combinedSampler.shouldSample(
{} as Context,
'traceId',
'spanName',
SpanKind.CLIENT,
{},
[]
);
expect(result.decision).toBe(SamplingDecision.RECORD_AND_SAMPLED);
});

it('should return RECORD_AND_SAMPLED if no samplers return NOT_RECORD', () => {
jest
.spyOn(lumigoSampler, 'shouldSample')
.mockReturnValue({ decision: SamplingDecision.RECORD_AND_SAMPLED });
jest
.spyOn(mongodbSampler, 'shouldSample')
.mockReturnValue({ decision: SamplingDecision.RECORD });

const result = combinedSampler.shouldSample(
{} as Context,
'traceId',
'spanName',
SpanKind.CLIENT,
{},
[]
);
expect(result.decision).toBe(SamplingDecision.RECORD_AND_SAMPLED);
});
});
54 changes: 54 additions & 0 deletions src/samplers/combinedSampler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
Sampler,
SamplingResult,
SamplingDecision,
ParentBasedSampler,
} from '@opentelemetry/sdk-trace-base';
import type { Context, Link, Attributes, SpanKind } from '@opentelemetry/api';

import { LumigoSampler } from './lumigoSampler';
import { MongodbSampler } from './mongodbSampler';
import { RedisSampler } from './redisSampler';

export class CombinedSampler implements Sampler {
private samplers: Sampler[];

constructor(...samplers: Sampler[]) {
this.samplers = samplers;
}
/* eslint-disable @typescript-eslint/no-unused-vars */
shouldSample(
context: Context,
traceId: string,
spanName: string,
spanKind: SpanKind,
attributes: Attributes,
links: Link[]
): SamplingResult {
// Iterate through each sampler
for (const sampler of this.samplers) {
const result = sampler.shouldSample(context, traceId, spanName, spanKind, attributes, links);

// If any sampler decides NOT_RECORD, we respect that decision
if (result.decision === SamplingDecision.NOT_RECORD) {
return result;
}
}

// If none decided to NOT_RECORD, we default to RECORD_AND_SAMPLED
return { decision: SamplingDecision.RECORD_AND_SAMPLED };
}
}

export const getCombinedSampler = () => {
const lumigoSampler = new LumigoSampler();
const mongodbSampler = new MongodbSampler();
const redisSampler = new RedisSampler();
const combinedSampler = new CombinedSampler(lumigoSampler, mongodbSampler, redisSampler);

return new ParentBasedSampler({
root: combinedSampler,
remoteParentSampled: combinedSampler,
localParentSampled: combinedSampler,
});
};
151 changes: 151 additions & 0 deletions src/samplers/mongodbSampler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { MongodbSampler, extractClientAttribute, matchMongoIsMaster } from './mongodbSampler';
import { Context, SpanKind, Attributes, Link } from '@opentelemetry/api';
import { SamplingDecision } from '@opentelemetry/sdk-trace-base';

describe('MongodbSampler', () => {
let sampler: MongodbSampler;

beforeEach(() => {
sampler = new MongodbSampler();
delete process.env.LUMIGO_REDUCED_MONGO_INSTRUMENTATION;
});

it('should return RECORD_AND_SAMPLED when dbSystem and dbOperation are not provided', () => {
const result = sampler.shouldSample(
{} as Context,
'traceId',
'spanName',
SpanKind.CLIENT,
{},
[]
);
expect(result.decision).toBe(SamplingDecision.RECORD_AND_SAMPLED);
});

it('should return NOT_RECORD when dbSystem is mongodb and dbOperation is isMaster and LUMIGO_REDUCED_MONGO_INSTRUMENTATION is true', () => {
process.env.LUMIGO_REDUCED_MONGO_INSTRUMENTATION = 'true';
eugene-lumigo marked this conversation as resolved.
Show resolved Hide resolved
const attributes: Attributes = { 'db.system': 'mongodb', 'db.operation': 'isMaster' };
const result = sampler.shouldSample(
{} as Context,
'traceId',
'spanName',
SpanKind.CLIENT,
attributes,
[]
);
expect(result.decision).toBe(SamplingDecision.NOT_RECORD);
});

it('should return NOT_RECORD when dbSystem is mongodb and dbOperation is isMaster', () => {
const attributes: Attributes = { 'db.system': 'mongodb', 'db.operation': 'isMaster' };
const result = sampler.shouldSample(
{} as Context,
'traceId',
'spanName',
SpanKind.CLIENT,
attributes,
[]
);
expect(result.decision).toBe(SamplingDecision.NOT_RECORD);
});

it('should return NOT_RECORD when spanName is mongodb.isMaster', () => {
const attributes: Attributes = {};
const result = sampler.shouldSample(
{} as Context,
'traceId',
'mongodb.isMaster',
SpanKind.CLIENT,
attributes,
[]
);
expect(result.decision).toBe(SamplingDecision.NOT_RECORD);
});

it('should return RECORD_AND_SAMPLED when dbSystem is mongodb and dbOperation is not isMaster', () => {
process.env.LUMIGO_REDUCED_MONGO_INSTRUMENTATION = 'true';
const attributes: Attributes = { 'db.system': 'mongodb', 'db.operation': 'find' };
const result = sampler.shouldSample(
{} as Context,
'traceId',
'spanName',
SpanKind.CLIENT,
attributes,
[]
);
expect(result.decision).toBe(SamplingDecision.RECORD_AND_SAMPLED);
});

it('should return RECORD_AND_SAMPLED when LUMIGO_REDUCED_MONGO_INSTRUMENTATION is false', () => {
process.env.LUMIGO_REDUCED_MONGO_INSTRUMENTATION = 'false';
const attributes: Attributes = { 'db.system': 'mongodb', 'db.operation': 'isMaster' };
const result = sampler.shouldSample(
{} as Context,
'traceId',
'mongodb.isMaster',
SpanKind.CLIENT,
attributes,
[]
);
expect(result.decision).toBe(SamplingDecision.RECORD_AND_SAMPLED);
});
});

describe('extractClientAttribute', () => {
it('should return the attribute value as string when attributeName is present and spanKind is CLIENT', () => {
const attributes: Attributes = { 'db.system': 'mongodb' };
const result = extractClientAttribute(attributes, 'db.system', SpanKind.CLIENT);
expect(result).toBe('mongodb');
});

it('should return null when attributeName is not present', () => {
const attributes: Attributes = {};
const result = extractClientAttribute(attributes, 'db.system', SpanKind.CLIENT);
expect(result).toBeNull();
});

it('should return null when spanKind is not CLIENT', () => {
const attributes: Attributes = { 'db.system': 'mongodb' };
const result = extractClientAttribute(attributes, 'db.system', SpanKind.SERVER);
expect(result).toBeNull();
});
});

describe('doesMatchClientSpanFiltering', () => {
beforeEach(() => {
delete process.env.LUMIGO_REDUCED_MONGO_INSTRUMENTATION;
});
it('should return true when dbSystem is mongodb, dbOperation is isMaster and LUMIGO_REDUCED_MONGO_INSTRUMENTATION is true', () => {
process.env.LUMIGO_REDUCED_MONGO_INSTRUMENTATION = 'true';
const result = matchMongoIsMaster('any', 'mongodb', 'isMaster');
expect(result).toBe(true);
});

it('should return true when dbSystem is mongodb, dbOperation is isMaster', () => {
const result = matchMongoIsMaster('any', 'mongodb', 'isMaster');
expect(result).toBe(true);
});

it('should return true when spanName is mongodb.isMaster', () => {
const result = matchMongoIsMaster('mongodb.isMaster', 'any', 'any');
expect(result).toBe(true);
});

it('should return false when dbSystem is not mongodb', () => {
process.env.LUMIGO_REDUCED_MONGO_INSTRUMENTATION = 'true';
const result = matchMongoIsMaster('any', 'mysql', 'isMaster');
expect(result).toBe(false);
});

it('should return false when dbOperation is not isMaster', () => {
process.env.LUMIGO_REDUCED_MONGO_INSTRUMENTATION = 'true';
const result = matchMongoIsMaster('any', 'mongodb', 'find');
expect(result).toBe(false);
});

it('should return false when LUMIGO_REDUCED_MONGO_INSTRUMENTATION is false', () => {
process.env.LUMIGO_REDUCED_MONGO_INSTRUMENTATION = 'false';
const result = matchMongoIsMaster('any', 'mongodb', 'isMaster');
expect(result).toBe(false);
});
});
80 changes: 80 additions & 0 deletions src/samplers/mongodbSampler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
Sampler,
ParentBasedSampler,
SamplingResult,
SamplingDecision,
} from '@opentelemetry/sdk-trace-base';
import { Context, Link, Attributes, SpanKind } from '@opentelemetry/api';
import { logger } from '../logging';

export class MongodbSampler implements Sampler {
/* eslint-disable @typescript-eslint/no-unused-vars */
shouldSample(
context: Context,
traceId: string,
spanName: string,
spanKind: SpanKind,
attributes: Attributes,
links: Link[]
): SamplingResult {
// Note, there is probably a bug in opentelemetry api, making mongoSampler always receives attributes array empty.
// This makes it impossible to filter based on db.system and db.operation attributes. Filter based on spanName only.
// Opentemetry version upgrade might fix this issue.
eugene-lumigo marked this conversation as resolved.
Show resolved Hide resolved
// https://lumigo.atlassian.net/browse/RD-14250
let decision = SamplingDecision.RECORD_AND_SAMPLED;
eugene-lumigo marked this conversation as resolved.
Show resolved Hide resolved
const dbSystem = extractClientAttribute(attributes, 'db.system', spanKind);
const dbOperation = extractClientAttribute(attributes, 'db.operation', spanKind);

if (spanKind === SpanKind.CLIENT && matchMongoIsMaster(spanName, dbSystem, dbOperation)) {
logger.debug(
`Drop span ${spanName} with db.system: ${dbSystem} and db.operation: ${dbOperation}, because LUMIGO_REDUCED_MONGO_INSTRUMENTATION is enabled`
);
decision = SamplingDecision.NOT_RECORD;
}

return { decision: decision };
}
}

export const extractClientAttribute = (
attributes: Attributes,
attributeName: string,
spanKind: SpanKind
): string | null => {
if (attributeName && spanKind === SpanKind.CLIENT) {
const attributeValue = attributes[attributeName];
return attributeValue ? attributeValue.toString() : null;
}

return null;
};

export const matchMongoIsMaster = (
spanName: string,
dbSystem: string,
dbOperation: string
): boolean => {
const reduceMongoInstrumentation = process.env.LUMIGO_REDUCED_MONGO_INSTRUMENTATION;
let isReducedMongoInstrumentationEnabled: boolean;

if (reduceMongoInstrumentation == null || reduceMongoInstrumentation === '') {
isReducedMongoInstrumentationEnabled = true; // Default to true
} else if (reduceMongoInstrumentation.toLowerCase() === 'true') {
isReducedMongoInstrumentationEnabled = true;
} else
isReducedMongoInstrumentationEnabled = reduceMongoInstrumentation.toLowerCase() !== 'false';
eugene-lumigo marked this conversation as resolved.
Show resolved Hide resolved

return (
isReducedMongoInstrumentationEnabled &&
(spanName == 'mongodb.isMaster' || (dbSystem === 'mongodb' && dbOperation === 'isMaster'))
);
};

export const getMongoDBSampler = () => {
const mongodbSampler = new MongodbSampler();
return new ParentBasedSampler({
root: mongodbSampler,
remoteParentSampled: mongodbSampler,
localParentSampled: mongodbSampler,
});
};
Loading
Loading