Skip to content

Commit

Permalink
Merge pull request #141 from AlbertoMonteiro/dev
Browse files Browse the repository at this point in the history
Added OpenTelemetryEnvironment
  • Loading branch information
nblumhardt committed Jun 25, 2024
2 parents b0a18fa + f4a9d88 commit 39c0895
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Net.Http;
using Serilog.Configuration;
using Serilog.Sinks.OpenTelemetry;
using Serilog.Sinks.OpenTelemetry.Exporters;
using Serilog.Collections;
using Serilog.Configuration;
using Serilog.Core;
using Serilog.Events;
using Serilog.Sinks.OpenTelemetry;
using Serilog.Sinks.OpenTelemetry.Configuration;
using Serilog.Sinks.OpenTelemetry.Exporters;
using System.Net.Http;

namespace Serilog;

Expand All @@ -33,23 +34,30 @@ public static class OpenTelemetryLoggerConfigurationExtensions
#else
null;
#endif

/// <summary>
/// Send log events to an OTLP exporter.
/// </summary>
/// <param name="loggerSinkConfiguration">
/// The `WriteTo` configuration object.
/// </param>
/// <param name="configure">The configuration callback.</param>
/// <param name="ignoreEnvironment">If false the configuration will be overridden with values from <see href="https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/">OTLP Exporter Configuration environment variables</see>.</param>
public static LoggerConfiguration OpenTelemetry(
this LoggerSinkConfiguration loggerSinkConfiguration,
Action<BatchedOpenTelemetrySinkOptions> configure)
Action<BatchedOpenTelemetrySinkOptions> configure,
bool ignoreEnvironment = false)
{
if (configure == null) throw new ArgumentNullException(nameof(configure));

var options = new BatchedOpenTelemetrySinkOptions();
configure(options);

if (!ignoreEnvironment)
{
OpenTelemetryEnvironment.Configure(options, Environment.GetEnvironmentVariable);
}

var exporter = Exporter.Create(
endpoint: options.Endpoint,
protocol: options.Protocol,
Expand Down Expand Up @@ -117,7 +125,7 @@ public static LoggerConfiguration OpenTelemetry(
resourceAttributes?.AddTo(options.ResourceAttributes);
});
}

/// <summary>
/// Audit to an OTLP exporter, waiting for each event to be acknowledged, and propagating errors to the caller.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>This Serilog sink transforms Serilog events into OpenTelemetry
logs and sends them to an OTLP (gRPC or HTTP) endpoint.</Description>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
namespace Serilog.Sinks.OpenTelemetry.Configuration;

