Skip to content

Commit

Permalink
FEAT: add Kubernetes enricher (#28)
Browse files Browse the repository at this point in the history
* FEAT: add Serilog assembly version enricher

* FEAT: add Kubernetes enrichment of environment

* FEAT: add Kubernetes enrichment of environment

* PR-REVERT: version enricher

* Update Arcus.Observability.Telemetry.Serilog.csproj

* PR-FIX: use lower version of named arguments

* PR-SUG: make Kubernetes environment information configurable

* PR-SUG: simplify Kubernetes variable configuration

* PR-FIX: use correct log property names

* PR-SUG: revert configurability of the Kubernetes variables and properties

* Update telemetry-enrichment.md
  • Loading branch information
stijnmoreels authored Feb 25, 2020
1 parent 2b94a81 commit 3ee7f98
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 1 deletion.
23 changes: 22 additions & 1 deletion docs/features/telemetry-enrichment.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,25 @@ ILogger logger = new LoggerConfiguration()
.CreateLogger();

logger.Information("This event will be enriched with the runtime assembly product version");
```
```

## Kubernetes Enricher

The `Arcus.Observability.Telemetry.Serilog` library provides a [Kubernetes](https://kubernetes.io/) enricher that adds several machine information from the environment (variables).

**Example**
| Environment Variable | Log Property |
| ---------------------- | ------------ |
| `KUBERNETES_NODE_NAME` | NodeName |
| `KUBERNETES_POD_NAME` | PodName |
| `KUBERNETES_NAMESPACE` | Namespace |

**Usage**

```csharp
ILogger logger = new LoggerConfiguration()
.Enrich.With<KubernetesEnricher>()
.CreateLogger();

logger.Information("This event will be enriched with the Kubernetes environment information");
```
46 changes: 46 additions & 0 deletions src/Arcus.Observability.Telemetry.Serilog/KubernetesEnricher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using Serilog.Core;
using Serilog.Events;

namespace Arcus.Observability.Telemetry.Serilog
{
/// <summary>
/// Enrichment on log events that automatically adds Kubernetes information from the environment.
/// </summary>
public class KubernetesEnricher : ILogEventEnricher
{
private const string NodeNameVariable = "KUBERNETES_NODE_NAME",
PodNameVariable = "KUBERNETES_POD_NAME",
NamespaceVariable = "KUBERNETES_NAMESPACE";

private const string NodeNameProperty = "NodeName",
PodNameProperty = "PodName",
NamespaceProperty = "Namespace";

/// <summary>
/// Enrich the log event.
/// </summary>
/// <param name="logEvent">The log event to enrich.</param>
/// <param name="propertyFactory">Factory for creating new properties to add to the event.</param>
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
EnrichEnvironmentVariable(NodeNameVariable, NodeNameProperty, logEvent, propertyFactory);
EnrichEnvironmentVariable(PodNameVariable, PodNameProperty, logEvent, propertyFactory);
EnrichEnvironmentVariable(NamespaceVariable, NamespaceProperty, logEvent, propertyFactory);
}

private static void EnrichEnvironmentVariable(
string environmentVariableName,
string logPropertyName,
LogEvent logEvent,
ILogEventPropertyFactory propertyFactory)
{
string value = Environment.GetEnvironmentVariable(environmentVariableName, EnvironmentVariableTarget.Process);
if (!String.IsNullOrWhiteSpace(value))
{
LogEventProperty property = propertyFactory.CreateProperty(logPropertyName, value);
logEvent.AddPropertyIfAbsent(property);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using Arcus.Observability.Telemetry.Serilog;
using Serilog;
using Serilog.Events;
using Xunit;

namespace Arcus.Observability.Tests.Unit.Telemetry
{
[Trait("Category", "Unit")]
public class KubernetesEnricherTests
{
[Fact]
public void LogEvent_WithKubernetesEnricher_HasEnvironmentInformation()
{
// Arrange
const string kubernetesNodeName = "KUBERNETES_NODE_NAME",
kubernetesPodName = "KUBERNETES_POD_NAME",
kubernetesNamespace = "KUBERNETES_NAMESPACE";

string nodeName = $"node-{Guid.NewGuid()}";
string podName = $"pod-{Guid.NewGuid()}";
string @namespace = $"namespace-{Guid.NewGuid()}";

var spy = new InMemoryLogSink();
ILogger logger = new LoggerConfiguration()
.Enrich.With<KubernetesEnricher>()
.WriteTo.Sink(spy)
.CreateLogger();

using (TemporaryEnvironmentVariable.Create(kubernetesNodeName, nodeName))
using (TemporaryEnvironmentVariable.Create(kubernetesPodName, podName))
using (TemporaryEnvironmentVariable.Create(kubernetesNamespace, @namespace))
{
// Act
logger.Information("This log event should be enriched with Kubernetes information");
}

// Assert
LogEvent logEvent = Assert.Single(spy.CurrentLogEmits);
Assert.NotNull(logEvent);

ContainsLogProperty(logEvent, "NodeName", nodeName);
ContainsLogProperty(logEvent, "PodName", podName);
ContainsLogProperty(logEvent, "Namespace", @namespace);
}

[Fact]
public void LogEventWithNodeNameProperty_WithKubernetesEnricher_HasEnvironmentInformation()
{
// Arrange
string expectedNodeName = $"node-{Guid.NewGuid()}";
string ignoredNodeName = $"node-{Guid.NewGuid()}";

var spy = new InMemoryLogSink();
ILogger logger = new LoggerConfiguration()
.Enrich.With<KubernetesEnricher>()
.WriteTo.Sink(spy)
.CreateLogger();

using (TemporaryEnvironmentVariable.Create("KUBERNETES_NODE_NAME", ignoredNodeName))
{
// Act
logger.Information("This log even already has a Kubernetes NodeName {NodeName}", expectedNodeName);
}

// Assert
LogEvent logEvent = Assert.Single(spy.CurrentLogEmits);
Assert.NotNull(logEvent);

ContainsLogProperty(logEvent, "NodeName", expectedNodeName);
Assert.DoesNotContain(logEvent.Properties, prop => prop.Key == "PodName");
Assert.DoesNotContain(logEvent.Properties, prop => prop.Key == "Namespace");
}

[Fact]
public void LogEventWithPodNameProperty_WithKubernetesEnricher_HasEnvironmentInformation()
{
// Arrange
string expectedPodName = $"pod-{Guid.NewGuid()}";
string ignoredPodName = $"pod-{Guid.NewGuid()}";

var spy = new InMemoryLogSink();
ILogger logger = new LoggerConfiguration()
.Enrich.With<KubernetesEnricher>()
.WriteTo.Sink(spy)
.CreateLogger();

using (TemporaryEnvironmentVariable.Create("KUBERNETES_POD_NAME", ignoredPodName))
{
// Act
logger.Information("This log even already has a Kubernetes PodName {PodName}", expectedPodName);
}

// Assert
LogEvent logEvent = Assert.Single(spy.CurrentLogEmits);
Assert.NotNull(logEvent);

ContainsLogProperty(logEvent, "PodName", expectedPodName);
Assert.DoesNotContain(logEvent.Properties, prop => prop.Key == "NodeName");
Assert.DoesNotContain(logEvent.Properties, prop => prop.Key == "Namespace");
}

[Fact]
public void LogEventWithNamespaceProperty_WithKubernetesEnricher_HasEnvironmentInformation()
{
// Arrange
string expectedNamespace = $"namespace-{Guid.NewGuid()}";
string ignoredNamespace = $"namespace-{Guid.NewGuid()}";

var spy = new InMemoryLogSink();
ILogger logger = new LoggerConfiguration()
.Enrich.With<KubernetesEnricher>()
.WriteTo.Sink(spy)
.CreateLogger();

using (TemporaryEnvironmentVariable.Create("KUBERNETES_NAMESPACE", ignoredNamespace))
{
// Act
logger.Information("This log even already has a Kubernetes Namespace {Namespace}", expectedNamespace);
}

// Assert
LogEvent logEvent = Assert.Single(spy.CurrentLogEmits);
Assert.NotNull(logEvent);

ContainsLogProperty(logEvent, "Namespace", expectedNamespace);
Assert.DoesNotContain(logEvent.Properties, prop => prop.Key == "NodeName");
Assert.DoesNotContain(logEvent.Properties, prop => prop.Key == "PodName");
}

private static void ContainsLogProperty(LogEvent logEvent, string name, string expectedValue)
{
(string key, LogEventPropertyValue actual) =
Assert.Single(logEvent.Properties, prop => prop.Key == name);

string actualValue = actual.ToString().Trim('\"');
Assert.Equal(expectedValue, actualValue);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using GuardNet;

namespace Arcus.Observability.Tests.Unit.Telemetry
{
/// <summary>
/// Represents a temporary environment variable that gets removed when the model gets disposed.
/// </summary>
public class TemporaryEnvironmentVariable : IDisposable
{
private readonly string _name;

private TemporaryEnvironmentVariable(string name)
{
Guard.NotNullOrWhitespace(name, nameof(name));

_name = name;
}

public static TemporaryEnvironmentVariable Create(string name, string value)
{
Guard.NotNullOrWhitespace(name, nameof(name));
Guard.NotNullOrWhitespace(value, nameof(value));

Environment.SetEnvironmentVariable(name, value, EnvironmentVariableTarget.Process);
return new TemporaryEnvironmentVariable(name);
}


/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Environment.SetEnvironmentVariable(_name, value: null, target: EnvironmentVariableTarget.Process);
}
}
}

0 comments on commit 3ee7f98

Please sign in to comment.