Skip to content
Merged
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
13 changes: 13 additions & 0 deletions docs/docs/operator/migration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,19 @@ constructed it directly are affected.
**Migration:** pass the concrete controller type when constructing `Reconciler<TEntity>` yourself, or
rely on the SDK's registration.

### Entity logging scopes are created through a factory

`ResourceWatcher<TEntity>`, `EntityQueueBackgroundService<TEntity>` and their leader-/scope-aware
derivations take an additional `IEntityLoggingScopeFactory<TEntity>` argument. The factory applies all
registered enrichers while creating the scope (see
[Logging](./observability/logging#enriching-scopes-with-custom-fields)).

**Migration:** resolve `IEntityLoggingScopeFactory<TEntity>` through dependency injection. Use its
`CreateFor(...)` methods to create an enriched scope, and pass the factory when directly constructing or
subclassing the watcher and queue types. `EntityLoggingScope.CreateFor(...)` remains available for standalone,
unenriched scopes. The default factory is registered automatically per entity type and is a no-op beyond the
built-in fields when no enrichers are configured.

### New in v13 (non-breaking)

- **Scoped leader election**: `LeaderElectionType.Scoped` partitions responsibility across
Expand Down
131 changes: 96 additions & 35 deletions docs/docs/operator/observability/logging.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,104 @@ sidebar_position: 1

# Logging

## Logging with `ILogger` and Scopes
## Logging with `ILogger` and scopes

This project uses Microsoft's [`ILogger`](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging) interface for logging. It provides a standardized and extensible way to capture events within the application.
KubeOps uses Microsoft's [`ILogger`](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging) abstraction.
Application code can use the same logging pipeline by injecting a category logger such as
`ILogger<V1DemoEntityController>`.

Using _scopes_ enables hierarchical organization of log messages and allows contextual information to be attached to each entry.

### ILogger Basics
### `ILogger` basics

The `ILogger` interface is part of [`Microsoft.Extensions.Logging`](https://www.nuget.org/packages/Microsoft.Extensions.Logging) and provides methods to log messages at various severity levels (e.g., `Information`, `Warning`, `Error`).
The default application builders (`WebApplication.CreateBuilder` and `Host.CreateApplicationBuilder`) register
logging automatically. For a manually assembled service collection, call `services.AddLogging()`.

Logging can be enabled using either `WebApplication.CreateBuilder`, `Host.CreateDefaultBuilder`, or the `AddLogging` extension method on the `IServiceCollection`.

You can log from your code by injecting `ILogger<MyEntityController>` (or a similar type) into your component.

### Using Scopes
### Entity scopes

Scopes define a logical boundary in which all log entries are automatically enriched with contextual metadata. This is especially useful for correlating logs related to a specific request or operation.

By default, two components start a new scope for each operation they perform:
Two operator components create entity scopes:

- The [ResourceWatcher](https://github.com/dotnet/dotnet-operator-sdk/blob/main/src/KubeOps.Operator/Watcher/ResourceWatcher%7BTEntity%7D.cs) opens a scope for every watch event received from the Kubernetes API server.
- The [EntityQueueBackgroundService](https://github.com/dotnet/dotnet-operator-sdk/blob/main/src/KubeOps.Operator/Queue/EntityQueueBackgroundService%7BTEntity%7D.cs) opens a scope for every queue entry it processes before invoking the reconciler.
- `ResourceWatcher<TEntity>` opens a scope around every watch event received from the Kubernetes API server,
including bookmark events.
- `EntityQueueBackgroundService<TEntity>` opens one scope around every dequeued entry. The scope is built from the
entity snapshot stored in the queue and covers lock handling, entity loading, reconciliation, retry scheduling,
and skips. A later retry is a new queue entry and therefore receives a new scope.

Both scopes carry the same fields:

- `EventType`: Type of the reconciliation operation (`Added`, `Modified`, `Deleted`)
- `ReconciliationTriggerSource`: Origin of the reconciliation request (`ApiServer` for watch events, `Operator` for internal retries and periodic requeues)
- `Kind`: Custom Resource Definition (CRD) kind
- `Namespace`: CRD namespace
- `Name`: CRD name
- `Uid`: CRD unique identifier
- `ResourceVersion`: CRD resource version
- `EventType`: watch/reconciliation operation (`Added`, `Modified`, `Deleted`, or `Bookmark` for watch scopes)
- `ReconciliationTriggerSource`: `ApiServer` for watch-derived work, or `Operator` for a delayed requeue requested
by a reconciler. Error retries and lock-conflict requeues preserve the trigger source of the original entry
- `Kind`: Kubernetes object kind
- `Namespace`: Kubernetes object namespace
- `Name`: Kubernetes object name
- `Uid`: Kubernetes object UID
- `ResourceVersion`: Kubernetes object resource version

For `Added` and `Modified` entries, the queue service still loads the current object from the API server before
dispatching it to the reconciler. The logging scope deliberately remains based on the queued event snapshot. Its
`ResourceVersion` and entity-derived custom fields therefore describe the event that caused the queue entry, not
necessarily the newer object instance passed to the reconciler. Controllers that need a scope based on the current
object can create one explicitly through `IEntityLoggingScopeFactory<TEntity>`.

### Enriching scopes with custom fields

You can create additional scopes in your code using `logger.BeginScope(state)`, where `state` can be either a string or a custom object.
The built-in fields can be extended with custom structured properties on both watch and reconcile scopes.
Implement `IEntityLoggingScopeEnricher<TEntity>` for the entity type and register it on the operator builder:

```csharp
public sealed class DemoEnricher : IEntityLoggingScopeEnricher<V1DemoEntity>
{
public void Enrich(
V1DemoEntity entity,
EntityLoggingPhase phase,
IDictionary<string, object> properties)
{
properties.TryAdd("Owner", entity.Spec.Owner);
properties.TryAdd("Phase", phase);
}
}
```

```csharp
builder.AddEntityLoggingScopeEnricher<V1DemoEntity, DemoEnricher>();
```

Inject `IEntityLoggingScopeFactory<TEntity>` when application code needs the same enriched scope:

```csharp
public sealed class DemoService(
ILogger<DemoService> logger,
IEntityLoggingScopeFactory<V1DemoEntity> scopeFactory)
{
public void LogWatchEvent(WatchEventType eventType, V1DemoEntity entity)
{
using var scope = logger.BeginScope(scopeFactory.CreateFor(eventType, entity));
logger.LogInformation("Processing an application-defined watch event.");
}
}
```

Semantics and constraints:

- **Lifetime**: enrichers are registered and resolved as singletons and must be thread-safe because watch and
reconciliation scopes can be created concurrently.
- **Synchronous hot path**: `Enrich` is called whenever an entity scope is built: once per watch event and once per
dequeued reconciliation entry. Enrichers must be synchronous, cheap, side-effect free, and only read from the
supplied `entity`; do not perform I/O or call the Kubernetes API.
- **Phase**: `phase` is either `Watch` or `Reconcile`, so an enricher can contribute
phase-specific data.
- **Order and add-only semantics**: enrichers run in registration order. Contributions are merged with
first-writer-wins semantics, so enrichers cannot change or remove the built-in identification fields or
properties contributed by an earlier enricher.
- **Error isolation**: if an enricher throws, the failure is logged and skipped; the remaining enrichers
still run and watching/reconciliation is never interrupted.

`EntityLoggingScope.CreateFor(...)` remains available for standalone scopes that only need the seven built-in
fields. It does not resolve dependency injection and therefore does not run registered enrichers; use
`IEntityLoggingScopeFactory<TEntity>` when enrichment is required.

To include scopes in the logging output, they must be explicitly enabled either via configuration or code:

Expand All @@ -60,29 +124,26 @@ To include scopes in the logging output, they must be explicitly enabled either
}
```

**Programmatic configuration:**
**Programmatic console configuration:**

```csharp
builder
.ConfigureLogging((hostBuilderContext, loggingBuilder) =>
{
loggingBuilder
.AddSimpleConsole(options => options.IncludeScopes = true);
});
builder.Logging.AddSimpleConsole(options => options.IncludeScopes = true);
```

:::tip
To enable scopes with OpenTelemetry, configure it as follows:
For the OpenTelemetry logging provider, enable scope capture when registering the provider:

```json
"OpenTelemetry": {
"IncludeScopes": true,
"ParseStateValues": true,
"IncludeFormattedMessage": true
}
```csharp
builder.Logging.AddOpenTelemetry(options =>
{
options.IncludeScopes = true;
options.ParseStateValues = true;
options.IncludeFormattedMessage = true;
});
```

The scope state must be an `IReadOnlyDictionary<string, object?>` to ensure correct serialization and inclusion in log entries.
This registers the provider; configure an exporter separately. Projects using `KubeOps.Aspire` can instead call
`AddKubeOpsServiceDefaults()`, which enables OpenTelemetry scope capture as part of the service defaults.
:::

:::tip
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

using k8s;
using k8s.Models;

using KubeOps.Abstractions.Logging;

using Microsoft.Extensions.DependencyInjection;

namespace KubeOps.Abstractions.Builder;

/// <summary>
/// Registration helpers for <see cref="IEntityLoggingScopeEnricher{TEntity}"/> implementations that contribute
/// additional structured properties to the logging scope surrounding every watch event and reconciliation.
/// </summary>
public static class EntityLoggingScopeEnricherBuilderExtensions
{
/// <summary>
/// Registers a logging scope enricher applied to scopes created for <typeparamref name="TEntity"/>.
/// </summary>
/// <typeparam name="TEntity">The entity type the enricher applies to.</typeparam>
/// <typeparam name="TEnricher">The enricher implementation.</typeparam>
/// <param name="builder">The operator builder.</param>
/// <returns>The same builder for chaining.</returns>
public static IOperatorBuilder AddEntityLoggingScopeEnricher<TEntity, TEnricher>(this IOperatorBuilder builder)
where TEntity : IKubernetesObject<V1ObjectMeta>
where TEnricher : class, IEntityLoggingScopeEnricher<TEntity>
{
builder.Services.AddSingleton<IEntityLoggingScopeEnricher<TEntity>, TEnricher>();
return builder;
}
}
18 changes: 18 additions & 0 deletions src/KubeOps.Abstractions/Logging/EntityLoggingPhase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

namespace KubeOps.Abstractions.Logging;

/// <summary>
/// Identifies the pipeline stage in which an entity logging scope is being created, allowing
/// enrichers to contribute different properties for a watch event than for a reconciliation.
/// </summary>
public enum EntityLoggingPhase
{
/// <summary>The scope is created while ingesting a watch event from the API server.</summary>
Watch,

/// <summary>The scope is created while reconciling a queued entity.</summary>
Reconcile,
}
27 changes: 27 additions & 0 deletions src/KubeOps.Abstractions/Logging/IEntityLoggingScopeEnricher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

using k8s;
using k8s.Models;

namespace KubeOps.Abstractions.Logging;

/// <summary>
/// Contributes additional structured properties to logging scopes created for <typeparamref name="TEntity"/>.
/// Enrichers can add properties, but cannot change or remove existing properties.
/// </summary>
/// <typeparam name="TEntity">The entity type this enricher applies to.</typeparam>
/// <remarks>
/// Register via <c>builder.AddEntityLoggingScopeEnricher&lt;TEntity, TEnricher&gt;()</c>. Enrichers are resolved
/// as singletons, may be invoked concurrently, and run synchronously on the hot path.
/// </remarks>
public interface IEntityLoggingScopeEnricher<TEntity>
where TEntity : IKubernetesObject<V1ObjectMeta>
{
/// <summary>Adds properties to the logging scope being built for <typeparamref name="TEntity"/>.</summary>
/// <param name="entity">The concrete entity the scope is being created for.</param>
/// <param name="phase">The pipeline stage in which the scope is being created.</param>
/// <param name="properties">The properties contributed by this enricher.</param>
void Enrich(TEntity entity, EntityLoggingPhase phase, IDictionary<string, object> properties);
}
5 changes: 5 additions & 0 deletions src/KubeOps.Operator/Builder/OperatorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,11 @@ private void AddOperatorBase()
Services.AddSingleton(typeof(IEntityLabelSelector<>), typeof(DefaultEntityLabelSelector<>));
Services.AddSingleton(typeof(IEntityFieldSelector<>), typeof(DefaultEntityFieldSelector<>));

// The factory is the public scope-creation boundary. It delegates enrichment to a no-op pipeline
// when no IEntityLoggingScopeEnricher<TEntity> is registered.
Services.TryAddSingleton(typeof(EntityLoggingScopeEnricherPipeline<>));
Services.TryAddSingleton(typeof(IEntityLoggingScopeFactory<>), typeof(EntityLoggingScopeFactory<>));

if (Settings.LeaderElectionType == LeaderElectionType.Single)
{
Services.AddLeaderElection();
Expand Down
Loading
Loading