static class OpenTelemetryEnvironment
{
private const string PROTOCOL = "OTEL_EXPORTER_OTLP_PROTOCOL";
private const string ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT";
private const string HEADERS = "OTEL_EXPORTER_OTLP_HEADERS";
private const string RESOURCE_ATTRIBUTES = "OTEL_RESOURCE_ATTRIBUTES";

public static void Configure(BatchedOpenTelemetrySinkOptions options, Func<string, string?> getEnvironmentVariable)
{
options.Protocol = getEnvironmentVariable(PROTOCOL) switch
{
"http/protobuf" => OtlpProtocol.HttpProtobuf,
"grpc" => OtlpProtocol.Grpc,
_ => options.Protocol
};

if (getEnvironmentVariable(ENDPOINT) is { Length: > 1 } endpoint)
options.Endpoint = endpoint;

if (options.Protocol == OtlpProtocol.HttpProtobuf && !string.IsNullOrEmpty(options.Endpoint) && !options.Endpoint.EndsWith("/v1/logs"))
options.Endpoint = $"{options.Endpoint}/v1/logs";

FillHeadersIfPresent(getEnvironmentVariable(HEADERS), options.Headers);

FillHeadersResourceAttributesIfPresent(getEnvironmentVariable(RESOURCE_ATTRIBUTES), options.ResourceAttributes);
}

private static void FillHeadersIfPresent(string? config, IDictionary<string, string> headers)
{
foreach (var part in config?.Split(',') ?? [])
{
if (part.Split('=') is { Length: 2 } parts)
headers.Add(parts[0], parts[1]);
else
throw new InvalidOperationException($"Invalid header format: {part} in {HEADERS} environment variable.");
}
}

private static void FillHeadersResourceAttributesIfPresent(string? config, IDictionary<string, object> resourceAttributes)
{
foreach (var part in config?.Split(',') ?? [])
{
if (part.Split('=') is { Length: 2 } parts)
resourceAttributes.Add(parts[0], parts[1]);
else
throw new InvalidOperationException($"Invalid resourceAttributes format: {part} in {RESOURCE_ATTRIBUTES} environment variable.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using Serilog.Sinks.OpenTelemetry.Configuration;
using Xunit;

namespace Serilog.Sinks.OpenTelemetry.Tests;

public class OpenTelemetryEnvironmentTests
{
[Fact]
public void ConfigureFillOptionsWithEnvironmentVariablesValues()
{
BatchedOpenTelemetrySinkOptions options = new();
var endpoint = "http://localhost";
var protocol = OtlpProtocol.Grpc;
var headers = "header1=1,header2=2";
var resourceAttributes = "name1=1,name2=2";

OpenTelemetryEnvironment.Configure(options, GetEnvVar);

Assert.Equal(endpoint, options.Endpoint);
Assert.Equal(protocol, options.Protocol);
Assert.Collection(options.Headers,
e => Assert.Equal(("header1", "1"), (e.Key, e.Value)),
e => Assert.Equal(("header2", "2"), (e.Key, e.Value)));
Assert.Collection(options.ResourceAttributes,
e => Assert.Equal(("name1", "1"), (e.Key, e.Value)),
e => Assert.Equal(("name2", "2"), (e.Key, e.Value)));

string? GetEnvVar(string name)
=> name switch
{
"OTEL_EXPORTER_OTLP_ENDPOINT" => endpoint,
"OTEL_EXPORTER_OTLP_HEADERS" => headers,
"OTEL_RESOURCE_ATTRIBUTES" => resourceAttributes,
"OTEL_EXPORTER_OTLP_PROTOCOL" => "grpc",
_ => null
};
}

[Fact]
public void ConfigureAppendPathToEndpointIfProtocolIsHttpProtobufAndEndpointDoesntEndsWithProperValue()
{
BatchedOpenTelemetrySinkOptions options = new();
var endpoint = "http://localhost";
var protocol = OtlpProtocol.HttpProtobuf;

OpenTelemetryEnvironment.Configure(options, GetEnvVar);

Assert.Equal($"{endpoint}/v1/logs", options.Endpoint);
Assert.Equal(protocol, options.Protocol);

string? GetEnvVar(string name)
=> name switch
{
"OTEL_EXPORTER_OTLP_ENDPOINT" => endpoint,
"OTEL_EXPORTER_OTLP_PROTOCOL" => "http/protobuf",
_ => null
};
}

[Fact]
public void ConfigureThrowsIfHeaderEnvIsInvalidFormat()
{
BatchedOpenTelemetrySinkOptions options = new();
var headers = "header1";

var exception = Assert.Throws<InvalidOperationException>(() => OpenTelemetryEnvironment.Configure(options, GetEnvVar));

Assert.Equal("Invalid header format: header1 in OTEL_EXPORTER_OTLP_HEADERS environment variable.", exception.Message);

string? GetEnvVar(string name)
=> name switch
{
"OTEL_EXPORTER_OTLP_HEADERS" => headers,
_ => null
};
}

[Fact]
public void ConfigureThrowsIfResourceAttributesEnvIsInvalidFormat()
{
BatchedOpenTelemetrySinkOptions options = new();
var resourceAttributes = "resource1";

var exception = Assert.Throws<InvalidOperationException>(() => OpenTelemetryEnvironment.Configure(options, GetEnvVar));

Assert.Equal("Invalid resourceAttributes format: resource1 in OTEL_RESOURCE_ATTRIBUTES environment variable.", exception.Message);

string? GetEnvVar(string name)
=> name switch
{
"OTEL_RESOURCE_ATTRIBUTES" => resourceAttributes,
_ => null
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
public static class OpenTelemetryLoggerConfigurationExtensions
{
public static Serilog.LoggerConfiguration OpenTelemetry(this Serilog.Configuration.LoggerAuditSinkConfiguration loggerAuditSinkConfiguration, System.Action<Serilog.Sinks.OpenTelemetry.OpenTelemetrySinkOptions> configure) { }
public static Serilog.LoggerConfiguration OpenTelemetry(this Serilog.Configuration.LoggerSinkConfiguration loggerSinkConfiguration, System.Action<Serilog.Sinks.OpenTelemetry.BatchedOpenTelemetrySinkOptions> configure) { }
public static Serilog.LoggerConfiguration OpenTelemetry(this Serilog.Configuration.LoggerSinkConfiguration loggerSinkConfiguration, System.Action<Serilog.Sinks.OpenTelemetry.BatchedOpenTelemetrySinkOptions> configure, bool ignoreEnvironment = false) { }
public static Serilog.LoggerConfiguration OpenTelemetry(this Serilog.Configuration.LoggerAuditSinkConfiguration loggerAuditSinkConfiguration, string endpoint = "http://localhost:4317", Serilog.Sinks.OpenTelemetry.OtlpProtocol protocol = 0, System.Collections.Generic.IDictionary<string, string>? headers = null, System.Collections.Generic.IDictionary<string, object>? resourceAttributes = null, Serilog.Sinks.OpenTelemetry.IncludedData? includedData = default) { }
public static Serilog.LoggerConfiguration OpenTelemetry(this Serilog.Configuration.LoggerSinkConfiguration loggerSinkConfiguration, string endpoint = "http://localhost:4317", Serilog.Sinks.OpenTelemetry.OtlpProtocol protocol = 0, System.Collections.Generic.IDictionary<string, string>? headers = null, System.Collections.Generic.IDictionary<string, object>? resourceAttributes = null, Serilog.Sinks.OpenTelemetry.IncludedData? includedData = default, Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0, Serilog.Core.LoggingLevelSwitch? levelSwitch = null) { }
}
Expand Down

0 comments on commit 39c0895

Please sign in to comment.