509 lines
20 KiB
C#
509 lines
20 KiB
C#
// <copyright file="OtlpLogExporterTests.cs" company="OpenTelemetry Authors">
|
|
// Copyright The OpenTelemetry Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
// </copyright>
|
|
|
|
using System.Diagnostics;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
|
|
using OpenTelemetry.Internal;
|
|
using OpenTelemetry.Logs;
|
|
using OpenTelemetry.Tests;
|
|
using OpenTelemetry.Trace;
|
|
using Xunit;
|
|
using OtlpCommon = OpenTelemetry.Proto.Common.V1;
|
|
using OtlpLogs = OpenTelemetry.Proto.Logs.V1;
|
|
|
|
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests
|
|
{
|
|
public class OtlpLogExporterTests : Http2UnencryptedSupportTests
|
|
{
|
|
private static readonly SdkLimitOptions DefaultSdkLimitOptions = new();
|
|
|
|
[Fact]
|
|
public void AddOtlpLogExporterOptionsTest()
|
|
{
|
|
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
|
|
var loggerOptions = new OpenTelemetryLoggerOptions();
|
|
Assert.False(loggerOptions.ParseStateValues);
|
|
loggerOptions.AddOtlpExporter();
|
|
Assert.True(loggerOptions.ParseStateValues);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddOtlpLogExporterSetsParseStateValueToTrue()
|
|
{
|
|
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
|
|
var logRecords = new List<LogRecord>();
|
|
using var loggerFactory = LoggerFactory.Create(builder =>
|
|
{
|
|
builder.AddOpenTelemetry(options =>
|
|
{
|
|
options.AddInMemoryExporter(logRecords);
|
|
options.AddOtlpExporter();
|
|
});
|
|
});
|
|
|
|
var logger = loggerFactory.CreateLogger("OtlpLogExporterTests");
|
|
logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99);
|
|
Assert.Single(logRecords);
|
|
var logRecord = logRecords[0];
|
|
Assert.Null(logRecord.State);
|
|
Assert.NotNull(logRecord.StateValues);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddOtlpLogExporterParseStateValueCanBeTurnedOff()
|
|
{
|
|
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
|
|
var logRecords = new List<LogRecord>();
|
|
using var loggerFactory = LoggerFactory.Create(builder =>
|
|
{
|
|
builder.AddOpenTelemetry(options =>
|
|
{
|
|
options.AddInMemoryExporter(logRecords);
|
|
options.AddOtlpExporter();
|
|
options.ParseStateValues = false;
|
|
});
|
|
});
|
|
|
|
var logger = loggerFactory.CreateLogger("OtlpLogExporterTests");
|
|
logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99);
|
|
Assert.Single(logRecords);
|
|
var logRecord = logRecords[0];
|
|
Assert.NotNull(logRecord.State);
|
|
Assert.Null(logRecord.StateValues);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddOtlpLogExporterParseStateValueCanBeTurnedOffHosting()
|
|
{
|
|
var logRecords = new List<LogRecord>();
|
|
|
|
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
|
|
var hostBuilder = new HostBuilder();
|
|
hostBuilder.ConfigureLogging(logging => logging.AddOpenTelemetry(options =>
|
|
{
|
|
options.AddInMemoryExporter(logRecords);
|
|
options.AddOtlpExporter();
|
|
}));
|
|
|
|
hostBuilder.ConfigureServices(services =>
|
|
services.Configure<OpenTelemetryLoggerOptions>(options => options.ParseStateValues = false));
|
|
|
|
var host = hostBuilder.Build();
|
|
var loggerFactory = host.Services.GetService<ILoggerFactory>();
|
|
var logger = loggerFactory.CreateLogger("OtlpLogExporterTests");
|
|
logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99);
|
|
Assert.Single(logRecords);
|
|
var logRecord = logRecords[0];
|
|
Assert.NotNull(logRecord.State);
|
|
Assert.Null(logRecord.StateValues);
|
|
}
|
|
|
|
[Fact]
|
|
public void OtlpLogRecordTestWhenStateValuesArePopulated()
|
|
{
|
|
var logRecords = new List<LogRecord>();
|
|
using var loggerFactory = LoggerFactory.Create(builder =>
|
|
{
|
|
builder.AddOpenTelemetry(options =>
|
|
{
|
|
options.IncludeFormattedMessage = true;
|
|
options.ParseStateValues = true;
|
|
options.AddInMemoryExporter(logRecords);
|
|
});
|
|
});
|
|
|
|
var logger = loggerFactory.CreateLogger("OtlpLogExporterTests");
|
|
logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99);
|
|
|
|
Assert.Single(logRecords);
|
|
|
|
var logRecord = logRecords[0];
|
|
var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions);
|
|
|
|
Assert.NotNull(otlpLogRecord);
|
|
Assert.Equal("Hello from tomato 2.99.", otlpLogRecord.Body.StringValue);
|
|
Assert.Equal(4, otlpLogRecord.Attributes.Count);
|
|
|
|
var attribute = otlpLogRecord.Attributes[0];
|
|
Assert.Equal("dotnet.ilogger.category", attribute.Key);
|
|
Assert.Equal("OtlpLogExporterTests", attribute.Value.StringValue);
|
|
|
|
attribute = otlpLogRecord.Attributes[1];
|
|
Assert.Equal("name", attribute.Key);
|
|
Assert.Equal("tomato", attribute.Value.StringValue);
|
|
|
|
attribute = otlpLogRecord.Attributes[2];
|
|
Assert.Equal("price", attribute.Key);
|
|
Assert.Equal(2.99, attribute.Value.DoubleValue);
|
|
|
|
attribute = otlpLogRecord.Attributes[3];
|
|
Assert.Equal("{OriginalFormat}", attribute.Key);
|
|
Assert.Equal("Hello from {name} {price}.", attribute.Value.StringValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void CheckToOtlpLogRecordLoggerCategory()
|
|
{
|
|
var logRecords = new List<LogRecord>();
|
|
using var loggerFactory = LoggerFactory.Create(builder =>
|
|
{
|
|
builder.AddOpenTelemetry(options =>
|
|
{
|
|
options.AddInMemoryExporter(logRecords);
|
|
});
|
|
});
|
|
|
|
var logger1 = loggerFactory.CreateLogger("CategoryA");
|
|
logger1.LogInformation("Hello");
|
|
Assert.Single(logRecords);
|
|
|
|
var logRecord = logRecords[0];
|
|
var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions);
|
|
Assert.NotNull(otlpLogRecord);
|
|
Assert.Single(otlpLogRecord.Attributes);
|
|
|
|
var attribute = otlpLogRecord.Attributes[0];
|
|
Assert.Equal("dotnet.ilogger.category", attribute.Key);
|
|
Assert.Equal("CategoryA", attribute.Value.StringValue);
|
|
|
|
logRecords.Clear();
|
|
var logger2 = loggerFactory.CreateLogger(string.Empty);
|
|
logger2.LogInformation("Hello");
|
|
Assert.Single(logRecords);
|
|
|
|
logRecord = logRecords[0];
|
|
otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions);
|
|
Assert.NotNull(otlpLogRecord);
|
|
Assert.Empty(otlpLogRecord.Attributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void CheckToOtlpLogRecordEventId()
|
|
{
|
|
var logRecords = new List<LogRecord>();
|
|
using var loggerFactory = LoggerFactory.Create(builder =>
|
|
{
|
|
builder.AddOpenTelemetry(options =>
|
|
{
|
|
options.IncludeFormattedMessage = true;
|
|
options.ParseStateValues = true;
|
|
options.AddInMemoryExporter(logRecords);
|
|
});
|
|
});
|
|
|
|
var logger = loggerFactory.CreateLogger("OtlpLogExporterTests");
|
|
logger.LogInformation(new EventId(10, null), "Hello from {name} {price}.", "tomato", 2.99);
|
|
Assert.Single(logRecords);
|
|
|
|
var logRecord = logRecords[0];
|
|
var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions);
|
|
|
|
Assert.NotNull(otlpLogRecord);
|
|
Assert.Equal("Hello from tomato 2.99.", otlpLogRecord.Body.StringValue);
|
|
|
|
var otlpLogRecordAttributes = otlpLogRecord.Attributes.ToString();
|
|
|
|
// Event
|
|
Assert.Contains("Id", otlpLogRecordAttributes);
|
|
Assert.Contains("10", otlpLogRecordAttributes);
|
|
|
|
logRecords.Clear();
|
|
|
|
logger.LogInformation(new EventId(10, "MyEvent10"), "Hello from {name} {price}.", "tomato", 2.99);
|
|
Assert.Single(logRecords);
|
|
|
|
logRecord = logRecords[0];
|
|
otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions);
|
|
Assert.NotNull(otlpLogRecord);
|
|
Assert.Equal("Hello from tomato 2.99.", otlpLogRecord.Body.StringValue);
|
|
|
|
otlpLogRecordAttributes = otlpLogRecord.Attributes.ToString();
|
|
|
|
// Event
|
|
Assert.Contains("Id", otlpLogRecordAttributes);
|
|
Assert.Contains("10", otlpLogRecordAttributes);
|
|
Assert.Contains("Name", otlpLogRecordAttributes);
|
|
Assert.Contains("MyEvent10", otlpLogRecordAttributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void CheckToOtlpLogRecordTraceIdSpanIdFlagWithNoActivity()
|
|
{
|
|
var logRecords = new List<LogRecord>();
|
|
using var loggerFactory = LoggerFactory.Create(builder =>
|
|
{
|
|
builder.AddOpenTelemetry(options =>
|
|
{
|
|
options.AddInMemoryExporter(logRecords);
|
|
});
|
|
});
|
|
|
|
var logger = loggerFactory.CreateLogger("OtlpLogExporterTests");
|
|
logger.LogInformation("Log when there is no activity.");
|
|
var logRecord = logRecords[0];
|
|
var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions);
|
|
|
|
Assert.Null(Activity.Current);
|
|
Assert.True(otlpLogRecord.TraceId.IsEmpty);
|
|
Assert.True(otlpLogRecord.SpanId.IsEmpty);
|
|
Assert.True(otlpLogRecord.Flags == 0);
|
|
}
|
|
|
|
[Fact]
|
|
public void CheckToOtlpLogRecordSpanIdTraceIdAndFlag()
|
|
{
|
|
var logRecords = new List<LogRecord>();
|
|
using var loggerFactory = LoggerFactory.Create(builder =>
|
|
{
|
|
builder.AddOpenTelemetry(options =>
|
|
{
|
|
options.AddInMemoryExporter(logRecords);
|
|
});
|
|
});
|
|
|
|
var logger = loggerFactory.CreateLogger("OtlpLogExporterTests");
|
|
|
|
ActivityTraceId expectedTraceId = default;
|
|
ActivitySpanId expectedSpanId = default;
|
|
using (var activity = new Activity(Utils.GetCurrentMethodName()).Start())
|
|
{
|
|
logger.LogInformation("Log within an activity.");
|
|
|
|
expectedTraceId = activity.TraceId;
|
|
expectedSpanId = activity.SpanId;
|
|
}
|
|
|
|
var logRecord = logRecords[0];
|
|
var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions);
|
|
|
|
Assert.Equal(expectedTraceId.ToString(), ActivityTraceId.CreateFromBytes(otlpLogRecord.TraceId.ToByteArray()).ToString());
|
|
Assert.Equal(expectedSpanId.ToString(), ActivitySpanId.CreateFromBytes(otlpLogRecord.SpanId.ToByteArray()).ToString());
|
|
Assert.Equal((uint)logRecord.TraceFlags, otlpLogRecord.Flags);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(LogLevel.Trace)]
|
|
[InlineData(LogLevel.Debug)]
|
|
[InlineData(LogLevel.Information)]
|
|
[InlineData(LogLevel.Warning)]
|
|
[InlineData(LogLevel.Error)]
|
|
[InlineData(LogLevel.Critical)]
|
|
public void CheckToOtlpLogRecordSeverityLevelAndText(LogLevel logLevel)
|
|
{
|
|
var logRecords = new List<LogRecord>();
|
|
using var loggerFactory = LoggerFactory.Create(builder =>
|
|
{
|
|
builder.AddOpenTelemetry(options =>
|
|
{
|
|
options.AddInMemoryExporter(logRecords);
|
|
options.IncludeFormattedMessage = true;
|
|
})
|
|
.AddFilter("CheckToOtlpLogRecordSeverityLevelAndText", LogLevel.Trace);
|
|
});
|
|
|
|
var logger = loggerFactory.CreateLogger("CheckToOtlpLogRecordSeverityLevelAndText");
|
|
logger.Log(logLevel, "Hello from {name} {price}.", "tomato", 2.99);
|
|
Assert.Single(logRecords);
|
|
|
|
var logRecord = logRecords[0];
|
|
var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions);
|
|
|
|
Assert.NotNull(otlpLogRecord);
|
|
Assert.Equal(logRecord.LogLevel.ToString(), otlpLogRecord.SeverityText);
|
|
switch (logLevel)
|
|
{
|
|
case LogLevel.Trace:
|
|
Assert.Equal(OtlpLogs.SeverityNumber.Trace, otlpLogRecord.SeverityNumber);
|
|
break;
|
|
case LogLevel.Debug:
|
|
Assert.Equal(OtlpLogs.SeverityNumber.Debug, otlpLogRecord.SeverityNumber);
|
|
break;
|
|
case LogLevel.Information:
|
|
Assert.Equal(OtlpLogs.SeverityNumber.Info, otlpLogRecord.SeverityNumber);
|
|
break;
|
|
case LogLevel.Warning:
|
|
Assert.Equal(OtlpLogs.SeverityNumber.Warn, otlpLogRecord.SeverityNumber);
|
|
break;
|
|
case LogLevel.Error:
|
|
Assert.Equal(OtlpLogs.SeverityNumber.Error, otlpLogRecord.SeverityNumber);
|
|
break;
|
|
case LogLevel.Critical:
|
|
Assert.Equal(OtlpLogs.SeverityNumber.Fatal, otlpLogRecord.SeverityNumber);
|
|
break;
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public void CheckToOtlpLogRecordBodyIsPopulated(bool includeFormattedMessage)
|
|
{
|
|
var logRecords = new List<LogRecord>();
|
|
using var loggerFactory = LoggerFactory.Create(builder =>
|
|
{
|
|
builder.AddOpenTelemetry(options =>
|
|
{
|
|
options.AddInMemoryExporter(logRecords);
|
|
options.IncludeFormattedMessage = includeFormattedMessage;
|
|
options.ParseStateValues = true;
|
|
});
|
|
});
|
|
|
|
var logger = loggerFactory.CreateLogger("OtlpLogExporterTests");
|
|
|
|
// Scenario 1 - Using ExtensionMethods on ILogger.Log
|
|
logger.LogInformation("OpenTelemetry {Greeting} {Subject}!", "Hello", "World");
|
|
Assert.Single(logRecords);
|
|
|
|
var logRecord = logRecords[0];
|
|
var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions);
|
|
|
|
Assert.NotNull(otlpLogRecord);
|
|
if (includeFormattedMessage)
|
|
{
|
|
Assert.Equal(logRecord.FormattedMessage, otlpLogRecord.Body.StringValue);
|
|
}
|
|
else
|
|
{
|
|
Assert.Equal("OpenTelemetry {Greeting} {Subject}!", otlpLogRecord.Body.StringValue);
|
|
}
|
|
|
|
logRecords.Clear();
|
|
|
|
// Scenario 2 - Using the raw ILogger.Log Method
|
|
logger.Log(LogLevel.Information, default, "state", exception: null, (st, ex) => "Formatted Message");
|
|
Assert.Single(logRecords);
|
|
|
|
logRecord = logRecords[0];
|
|
otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions);
|
|
|
|
Assert.NotNull(otlpLogRecord);
|
|
if (includeFormattedMessage)
|
|
{
|
|
Assert.Equal(logRecord.FormattedMessage, otlpLogRecord.Body.StringValue);
|
|
}
|
|
else
|
|
{
|
|
Assert.Null(otlpLogRecord.Body);
|
|
}
|
|
|
|
logRecords.Clear();
|
|
|
|
// Scenario 3 - Using the raw ILogger.Log Method, but with null
|
|
// formatter.
|
|
logger.Log(LogLevel.Information, default, "state", exception: null, formatter: null);
|
|
Assert.Single(logRecords);
|
|
|
|
logRecord = logRecords[0];
|
|
otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions);
|
|
|
|
Assert.NotNull(otlpLogRecord);
|
|
|
|
// There is no formatter, so no way to populate Body.
|
|
// Exporter won't even attempt to do ToString() on State.
|
|
if (includeFormattedMessage)
|
|
{
|
|
Assert.Null(otlpLogRecord.Body);
|
|
}
|
|
else
|
|
{
|
|
Assert.Null(otlpLogRecord.Body);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void CheckToOtlpLogRecordExceptionAttributes()
|
|
{
|
|
var logRecords = new List<LogRecord>();
|
|
using var loggerFactory = LoggerFactory.Create(builder =>
|
|
{
|
|
builder.AddOpenTelemetry(options =>
|
|
{
|
|
options.AddInMemoryExporter(logRecords);
|
|
});
|
|
});
|
|
|
|
var logger = loggerFactory.CreateLogger("OtlpLogExporterTests");
|
|
logger.LogInformation(new Exception("Exception Message"), "Exception Occurred");
|
|
|
|
var logRecord = logRecords[0];
|
|
var loggedException = logRecord.Exception;
|
|
var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions);
|
|
|
|
Assert.NotNull(otlpLogRecord);
|
|
var otlpLogRecordAttributes = otlpLogRecord.Attributes.ToString();
|
|
Assert.Contains(SemanticConventions.AttributeExceptionType, otlpLogRecordAttributes);
|
|
Assert.Contains(logRecord.Exception.GetType().Name, otlpLogRecordAttributes);
|
|
|
|
Assert.Contains(SemanticConventions.AttributeExceptionMessage, otlpLogRecordAttributes);
|
|
Assert.Contains(logRecord.Exception.Message, otlpLogRecordAttributes);
|
|
|
|
Assert.Contains(SemanticConventions.AttributeExceptionStacktrace, otlpLogRecordAttributes);
|
|
Assert.Contains(logRecord.Exception.ToInvariantString(), otlpLogRecordAttributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void CheckToOtlpLogRecordRespectsAttributeLimits()
|
|
{
|
|
var sdkLimitOptions = new SdkLimitOptions
|
|
{
|
|
AttributeCountLimit = 3, // 3 => LogCategory, exception.type and exception.message
|
|
AttributeValueLengthLimit = 8,
|
|
};
|
|
|
|
var logRecords = new List<LogRecord>();
|
|
using var loggerFactory = LoggerFactory.Create(builder =>
|
|
{
|
|
builder.AddOpenTelemetry(options =>
|
|
{
|
|
options.AddInMemoryExporter(logRecords);
|
|
});
|
|
});
|
|
|
|
var logger = loggerFactory.CreateLogger("OtlpLogExporterTests");
|
|
logger.LogInformation(new NotSupportedException("I'm the exception message."), "Exception Occurred");
|
|
|
|
var logRecord = logRecords[0];
|
|
var otlpLogRecord = logRecord.ToOtlpLog(sdkLimitOptions);
|
|
|
|
Assert.NotNull(otlpLogRecord);
|
|
Assert.Equal(1u, otlpLogRecord.DroppedAttributesCount);
|
|
|
|
var exceptionTypeAtt = TryGetAttribute(otlpLogRecord, SemanticConventions.AttributeExceptionType);
|
|
Assert.NotNull(exceptionTypeAtt);
|
|
|
|
// "NotSuppo" == first 8 chars from the exception typename "NotSupportedException"
|
|
Assert.Equal("NotSuppo", exceptionTypeAtt.Value.StringValue);
|
|
var exceptionMessageAtt = TryGetAttribute(otlpLogRecord, SemanticConventions.AttributeExceptionMessage);
|
|
Assert.NotNull(exceptionMessageAtt);
|
|
|
|
// "I'm the " == first 8 chars from the exception message
|
|
Assert.Equal("I'm the ", exceptionMessageAtt.Value.StringValue);
|
|
|
|
var exceptionStackTraceAtt = TryGetAttribute(otlpLogRecord, SemanticConventions.AttributeExceptionStacktrace);
|
|
Assert.Null(exceptionStackTraceAtt);
|
|
}
|
|
|
|
private static OtlpCommon.KeyValue TryGetAttribute(OtlpLogs.LogRecord record, string key)
|
|
{
|
|
return record.Attributes.FirstOrDefault(att => att.Key == key);
|
|
}
|
|
}
|
|
}
|