Skip to content

Commit

Permalink
feat: redisSampler
Browse files Browse the repository at this point in the history
  • Loading branch information
Eugene Orlovsky committed Dec 6, 2024
1 parent 4dd4095 commit e076141
Show file tree
Hide file tree
Showing 3 changed files with 274 additions and 1 deletion.
4 changes: 3 additions & 1 deletion src/samplers/combinedSampler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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[];
Expand Down Expand Up @@ -43,7 +44,8 @@ export class CombinedSampler implements Sampler {
export const getCombinedSampler = () => {
const lumigoSampler = new LumigoSampler();
const mongodbSampler = new MongodbSampler();
const combinedSampler = new CombinedSampler(lumigoSampler, mongodbSampler);
const redisSampler = new RedisSampler();
const combinedSampler = new CombinedSampler(lumigoSampler, mongodbSampler, redisSampler);

return new ParentBasedSampler({
root: combinedSampler,
Expand Down
173 changes: 173 additions & 0 deletions src/samplers/redisSampler.test.ts
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);
});
});
98 changes: 98 additions & 0 deletions src/samplers/redisSampler.ts
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,
});
};

0 comments on commit e076141

Please sign in to comment.