-
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.
- Loading branch information
Eugene Orlovsky
committed
Dec 6, 2024
1 parent
4dd4095
commit e076141
Showing
3 changed files
with
274 additions
and
1 deletion.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
import { | ||
RedisSampler, | ||
extractClientAttribute, | ||
matchRedisInfoStatement, | ||
getRedisDBSampler, | ||
} from './redisSampler'; | ||
import { Context, SpanKind, Attributes, Link } from '@opentelemetry/api'; | ||
import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; | ||
import { logger } from '../logging'; | ||
|
||
describe('RedisSampler', () => { | ||
let sampler: RedisSampler; | ||
|
||
beforeEach(() => { | ||
sampler = new RedisSampler(); | ||
delete process.env.LUMIGO_REDUCED_REDIS_INSTRUMENTATION; | ||
}); | ||
|
||
it('should return RECORD_AND_SAMPLED when dbSystem and dbStatement 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 redis and dbStatement is INFO and LUMIGO_REDUCED_REDIS_INSTRUMENTATION is true', () => { | ||
process.env.LUMIGO_REDUCED_REDIS_INSTRUMENTATION = 'true'; | ||
const attributes: Attributes = { 'db.system': 'redis', 'db.statement': 'INFO' }; | ||
const result = sampler.shouldSample( | ||
{} as Context, | ||
'traceId', | ||
'spanName', | ||
SpanKind.CLIENT, | ||
attributes, | ||
[] | ||
); | ||
expect(result.decision).toBe(SamplingDecision.NOT_RECORD); | ||
}); | ||
|
||
it('should return RECORD_AND_SAMPLED when dbSystem is redis and dbStatement is not INFO', () => { | ||
process.env.LUMIGO_REDUCED_REDIS_INSTRUMENTATION = 'true'; | ||
const attributes: Attributes = { 'db.system': 'redis', 'db.statement': 'SET key value' }; | ||
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_REDIS_INSTRUMENTATION is false', () => { | ||
process.env.LUMIGO_REDUCED_REDIS_INSTRUMENTATION = 'false'; | ||
const attributes: Attributes = { 'db.system': 'redis', 'db.statement': 'INFO' }; | ||
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 dbSystem and dbStatement are null', () => { | ||
const attributes: Attributes = { 'db.system': null, 'db.statement': null }; | ||
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 dbSystem is null and dbStatement is provided', () => { | ||
const attributes: Attributes = { 'db.system': null, 'db.statement': 'SET key value' }; | ||
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 dbSystem is provided and dbStatement is null', () => { | ||
const attributes: Attributes = { 'db.system': 'redis', 'db.statement': null }; | ||
const result = sampler.shouldSample( | ||
{} as Context, | ||
'traceId', | ||
'spanName', | ||
SpanKind.CLIENT, | ||
attributes, | ||
[] | ||
); | ||
expect(result.decision).toBe(SamplingDecision.RECORD_AND_SAMPLED); | ||
}); | ||
|
||
it('should return NOT_RECORD when dbSystem is redis and dbStatement is INFO with surrounding quotes', () => { | ||
process.env.LUMIGO_REDUCED_REDIS_INSTRUMENTATION = 'true'; | ||
const attributes: Attributes = { 'db.system': 'redis', 'db.statement': '"INFO"' }; | ||
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 redis and dbStatement is INFO SERVER', () => { | ||
process.env.LUMIGO_REDUCED_REDIS_INSTRUMENTATION = 'true'; | ||
const attributes: Attributes = { 'db.system': 'redis', 'db.statement': 'INFO SERVER' }; | ||
const result = sampler.shouldSample( | ||
{} as Context, | ||
'traceId', | ||
'spanName', | ||
SpanKind.CLIENT, | ||
attributes, | ||
[] | ||
); | ||
expect(result.decision).toBe(SamplingDecision.NOT_RECORD); | ||
}); | ||
}); | ||
|
||
describe('extractClientAttribute', () => { | ||
it('should return the attribute value as string when attributeName is present and spanKind is CLIENT', () => { | ||
const attributes: Attributes = { 'db.system': 'redis' }; | ||
const result = extractClientAttribute(attributes, 'db.system', SpanKind.CLIENT); | ||
expect(result).toBe('redis'); | ||
}); | ||
|
||
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': 'redis' }; | ||
const result = extractClientAttribute(attributes, 'db.system', SpanKind.SERVER); | ||
expect(result).toBeNull(); | ||
}); | ||
}); | ||
|
||
describe('matchRedisInfoStatement', () => { | ||
it('should return true when dbSystem is redis, dbStatement is INFO and LUMIGO_REDUCED_REDIS_INSTRUMENTATION is true', () => { | ||
process.env.LUMIGO_REDUCED_REDIS_INSTRUMENTATION = 'true'; | ||
const result = matchRedisInfoStatement('redis.Info', 'redis', 'INFO'); | ||
expect(result).toBe(true); | ||
}); | ||
|
||
it('should return false when LUMIGO_REDUCED_REDIS_INSTRUMENTATION is false', () => { | ||
process.env.LUMIGO_REDUCED_REDIS_INSTRUMENTATION = 'false'; | ||
const result = matchRedisInfoStatement('redis.Info', 'redis', 'INFO'); | ||
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,98 @@ | ||
import { | ||
Sampler, | ||
ParentBasedSampler, | ||
SamplingResult, | ||
SamplingDecision, | ||
} from '@opentelemetry/sdk-trace-base'; | ||
import { Context, Link, Attributes, SpanKind } from '@opentelemetry/api'; | ||
import { logger } from '../logging'; | ||
|
||
export class RedisSampler implements Sampler { | ||
/* eslint-disable @typescript-eslint/no-unused-vars */ | ||
shouldSample( | ||
context: Context, | ||
traceId: string, | ||
spanName: string, | ||
spanKind: SpanKind, | ||
attributes: Attributes, | ||
links: Link[] | ||
): SamplingResult { | ||
console.log(`spanName: ${spanName}, attributes: ${JSON.stringify(attributes)}`); | ||
|
||
let decision = SamplingDecision.RECORD_AND_SAMPLED; | ||
const dbSystem = extractClientAttribute(attributes, 'db.system', spanKind); | ||
const dbStatement = extractClientAttribute(attributes, 'db.statement', spanKind); | ||
|
||
if (spanKind === SpanKind.CLIENT && matchRedisInfoStatement(spanName, dbSystem, dbStatement)) { | ||
logger.debug( | ||
`Dropping span ${spanName} with db.system: ${dbSystem} and db.statement: ${dbStatement}, because LUMIGO_REDUCED_REDIS_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 matchRedisInfoStatement = ( | ||
spanName: string, | ||
dbSystem: string, | ||
dbStatement: string | null | undefined | ||
): boolean => { | ||
const reduceRedisInstrumentation = process.env.LUMIGO_REDUCED_REDIS_INSTRUMENTATION; | ||
let isReducedRedisInstrumentationEnabled: boolean; | ||
|
||
if (reduceRedisInstrumentation == null || reduceRedisInstrumentation === '') { | ||
isReducedRedisInstrumentationEnabled = true; // Default to true | ||
} else if (reduceRedisInstrumentation.toLowerCase() === 'true') { | ||
isReducedRedisInstrumentationEnabled = true; | ||
} else { | ||
isReducedRedisInstrumentationEnabled = reduceRedisInstrumentation.toLowerCase() !== 'false'; | ||
} | ||
|
||
// Safely handle null or undefined dbStatement by defaulting to empty string | ||
const safeDbStatement = dbStatement ?? ''; | ||
|
||
// Normalize dbStatement: | ||
// 1. Remove surrounding double quotes if present. | ||
// 2. Convert to uppercase for case-insensitive comparison. | ||
// 3. Trim whitespace. | ||
const normalizedDbStatement = safeDbStatement | ||
.replace(/^"(.*)"$/, '$1') | ||
.toUpperCase() | ||
.trim(); | ||
|
||
// Matches either: | ||
// - "INFO" alone | ||
// - "INFO SERVER" (with one or more spaces in between) | ||
// | ||
// Does NOT match just "SERVER". | ||
const infoRegex = /^INFO(\s+SERVER)?$/i; | ||
|
||
return ( | ||
isReducedRedisInstrumentationEnabled && | ||
(spanName === 'redis.Info' || (dbSystem === 'redis' && infoRegex.test(normalizedDbStatement))) | ||
); | ||
}; | ||
|
||
export const getRedisDBSampler = () => { | ||
const redisSampler = new RedisSampler(); | ||
return new ParentBasedSampler({ | ||
root: redisSampler, | ||
remoteParentSampled: redisSampler, | ||
localParentSampled: redisSampler, | ||
}); | ||
}; |