Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added OpenTelemetryEnvironment #141

Merged
merged 6 commits into from
Jun 25, 2024
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
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