Skip to content

Conversation

JacksonWeber
Copy link
Contributor

Which problem is this PR solving?

Updates the logs SDK to support the logger configurator, minimum severity filtering and trace-based log filtering.

Short description of the changes

This pull request introduces a configurable logger filtering mechanism to the OpenTelemetry SDK logs package, allowing log records to be filtered based on severity and trace sampling, and enabling per-logger configuration through a new pattern-based API. The core changes add a LoggerConfigurator interface, implement pattern-based configuration, and apply these configurations during log record emission. This improves flexibility and compliance with logging specifications.

Logger configuration and filtering enhancements

  • Added LoggerConfig and LoggerConfigurator types, allowing logger behavior (such as minimum severity and trace-based filtering) to be configured per logger. These are now part of LoggerProviderConfig and are used throughout the logger lifecycle. [1] [2]
  • Implemented the createLoggerConfigurator utility in LoggerConfigurators.ts to support pattern-based logger configuration, including wildcard matching for logger names.
  • Updated LoggerProviderSharedState to cache and retrieve per-logger configuration using the new configurator function, ensuring efficient and correct config application. [1] [2]

Log record filtering logic

  • Enhanced the Logger.emit method to apply minimum severity and trace-based filtering according to the resolved logger configuration, dropping log records that do not meet the criteria. [1] [2]

API and test updates

  • Exported new configuration types and utilities from the package entrypoint for external use, and updated tests to cover the new configurator and filtering logic. [1] [2] [3]

Type of change

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration

  • Unit tests for each type of filtering added

test-minimum-severity.js

const { LoggerProvider, createLoggerConfigurator, InMemoryLogRecordExporter, SimpleLogRecordProcessor } = require('./build/src/index.js');
const { SeverityNumber } = require('@opentelemetry/api-logs');
// Create logger configurator with minimum severity level of ERROR
const loggerConfigurator = createLoggerConfigurator([
    {
        pattern: '*', // Match all loggers
        config: {
            minimumSeverity: SeverityNumber.ERROR // Only process ERROR level and above
        }
    }
]);
// Create an exporter to capture logs
const exporter = new InMemoryLogRecordExporter();
// Create logger provider with configurator and processor
const loggerProvider = new LoggerProvider({
    loggerConfigurator: loggerConfigurator,
    processors: [new SimpleLogRecordProcessor(exporter)]
});
// Get logger
const logger = loggerProvider.getLogger('test-app');
// Emit logs at different levels
console.log('\n--- Emitting logs at different severity levels ---');
logger.emit({ body: 'Debug message', severityNumber: SeverityNumber.DEBUG });
logger.emit({ body: 'Info message', severityNumber: SeverityNumber.INFO });
logger.emit({ body: 'Warn message', severityNumber: SeverityNumber.WARN });
logger.emit({ body: 'Error message', severityNumber: SeverityNumber.ERROR });
logger.emit({ body: 'Fatal message', severityNumber: SeverityNumber.FATAL });
// Check what was exported
const exportedLogs = exporter.getFinishedLogRecords();
console.log('\n--- Exported logs (should only be ERROR and FATAL) ---');
console.log(`Total exported: ${exportedLogs.length}`);
exportedLogs.forEach(log => {
    console.log(`  - ${log.body} (severity: ${log.severityNumber})`);
});
console.log('\n--- Expected: 2 logs (ERROR and FATAL) ---');
console.log(`Actual: ${exportedLogs.length} logs`);
console.log(exportedLogs.length === 2 ? '✅ PASS' : '❌ FAIL');

test-trace-based-sampling.js

