Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ public void emit() {
if (loggerSharedState.hasBeenShutdown()) {
return;
}

if (logger.canEmit()) {
loggerSharedState.getLoggerInstrumentation().emitLog();
}

Context context = this.context == null ? Context.current() : this.context;
if (!logger.isEnabled(severity, context)) {
return;
Expand All @@ -149,7 +154,6 @@ public void emit() {
? this.loggerSharedState.getClock().now()
: this.observedTimestampEpochNanos;

loggerSharedState.getLoggerInstrumentation().emitLog();
loggerSharedState
.getLogRecordProcessor()
.onEmit(context, createLogRecord(context, observedTimestampEpochNanos));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ public boolean isEnabled(Severity severity, Context context) {
return true;
}

/** Returns whether this logger can ever emit any log records. */
boolean canEmit() {

@anuraaga anuraaga Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Personally the need to introduce this new concept and the different behavior based on the same concept of LoggerConfig show well why this new definition seems problematic. But as I have no interest in influencing the spec I will implement whatever comes out of it ;)

/cc @trask

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So the idea here is that from a counting of otel.sdk.log.created perspective, we need to differentiate why a logger was disabled? I.e.

  • If LoggerConfig.enabled=true but its disabled due to severity/trace-based rules, count.
  • If LoggerConfig.enabled=false, don't count.

If that's right, @cijothomas I too don't understand this. From the PR it seems the goal seems to be to abide by the spec language where LoggerConfig.enabled=false behaves equivalently to a noop. But does this actually produce intuitive semantics for a user consuming otel.sdk.log.created?

To me, it seems like the semantics should be one of:

  • If Logger.enabled() returns false for any reason, do not increment otel.sdk.log.created.
  • OR increment otel.sdk.log.created always, regardless of Logger.enabled(). (This would require changing / re-interpretting the "equivalently to a noop" clause)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I put the distinction between Logger being disabled completely vs Logger doing filtering based on some criterion.

i.e

  1. LoggerConfig.enabled=false = blanket disabled Logger, must behave NoOp. In other words, this would behave similar to the situation of API only, no SDK scenario.

vs

  1. LoggerConfig has other filtering rules. Here, Logger is not completely disabled. Each log record is inspected, and filtered based on trace-based, severity-based (and more in future). This counts.

This is distinct enough - 1 is total disablement of a Logger. It won't look at the LogRecord passed to it as if OTel has never seen the LogRecord.
2 makes decision about filtering by looking at LogRecord. Log has reached a valid Logger, but then dropped.

OR increment otel.sdk.log.created always, regardless of Logger.enabled(). (This would require changing / re-interpretting the "equivalently to a noop" clause)

This is doable. The spec part requiring noop is not marked stable. Are we okay to record internal metric for a completely disabled Logger? The metric reporting cost is non-zero (though with bound/pre-built-attributes, it can be very low)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

https://github.com/open-telemetry/opentelemetry-specification/pull/5253/changes I opened this in spec to see if this is the preferred direction.

return loggerEnabled;
}

@Override
public LogRecordBuilder logRecordBuilder() {
if (loggerEnabled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.logs.Logger;
import io.opentelemetry.api.logs.LoggerProvider;
import io.opentelemetry.api.logs.Severity;
import io.opentelemetry.api.metrics.MeterProvider;
import io.opentelemetry.internal.testing.slf4j.SuppressLogger;
import io.opentelemetry.sdk.common.CompletableResultCode;
Expand All @@ -24,6 +25,7 @@
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessorBuilder;
import io.opentelemetry.sdk.logs.export.LogRecordExporter;
import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor;
import io.opentelemetry.sdk.logs.internal.LoggerConfig;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter;
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
Expand Down Expand Up @@ -314,4 +316,60 @@ void simpleExportError() {
.hasLongSumSatisfying(
s -> s.hasPointsSatisfying(p -> p.hasValue(1).hasAttributes())));
}

@Test
void filteringLogger() {
InMemoryMetricReader metricReader = InMemoryMetricReader.create();
MeterProvider meterProvider =
SdkMeterProvider.builder().registerMetricReader(metricReader).build();

InMemoryLogRecordExporter exporter = InMemoryLogRecordExporter.create();
LoggerProvider loggerProvider =
SdkLoggerProvider.builder()
.addLoggerConfiguratorCondition(
scope -> scope.getName().equals("warnAbove"),
LoggerConfig.builder().setMinimumSeverity(Severity.WARN).build())
.addLoggerConfiguratorCondition(
scope -> scope.getName().equals("disabled"), LoggerConfig.disabled())
.addLogRecordProcessor(
SimpleLogRecordProcessor.builder(exporter)
.setMeterProvider(() -> meterProvider)
.build())
.setMeterProvider(() -> meterProvider)
.build();

Logger defaultLogger = loggerProvider.get("test"); // 2 created, 2 emitted
defaultLogger.logRecordBuilder().setSeverity(Severity.WARN).emit();
defaultLogger.logRecordBuilder().setSeverity(Severity.INFO).emit();

Logger warnAbove = loggerProvider.get("warnAbove"); // 2 created, 1 emitted
warnAbove.logRecordBuilder().setSeverity(Severity.WARN).emit();
warnAbove.logRecordBuilder().setSeverity(Severity.INFO).emit();

Logger disabled = loggerProvider.get("disabled"); // 0 created, 0 emitted
disabled.logRecordBuilder().setSeverity(Severity.WARN).emit();
disabled.logRecordBuilder().setSeverity(Severity.INFO).emit();

assertThat(metricReader.collectAllMetrics())
.satisfiesExactlyInAnyOrder(
m ->
assertThat(m)
.hasName("otel.sdk.log.created")
.hasLongSumSatisfying(
s -> s.hasPointsSatisfying(p -> p.hasValue(4).hasAttributes())),
m ->
assertThat(m)
.hasName("otel.sdk.processor.log.processed")
.hasLongSumSatisfying(
s ->
s.hasPointsSatisfying(
p ->
p.hasValue(3)
.hasAttributes(
Attributes.of(
OTEL_COMPONENT_NAME,
"simple_log_processor/0",
OTEL_COMPONENT_TYPE,
"simple_log_processor")))));
}
}
Loading