//
// 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.
//
#if !NETFRAMEWORK
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Exporter;
using OpenTelemetry.Logs;
using OpenTelemetry.Tests;
using OpenTelemetry.Trace;
using Xunit;
namespace OpenTelemetry.Logs.Tests
{
public sealed class LogRecordTest
{
private enum Field
{
FormattedMessage,
State,
StateValues,
}
[Fact]
public void CheckCategoryNameForLog()
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null);
var logger = loggerFactory.CreateLogger();
logger.LogInformation("Log");
var categoryName = exportedItems[0].CategoryName;
Assert.Equal(typeof(LogRecordTest).FullName, categoryName);
}
[Theory]
[InlineData(LogLevel.Trace)]
[InlineData(LogLevel.Debug)]
[InlineData(LogLevel.Information)]
[InlineData(LogLevel.Warning)]
[InlineData(LogLevel.Error)]
[InlineData(LogLevel.Critical)]
public void CheckLogLevel(LogLevel logLevel)
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null);
var logger = loggerFactory.CreateLogger();
const string message = "Log {logLevel}";
logger.Log(logLevel, message, logLevel);
var logLevelRecorded = exportedItems[0].LogLevel;
Assert.Equal(logLevel, logLevelRecorded);
}
[Fact]
public void CheckStateForUnstructuredLog()
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null);
var logger = loggerFactory.CreateLogger();
const string message = "Hello, World!";
logger.LogInformation(message);
var state = exportedItems[0].State as IReadOnlyList>;
// state only has {OriginalFormat}
Assert.Equal(1, state.Count);
Assert.Equal(message, state.ToString());
}
[Fact]
[SuppressMessage("CA2254", "CA2254", Justification = "While you shouldn't use interpolation in a log message, this test verifies things work with it anyway.")]
public void CheckStateForUnstructuredLogWithStringInterpolation()
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null);
var logger = loggerFactory.CreateLogger();
var message = $"Hello from potato {0.99}.";
logger.LogInformation(message);
var state = exportedItems[0].State as IReadOnlyList>;
// state only has {OriginalFormat}
Assert.Equal(1, state.Count);
Assert.Equal(message, state.ToString());
}
[Fact]
public void CheckStateForStructuredLogWithTemplate()
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null);
var logger = loggerFactory.CreateLogger();
const string message = "Hello from {name} {price}.";
logger.LogInformation(message, "tomato", 2.99);
var state = exportedItems[0].State as IReadOnlyList>;
// state has name, price and {OriginalFormat}
Assert.Equal(3, state.Count);
// Check if state has name
Assert.Contains(state, item => item.Key == "name");
Assert.Equal("tomato", state.First(item => item.Key == "name").Value);
// Check if state has price
Assert.Contains(state, item => item.Key == "price");
Assert.Equal(2.99, state.First(item => item.Key == "price").Value);
// Check if state has OriginalFormat
Assert.Contains(state, item => item.Key == "{OriginalFormat}");
Assert.Equal(message, state.First(item => item.Key == "{OriginalFormat}").Value);
Assert.Equal($"Hello from tomato 2.99.", state.ToString());
}
[Fact]
public void CheckStateForStructuredLogWithStrongType()
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null);
var logger = loggerFactory.CreateLogger();
var food = new Food { Name = "artichoke", Price = 3.99 };
logger.LogInformation("{food}", food);
var state = exportedItems[0].State as IReadOnlyList>;
// state has food and {OriginalFormat}
Assert.Equal(2, state.Count);
// Check if state has food
Assert.Contains(state, item => item.Key == "food");
var foodParameter = (Food)state.First(item => item.Key == "food").Value;
Assert.Equal(food.Name, foodParameter.Name);
Assert.Equal(food.Price, foodParameter.Price);
// Check if state has OriginalFormat
Assert.Contains(state, item => item.Key == "{OriginalFormat}");
Assert.Equal("{food}", state.First(item => item.Key == "{OriginalFormat}").Value);
Assert.Equal(food.ToString(), state.ToString());
}
[Fact]
public void CheckStateForStructuredLogWithAnonymousType()
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null);
var logger = loggerFactory.CreateLogger();
var anonymousType = new { Name = "pumpkin", Price = 5.99 };
logger.LogInformation("{food}", anonymousType);
var state = exportedItems[0].State as IReadOnlyList>;
// state has food and {OriginalFormat}
Assert.Equal(2, state.Count);
// Check if state has food
Assert.Contains(state, item => item.Key == "food");
var foodParameter = state.First(item => item.Key == "food").Value as dynamic;
Assert.Equal(anonymousType.Name, foodParameter.Name);
Assert.Equal(anonymousType.Price, foodParameter.Price);
// Check if state has OriginalFormat
Assert.Contains(state, item => item.Key == "{OriginalFormat}");
Assert.Equal("{food}", state.First(item => item.Key == "{OriginalFormat}").Value);
Assert.Equal(anonymousType.ToString(), state.ToString());
}
[Fact]
public void CheckStateForStructuredLogWithGeneralType()
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null);
var logger = loggerFactory.CreateLogger();
var food = new Dictionary
{
["Name"] = "truffle",
["Price"] = 299.99,
};
logger.LogInformation("{food}", food);
var state = exportedItems[0].State as IReadOnlyList>;
// state only has food and {OriginalFormat}
Assert.Equal(2, state.Count);
// Check if state has food
Assert.Contains(state, item => item.Key == "food");
var foodParameter = state.First(item => item.Key == "food").Value as Dictionary;
Assert.True(food.Count == foodParameter.Count && !food.Except(foodParameter).Any());
// Check if state has OriginalFormat
Assert.Contains(state, item => item.Key == "{OriginalFormat}");
Assert.Equal("{food}", state.First(item => item.Key == "{OriginalFormat}").Value);
var prevCulture = CultureInfo.CurrentCulture;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
try
{
Assert.Equal("[Name, truffle], [Price, 299.99]", state.ToString());
}
finally
{
CultureInfo.CurrentCulture = prevCulture;
}
}
[Fact]
public void CheckStateForExceptionLogged()
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null);
var logger = loggerFactory.CreateLogger();
var exceptionMessage = "Exception Message";
var exception = new Exception(exceptionMessage);
const string message = "Exception Occurred";
logger.LogInformation(exception, message);
var state = exportedItems[0].State;
var itemCount = state.GetType().GetProperty("Count").GetValue(state);
// state only has {OriginalFormat}
Assert.Equal(1, itemCount);
var loggedException = exportedItems[0].Exception;
Assert.NotNull(loggedException);
Assert.Equal(exceptionMessage, loggedException.Message);
Assert.Equal(message, state.ToString());
}
[Fact]
public void CheckStateCanBeSet()
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null);
var logger = loggerFactory.CreateLogger();
logger.LogInformation("This does not matter.");
var logRecord = exportedItems[0];
logRecord.State = "newState";
var expectedState = "newState";
Assert.Equal(expectedState, logRecord.State);
}
[Fact]
public void CheckStateValuesCanBeSet()
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true);
var logger = loggerFactory.CreateLogger();
logger.Log(
LogLevel.Information,
0,
new List> { new KeyValuePair("Key1", "Value1") },
null,
(s, e) => "OpenTelemetry!");
var logRecord = exportedItems[0];
var expectedStateValues = new List> { new KeyValuePair("Key2", "Value2") };
logRecord.StateValues = expectedStateValues;
Assert.Equal(expectedStateValues, logRecord.StateValues);
}
[Fact]
public void CheckFormattedMessageCanBeSet()
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeFormattedMessage = true);
var logger = loggerFactory.CreateLogger();
logger.LogInformation("OpenTelemetry {Greeting} {Subject}!", "Hello", "World");
var logRecord = exportedItems[0];
var expectedFormattedMessage = "OpenTelemetry Good Night!";
logRecord.FormattedMessage = expectedFormattedMessage;
Assert.Equal(expectedFormattedMessage, logRecord.FormattedMessage);
}
[Fact]
public void CheckStateCanBeSetByProcessor()
{
var exportedItems = new List();
var exporter = new InMemoryExporter(exportedItems);
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddOpenTelemetry(options =>
{
options.AddProcessor(new RedactionProcessor(Field.State));
options.AddInMemoryExporter(exportedItems);
});
});
var logger = loggerFactory.CreateLogger();
logger.LogInformation($"This does not matter.");
var state = exportedItems[0].State as IReadOnlyList>;
Assert.Equal("newStateKey", state[0].Key.ToString());
Assert.Equal("newStateValue", state[0].Value.ToString());
}
[Fact]
public void CheckStateValuesCanBeSetByProcessor()
{
var exportedItems = new List();
var exporter = new InMemoryExporter(exportedItems);
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddOpenTelemetry(options =>
{
options.AddProcessor(new RedactionProcessor(Field.StateValues));
options.AddInMemoryExporter(exportedItems);
options.ParseStateValues = true;
});
});
var logger = loggerFactory.CreateLogger();
logger.LogInformation("This does not matter.");
var stateValue = exportedItems[0];
Assert.Equal(new KeyValuePair("newStateValueKey", "newStateValueValue"), stateValue.StateValues[0]);
}
[Fact]
public void CheckFormattedMessageCanBeSetByProcessor()
{
var exportedItems = new List();
var exporter = new InMemoryExporter(exportedItems);
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddOpenTelemetry(options =>
{
options.AddProcessor(new RedactionProcessor(Field.FormattedMessage));
options.AddInMemoryExporter(exportedItems);
options.IncludeFormattedMessage = true;
});
});
var logger = loggerFactory.CreateLogger();
logger.LogInformation("OpenTelemetry {Greeting} {Subject}!", "Hello", "World");
var item = exportedItems[0];
Assert.Equal("OpenTelemetry Good Night!", item.FormattedMessage);
}
[Fact]
public void CheckTraceIdForLogWithinDroppedActivity()
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null);
var logger = loggerFactory.CreateLogger();
logger.LogInformation("Log within a dropped activity");
var logRecord = exportedItems[0];
Assert.Null(Activity.Current);
Assert.Equal(default, logRecord.TraceId);
Assert.Equal(default, logRecord.SpanId);
Assert.Equal(default, logRecord.TraceFlags);
}
[Fact]
public void CheckTraceIdForLogWithinActivityMarkedAsRecordOnly()
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null);
var logger = loggerFactory.CreateLogger();
var sampler = new RecordOnlySampler();
var exportedActivityList = new List();
var activitySourceName = "LogRecordTest";
using var activitySource = new ActivitySource(activitySourceName);
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource(activitySourceName)
.SetSampler(sampler)
.AddInMemoryExporter(exportedActivityList)
.Build();
using var activity = activitySource.StartActivity("Activity");
logger.LogInformation("Log within activity marked as RecordOnly");
var logRecord = exportedItems[0];
var currentActivity = Activity.Current;
Assert.NotNull(Activity.Current);
Assert.Equal(currentActivity.TraceId, logRecord.TraceId);
Assert.Equal(currentActivity.SpanId, logRecord.SpanId);
Assert.Equal(currentActivity.ActivityTraceFlags, logRecord.TraceFlags);
}
[Fact]
public void CheckTraceIdForLogWithinActivityMarkedAsRecordAndSample()
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null);
var logger = loggerFactory.CreateLogger();
var sampler = new AlwaysOnSampler();
var exportedActivityList = new List();
var activitySourceName = "LogRecordTest";
using var activitySource = new ActivitySource(activitySourceName);
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource(activitySourceName)
.SetSampler(sampler)
.AddInMemoryExporter(exportedActivityList)
.Build();
using var activity = activitySource.StartActivity("Activity");
logger.LogInformation("Log within activity marked as RecordAndSample");
var logRecord = exportedItems[0];
var currentActivity = Activity.Current;
Assert.NotNull(Activity.Current);
Assert.Equal(currentActivity.TraceId, logRecord.TraceId);
Assert.Equal(currentActivity.SpanId, logRecord.SpanId);
Assert.Equal(currentActivity.ActivityTraceFlags, logRecord.TraceFlags);
}
[Fact]
public void VerifyIncludeFormattedMessage_False()
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeFormattedMessage = false);
var logger = loggerFactory.CreateLogger();
logger.LogInformation("OpenTelemetry!");
var logRecord = exportedItems[0];
Assert.Null(logRecord.FormattedMessage);
}
[Fact]
public void VerifyIncludeFormattedMessage_True()
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeFormattedMessage = true);
var logger = loggerFactory.CreateLogger();
logger.LogInformation("OpenTelemetry!");
var logRecord = exportedItems[0];
Assert.Equal("OpenTelemetry!", logRecord.FormattedMessage);
logger.LogInformation("OpenTelemetry {Greeting} {Subject}!", "Hello", "World");
logRecord = exportedItems[1];
Assert.Equal("OpenTelemetry Hello World!", logRecord.FormattedMessage);
}
[Fact]
public void IncludeFormattedMessageTestWhenFormatterNull()
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeFormattedMessage = true);
var logger = loggerFactory.CreateLogger();
logger.Log(LogLevel.Information, default, "Hello World!", null, null);
var logRecord = exportedItems[0];
Assert.Null(logRecord.FormattedMessage);
// Pass null as formatter function
logger.Log(LogLevel.Information, default, "Hello World!", null, null);
logRecord = exportedItems[1];
Assert.Null(logRecord.FormattedMessage);
var expectedFormattedMessage = "formatted message";
logger.Log(LogLevel.Information, default, "Hello World!", null, (state, ex) => expectedFormattedMessage);
logRecord = exportedItems[2];
Assert.Equal(expectedFormattedMessage, logRecord.FormattedMessage);
}
[Fact]
public void VerifyIncludeScopes_False()
{
using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeScopes = false);
var logger = loggerFactory.CreateLogger();
using var scope = logger.BeginScope("string_scope");
logger.LogInformation("OpenTelemetry!");
var logRecord = exportedItems[0];
List