Skip to content

Commit

Permalink
feat: RD-14190-Redis-and-Mongo-reduced-instrumentation (#554)
Browse files Browse the repository at this point in the history
* feat: mongodg reduced 
* feat: redisSampler
  • Loading branch information
eugene-lumigo authored Dec 9, 2024
1 parent cd00109 commit 00055ab
Show file tree
Hide file tree
Showing 12 changed files with 833 additions and 28 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ 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`.
Defaults to `true`, meaning 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`.
Defaults to `true`, meaning the Redis instrumentation reduces the amount of data collected.

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';
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);
});
});
75 changes: 75 additions & 0 deletions src/samplers/mongodbSampler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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.
// https://lumigo.atlassian.net/browse/RD-14250
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`
);
return { decision: SamplingDecision.NOT_RECORD };
}

return { decision: SamplingDecision.RECORD_AND_SAMPLED };
}
}

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;
const isReducedMongoInstrumentationEnabled =
reduceMongoInstrumentation == null ||
reduceMongoInstrumentation === '' ||
reduceMongoInstrumentation.toLowerCase() !== 'false';

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

0 comments on commit 00055ab

Please sign in to comment.