Skip to content
Open
3 changes: 3 additions & 0 deletions src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
OpenTelemetry.Resources.Resource.Resource(System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, object!>>! attributes, string? schemaUrl) -> void
OpenTelemetry.Resources.Resource.SchemaUrl.get -> string?
static OpenTelemetry.Resources.ResourceBuilderExtensions.AddAttributes(this OpenTelemetry.Resources.ResourceBuilder! resourceBuilder, System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, object!>>! attributes, string? schemaUrl) -> OpenTelemetry.Resources.ResourceBuilder!
override OpenTelemetry.SimpleLogRecordExportProcessor.OnEnd(OpenTelemetry.Logs.LogRecord! data) -> void
override OpenTelemetry.BatchLogRecordExportProcessor.OnShutdown(int timeoutMilliseconds) -> bool
override OpenTelemetry.SimpleLogRecordExportProcessor.OnShutdown(int timeoutMilliseconds) -> bool
Comment thread
cijothomas marked this conversation as resolved.
24 changes: 24 additions & 0 deletions src/OpenTelemetry/Internal/SdkSelfObservability.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System.Diagnostics.Metrics;

namespace OpenTelemetry;

/// <summary>
/// Shared infrastructure for SDK self-observability metrics.
/// </summary>
internal static class SdkSelfObservability
{
#pragma warning disable SA1202 // internal before private - initialization order requires Meter first
Comment thread
cijothomas marked this conversation as resolved.
Outdated
private static readonly Meter Meter = new(new MeterOptions("otel.sdk.experimental")
{
Version = typeof(SdkSelfObservability).Assembly.GetName().Version?.ToString(),
});
Comment thread
cijothomas marked this conversation as resolved.
Outdated

internal static readonly Counter<long> LogProcessedCounter = Meter.CreateCounter<long>(
"otel.sdk.processor.log.processed",
"{log_record}",
"The number of log records for which the processing has finished, either successful or failed.");
#pragma warning restore SA1202
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ namespace OpenTelemetry;
/// </summary>
public class BatchLogRecordExportProcessor : BatchExportProcessor<LogRecord>
{
private static int instanceCounter = -1;

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.

While unlikely there'd ever be that many created in the lifetime of a process, maybe use long to give more headroom against overflow?


private readonly KeyValuePair<string, object?>[] successTags;
private readonly KeyValuePair<string, object?>[] queueFullTags;
private readonly KeyValuePair<string, object?>[] alreadyShutdownTags;
private volatile bool isShutdown;

/// <summary>
/// Initializes a new instance of the <see cref="BatchLogRecordExportProcessor"/> class.
/// </summary>
Expand All @@ -32,11 +39,32 @@ public BatchLogRecordExportProcessor(
exporterTimeoutMilliseconds,
maxExportBatchSize)
{
var index = Interlocked.Increment(ref instanceCounter);
var componentName = "batching_log_processor/" + index.ToString(System.Globalization.CultureInfo.InvariantCulture);
this.successTags =
[
new("otel.component.type", "batching_log_processor"),
new("otel.component.name", componentName),
];
this.queueFullTags =
[
new("otel.component.type", "batching_log_processor"),
new("otel.component.name", componentName),
new("error.type", "queue_full"),
];
this.alreadyShutdownTags =
[
new("otel.component.type", "batching_log_processor"),
new("otel.component.name", componentName),
new("error.type", "already_shutdown"),
];
Comment thread
cijothomas marked this conversation as resolved.
Outdated
}

/// <inheritdoc/>
public override void OnEnd(LogRecord data)
{
bool enqueued;

// Note: Intentionally not using Guard.ThrowIfNull to save prod cycles
#pragma warning disable CA1062 // Validate arguments of public methods
switch (data.Source)
Expand All @@ -45,7 +73,8 @@ public override void OnEnd(LogRecord data)
case LogRecord.LogRecordSource.FromSharedPool:
data.Buffer();
data.AddReference();
if (!this.TryExport(data))
enqueued = this.TryExport(data);
if (!enqueued)
{
LogRecordSharedPool.Current.Return(data);
}
Expand All @@ -54,16 +83,40 @@ public override void OnEnd(LogRecord data)

case LogRecord.LogRecordSource.CreatedManually:
data.Buffer();
this.TryExport(data);
enqueued = this.TryExport(data);
break;

case LogRecord.LogRecordSource.FromThreadStaticPool:
default:
Debug.Assert(data.Source == LogRecord.LogRecordSource.FromThreadStaticPool, "LogRecord source was something unexpected");

// Note: If we are using ThreadStatic pool we make a copy of the record.
this.TryExport(data.Copy());
enqueued = this.TryExport(data.Copy());
break;
}

// TODO: Consider switching to an ObservableCounter that piggybacks on

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.

Issue for tracking?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Will file a tracking issue for this.

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.

Cool - just add a link to the TODO once created.

// CircularBuffer.AddedCount and DroppedCount to eliminate per-item
// Counter.Add() overhead. This would require a registry pattern for
// multiple instances but avoids any hot-path cost when a listener is active.
if (this.isShutdown)
{
SdkSelfObservability.LogProcessedCounter.Add(1, this.alreadyShutdownTags);
}
else if (!enqueued)
{
SdkSelfObservability.LogProcessedCounter.Add(1, this.queueFullTags);
}
else
{
SdkSelfObservability.LogProcessedCounter.Add(1, this.successTags);
}
Comment thread
cijothomas marked this conversation as resolved.
Outdated
}

/// <inheritdoc/>
protected override bool OnShutdown(int timeoutMilliseconds)
{
this.isShutdown = true;
return base.OnShutdown(timeoutMilliseconds);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,46 @@ namespace OpenTelemetry;
/// </summary>
public class SimpleLogRecordExportProcessor : SimpleExportProcessor<LogRecord>
{
private static int instanceCounter = -1;

private readonly KeyValuePair<string, object?>[] successTags;
private readonly KeyValuePair<string, object?>[] alreadyShutdownTags;
private volatile bool isShutdown;

/// <summary>
/// Initializes a new instance of the <see cref="SimpleLogRecordExportProcessor"/> class.
/// </summary>
/// <param name="exporter">Log record exporter.</param>
public SimpleLogRecordExportProcessor(BaseExporter<LogRecord> exporter)
: base(exporter)
{
var index = Interlocked.Increment(ref instanceCounter);
var componentName = "simple_log_processor/" + index.ToString(System.Globalization.CultureInfo.InvariantCulture);
this.successTags =
[
new("otel.component.type", "simple_log_processor"),
new("otel.component.name", componentName),
];
this.alreadyShutdownTags =
[
new("otel.component.type", "simple_log_processor"),
new("otel.component.name", componentName),
new("error.type", "already_shutdown"),
];
Comment thread
cijothomas marked this conversation as resolved.
Outdated
}

/// <inheritdoc/>
public override void OnEnd(LogRecord data)
{
base.OnEnd(data);
SdkSelfObservability.LogProcessedCounter.Add(
1, this.isShutdown ? this.alreadyShutdownTags : this.successTags);
}

/// <inheritdoc/>
protected override bool OnShutdown(int timeoutMilliseconds)
{
this.isShutdown = true;
return base.OnShutdown(timeoutMilliseconds);
}
}
49 changes: 49 additions & 0 deletions src/OpenTelemetry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

* [Installation](#installation)
* [Introduction](#introduction)
* [Self-Observability (Experimental)](#self-observability-experimental)
* [Troubleshooting](#troubleshooting)
* [Self-diagnostics](#self-diagnostics)
* [Configuration Parameters](#configuration-parameters)
Expand Down Expand Up @@ -45,6 +46,54 @@ started](../../README.md#getting-started). For additional details about
initialization patterns see: [Initialize the
SDK](../../docs/README.md#initialize-the-sdk).

## Self-Observability (Experimental)

The SDK can emit metrics about its own internal operations, enabling operators to
monitor the health of the telemetry pipeline itself (e.g., detecting dropped log
records due to queue overflow).

> [!NOTE]
> Self-observability metrics are **experimental** and may change in future
> releases. They are emitted under the meter name `otel.sdk.experimental`.

### Opt-in

Self-observability metrics are only collected when a `MeterProvider` explicitly
subscribes to the `otel.sdk.experimental` meter. Without this, the overhead is
effectively zero (no-op).
Comment thread
cijothomas marked this conversation as resolved.
Outdated

```csharp
var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter("otel.sdk.experimental")
.AddOtlpExporter() // or any exporter
.Build();
```

### Available Metrics

These metrics follow the [OpenTelemetry SDK Self-Observability Semantic
Conventions](https://opentelemetry.io/docs/specs/semconv/otel/sdk-metrics/).

| Metric Name | Instrument | Unit | Description |
| --- | --- | --- | --- |
| `otel.sdk.processor.log.processed` | Counter | `{log_record}` | Number of log records processed by the SDK, tagged with outcome. |

### Attributes

| Attribute | Description | Example |
| --- | --- | --- |
| `otel.component.type` | The processor type. | `batching_log_processor`, `simple_log_processor` |
| `otel.component.name` | Unique instance identifier. | `batching_log_processor/0` |
| `error.type` | Present only on failure. | `queue_full`, `already_shutdown` |

When `error.type` is absent, the log record was successfully accepted by the
processor. When present:

* `queue_full` - The batch processor's internal queue was full; the log record
was dropped.
* `already_shutdown` - The processor had already been shut down; the log record
was lost.

## Troubleshooting

All the components shipped from this repo uses
Expand Down
Loading