const { LoggerProvider, createLoggerConfigurator, InMemoryLogRecordExporter, SimpleLogRecordProcessor } = require('./build/src/index.js');
const { SeverityNumber } = require('@opentelemetry/api-logs');
const { trace, context, TraceFlags, ROOT_CONTEXT } = require('@opentelemetry/api');
console.log('='.repeat(80));
console.log('Testing Trace-Based Filtering');
console.log('='.repeat(80));
// Create logger configurator with trace-based filtering enabled
const loggerConfigurator = createLoggerConfigurator([
    {
        pattern: '*',
        config: {
            traceBased: true  // Enable trace-based filtering
        }
    }
]);
// Create an exporter to capture logs
const exporter = new InMemoryLogRecordExporter();
// Create logger provider with configurator and processor
const loggerProvider = new LoggerProvider({
    loggerConfigurator: loggerConfigurator,
    processors: [new SimpleLogRecordProcessor(exporter)]
});
// Get logger
const logger = loggerProvider.getLogger('test-app');
console.log('\n--- Test 1: Log with SAMPLED trace (should be exported) ---');
const sampledSpanContext = {
    traceId: 'd4cda95b652f4a1592b449d5929fda1b',
    spanId: '6e0c63257de34c92',
    traceFlags: TraceFlags.SAMPLED  // 0x01
};
const sampledContext = trace.setSpanContext(ROOT_CONTEXT, sampledSpanContext);
logger.emit({
    body: 'Log from SAMPLED trace',
    severityNumber: SeverityNumber.INFO,
    context: sampledContext
});
console.log(`✓ Emitted log with sampled trace (traceFlags=${sampledSpanContext.traceFlags})`);
console.log('\n--- Test 2: Log with UNSAMPLED trace (should be DROPPED) ---');
const unsampledSpanContext = {
    traceId: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4',
    spanId: '7f8e9d0c1b2a3f4e',
    traceFlags: TraceFlags.NONE  // 0x00 (unsampled)
};
const unsampledContext = trace.setSpanContext(ROOT_CONTEXT, unsampledSpanContext);
logger.emit({
    body: 'Log from UNSAMPLED trace - should be dropped',
    severityNumber: SeverityNumber.ERROR,
    context: unsampledContext
});
console.log(`✓ Emitted log with unsampled trace (traceFlags=${unsampledSpanContext.traceFlags})`);
console.log('\n--- Test 3: Log without trace context (should be exported) ---');
logger.emit({
    body: 'Log without trace context',
    severityNumber: SeverityNumber.WARN,
    context: ROOT_CONTEXT
});
console.log('✓ Emitted log without trace context');
console.log('\n--- Test 4: Log with no context specified (should be exported) ---');
logger.emit({
    body: 'Log with implicit context',
    severityNumber: SeverityNumber.DEBUG
    // No context specified - uses active context (which is ROOT_CONTEXT with no span)
});
console.log('✓ Emitted log with implicit context');
// Check what was exported
const exportedLogs = exporter.getFinishedLogRecords();
console.log('\n' + '='.repeat(80));
console.log('RESULTS');
console.log('='.repeat(80));
console.log(`\nTotal logs emitted: 4`);
console.log(`Total logs exported: ${exportedLogs.length}`);
console.log(`Expected exports: 3 (only unsampled trace should be dropped)\n`);
exportedLogs.forEach((log, index) => {
    const traceFlags = log.spanContext?.traceFlags;
    const traceFlagsStr = traceFlags !== undefined ? `0x${traceFlags.toString(16).padStart(2, '0')}` : 'none';
    console.log(`${index + 1}. "${log.body}"`);
    console.log(`   - Severity: ${log.severityNumber} (${log.severityText || 'N/A'})`);
    console.log(`   - TraceFlags: ${traceFlagsStr}`);
    if (log.spanContext) {
        console.log(`   - TraceId: ${log.spanContext.traceId}`);
        console.log(`   - SpanId: ${log.spanContext.spanId}`);
    }
    console.log('');
});
console.log('='.repeat(80));
console.log('VALIDATION');
console.log('='.repeat(80));
const results = {
    correctCount: exportedLogs.length === 3,
    hasSampledLog: exportedLogs.some(log => log.body === 'Log from SAMPLED trace'),
    noUnsampledLog: !exportedLogs.some(log => log.body.includes('UNSAMPLED')),
    hasNoTraceLog: exportedLogs.some(log => log.body === 'Log without trace context'),
    hasImplicitLog: exportedLogs.some(log => log.body === 'Log with implicit context')
};
console.log(`\n✓ Correct number of logs (3): ${results.correctCount ? '✅ PASS' : '❌ FAIL'}`);
console.log(`✓ Sampled trace log exported: ${results.hasSampledLog ? '✅ PASS' : '❌ FAIL'}`);
console.log(`✓ Unsampled trace log dropped: ${results.noUnsampledLog ? '✅ PASS' : '❌ FAIL'}`);
console.log(`✓ No-trace-context log exported: ${results.hasNoTraceLog ? '✅ PASS' : '❌ FAIL'}`);
console.log(`✓ Implicit-context log exported: ${results.hasImplicitLog ? '✅ PASS' : '❌ FAIL'}`);
const allPassed = Object.values(results).every(r => r === true);
console.log(`\n${'='.repeat(80)}`);
console.log(`OVERALL: ${allPassed ? '✅ ALL TESTS PASSED' : '❌ SOME TESTS FAILED'}`);
console.log('='.repeat(80));
process.exit(allPassed ? 0 : 1);

Checklist:

  • Followed the style guidelines of this project
  • Unit tests have been added
  • Documentation has been updated

@JacksonWeber JacksonWeber requested a review from a team as a code owner October 7, 2025 02:12
Copy link

codecov bot commented Oct 7, 2025

Codecov Report

❌ Patch coverage is 98.18182% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 95.23%. Comparing base (dbb41c6) to head (afc2b6b).

Files with missing lines Patch % Lines
...ackages/sdk-logs/src/config/LoggerConfigurators.ts 95.45% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5991      +/-   ##
==========================================
+ Coverage   95.22%   95.23%   +0.01%     
==========================================
  Files         311      312       +1     
  Lines        8603     8655      +52     
  Branches     1801     1817      +16     
==========================================
+ Hits         8192     8243      +51     
- Misses        411      412       +1     
Files with missing lines Coverage Δ
experimental/packages/sdk-logs/src/Logger.ts 100.00% <100.00%> (ø)
...perimental/packages/sdk-logs/src/LoggerProvider.ts 100.00% <100.00%> (ø)
...sdk-logs/src/internal/LoggerProviderSharedState.ts 100.00% <100.00%> (ø)
...ackages/sdk-logs/src/config/LoggerConfigurators.ts 95.45% <95.45%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Member

@pichlermarc pichlermarc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specification: https://github.com/open-telemetry/opentelemetry-specification/blame/f31acdf68b1c886269ab39011a338a8d72dbebea/specification/logs/sdk.md#L102-L133

@JacksonWeber - please also mark all features that are marked as in-development in the spec as @experimental in the TSdoc. This way we can avoid blocking https://github.com/open-telemetry/opentelemetry-js/milestone/19

@JacksonWeber
Copy link
Contributor Author

Specification: https://github.com/open-telemetry/opentelemetry-specification/blame/f31acdf68b1c886269ab39011a338a8d72dbebea/specification/logs/sdk.md#L102-L133

@JacksonWeber - please also mark all features that are marked as in-development in the spec as @experimental in the TSdoc. This way we can avoid blocking https://github.com/open-telemetry/opentelemetry-js/milestone/19

Experimental features have been marked as such. Thanks for the feedback!

@pichlermarc pichlermarc added the spec-feature This is a request to implement a new feature which is already specified by the OTel specification label Oct 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg:sdk-logs spec-feature This is a request to implement a new feature which is already specified by the OTel specification

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants