-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: RD-14190-Redis-and-Mongo-reduced-instrumentation (#554)
* feat: mongodg reduced * feat: redisSampler
- Loading branch information
1 parent
cd00109
commit 00055ab
Showing
12 changed files
with
833 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); | ||
}; |
Oops, something went wrong.