Logs: Scope improvements (#2026)

This commit is contained in:
Mikel Blanchard 2021-05-06 10:12:43 -07:00 committed by GitHub
parent 9805587151
commit 6cdfcea67c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 456 additions and 22 deletions

View File

@ -44,12 +44,21 @@ internal class MyExporter : BaseExporter<LogRecord>
sb.Append($"{record}(");
int scopeDepth = -1;
record.ForEachScope(ProcessScope, sb);
static void ProcessScope(object scope, StringBuilder builder)
=> builder.Append($"{scope}");
void ProcessScope(LogRecordScope scope, StringBuilder builder)
{
if (++scopeDepth > 0)
{
builder.Append(", ");
}
sb.Append($")");
builder.Append($"{scope.Scope}");
}
sb.Append(')');
}
Console.WriteLine($"{this.name}.Export([{sb.ToString()}])");

View File

@ -20,6 +20,7 @@ using System.Linq;
using System.Net.Http;
using Examples.AspNetCore.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace Examples.AspNetCore.Controllers
{
@ -32,23 +33,39 @@ namespace Examples.AspNetCore.Controllers
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching",
};
private static HttpClient httpClient = new HttpClient();
private static readonly HttpClient HttpClient = new HttpClient();
private readonly ILogger<WeatherForecastController> logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
using var scope = this.logger.BeginScope("{Id}", Guid.NewGuid().ToString("N"));
// Making an http call here to serve as an example of
// how dependency calls will be captured and treated
// automatically as child of incoming request.
var res = httpClient.GetStringAsync("http://google.com").Result;
var res = HttpClient.GetStringAsync("http://google.com").Result;
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)],
})
.ToArray();
this.logger.LogInformation(
"WeatherForecasts generated {count}: {forecasts}",
forecast.Length,
forecast);
return forecast;
}
}
}

View File

@ -15,7 +15,10 @@
// </copyright>
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Logs;
namespace Examples.AspNetCore
{
@ -31,6 +34,23 @@ namespace Examples.AspNetCore
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.ConfigureLogging((context, builder) =>
{
builder.ClearProviders();
builder.AddConsole();
var useLogging = context.Configuration.GetValue<bool>("UseLogging");
if (useLogging)
{
builder.AddOpenTelemetry(options =>
{
options.IncludeScopes = true;
options.ParseStateValues = true;
options.IncludeFormattedMessage = true;
options.AddConsoleExporter();
});
}
});
}
}

View File

@ -8,6 +8,7 @@
},
"AllowedHosts": "*",
"UseExporter": "console",
"UseLogging": true,
"Jaeger": {
"ServiceName": "jaeger-test",
"AgentHost": "localhost",

View File

@ -16,6 +16,7 @@
#if NET461 || NETSTANDARD2_0
using System;
using System.Collections.Generic;
using OpenTelemetry.Logs;
using OpenTelemetry.Resources;
@ -64,10 +65,22 @@ namespace OpenTelemetry.Exporter
this.WriteLine($"{"LogRecord.Exception:".PadRight(RightPaddingLength)}{logRecord.Exception?.Message}");
}
int scopeDepth = -1;
logRecord.ForEachScope(ProcessScope, this);
static void ProcessScope(object scope, ConsoleLogRecordExporter exporter)
=> exporter.WriteLine($"{"LogRecord.Scope:".PadRight(RightPaddingLength)}{scope}");
void ProcessScope(LogRecordScope scope, ConsoleLogRecordExporter exporter)
{
if (++scopeDepth == 0)
{
exporter.WriteLine("LogRecord.ScopeValues (Key:Value):");
}
foreach (KeyValuePair<string, object> scopeItem in scope)
{
exporter.WriteLine($"[Scope.{scopeDepth}]:{scopeItem.Key.PadRight(RightPaddingLength)}{scopeItem.Value}");
}
}
var resource = this.ParentProvider.GetResource();
if (resource != Resource.Empty)

View File

@ -1,6 +1,17 @@
OpenTelemetry.Logs.LogRecord.ForEachScope<TState>(System.Action<object, TState> callback, TState state) -> void
OpenTelemetry.Logs.LogRecord.ForEachScope<TState>(System.Action<OpenTelemetry.Logs.LogRecordScope, TState> callback, TState state) -> void
OpenTelemetry.Logs.LogRecord.FormattedMessage.get -> string
OpenTelemetry.Logs.LogRecord.StateValues.get -> System.Collections.Generic.IReadOnlyList<System.Collections.Generic.KeyValuePair<string, object>>
OpenTelemetry.Logs.LogRecordScope
OpenTelemetry.Logs.LogRecordScope.Enumerator
OpenTelemetry.Logs.LogRecordScope.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair<string, object>
OpenTelemetry.Logs.LogRecordScope.Enumerator.Dispose() -> void
OpenTelemetry.Logs.LogRecordScope.Enumerator.Enumerator() -> void
OpenTelemetry.Logs.LogRecordScope.Enumerator.Enumerator(object scope) -> void
OpenTelemetry.Logs.LogRecordScope.Enumerator.MoveNext() -> bool
OpenTelemetry.Logs.LogRecordScope.Enumerator.Reset() -> void
OpenTelemetry.Logs.LogRecordScope.GetEnumerator() -> OpenTelemetry.Logs.LogRecordScope.Enumerator
OpenTelemetry.Logs.LogRecordScope.LogRecordScope() -> void
OpenTelemetry.Logs.LogRecordScope.Scope.get -> object
OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeFormattedMessage.get -> bool
OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeFormattedMessage.set -> void
OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeScopes.get -> bool
@ -16,6 +27,7 @@ OpenTelemetry.Trace.TracerProviderBuilderBase
OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation(string instrumentationName, string instrumentationVersion, System.Func<object> instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder
OpenTelemetry.Trace.TracerProviderBuilderBase.Build() -> OpenTelemetry.Trace.TracerProvider
OpenTelemetry.Trace.TracerProviderBuilderBase.TracerProviderBuilderBase() -> void
override OpenTelemetry.BatchLogRecordExportProcessor.OnEnd(OpenTelemetry.Logs.LogRecord data) -> void
override OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation<TInstrumentation>(System.Func<TInstrumentation> instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder
override OpenTelemetry.Trace.TracerProviderBuilderBase.AddLegacySource(string operationName) -> OpenTelemetry.Trace.TracerProviderBuilder
override OpenTelemetry.Trace.TracerProviderBuilderBase.AddSource(params string[] names) -> OpenTelemetry.Trace.TracerProviderBuilder

View File

@ -1,6 +1,17 @@
OpenTelemetry.Logs.LogRecord.ForEachScope<TState>(System.Action<object, TState> callback, TState state) -> void
OpenTelemetry.Logs.LogRecord.ForEachScope<TState>(System.Action<OpenTelemetry.Logs.LogRecordScope, TState> callback, TState state) -> void
OpenTelemetry.Logs.LogRecord.FormattedMessage.get -> string
OpenTelemetry.Logs.LogRecord.StateValues.get -> System.Collections.Generic.IReadOnlyList<System.Collections.Generic.KeyValuePair<string, object>>
OpenTelemetry.Logs.LogRecordScope
OpenTelemetry.Logs.LogRecordScope.Enumerator
OpenTelemetry.Logs.LogRecordScope.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair<string, object>
OpenTelemetry.Logs.LogRecordScope.Enumerator.Dispose() -> void
OpenTelemetry.Logs.LogRecordScope.Enumerator.Enumerator() -> void
OpenTelemetry.Logs.LogRecordScope.Enumerator.Enumerator(object scope) -> void
OpenTelemetry.Logs.LogRecordScope.Enumerator.MoveNext() -> bool
OpenTelemetry.Logs.LogRecordScope.Enumerator.Reset() -> void
OpenTelemetry.Logs.LogRecordScope.GetEnumerator() -> OpenTelemetry.Logs.LogRecordScope.Enumerator
OpenTelemetry.Logs.LogRecordScope.LogRecordScope() -> void
OpenTelemetry.Logs.LogRecordScope.Scope.get -> object
OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeFormattedMessage.get -> bool
OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeFormattedMessage.set -> void
OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeScopes.get -> bool
@ -16,6 +27,7 @@ OpenTelemetry.Trace.TracerProviderBuilderBase
OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation(string instrumentationName, string instrumentationVersion, System.Func<object> instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder
OpenTelemetry.Trace.TracerProviderBuilderBase.Build() -> OpenTelemetry.Trace.TracerProvider
OpenTelemetry.Trace.TracerProviderBuilderBase.TracerProviderBuilderBase() -> void
override OpenTelemetry.BatchLogRecordExportProcessor.OnEnd(OpenTelemetry.Logs.LogRecord data) -> void
override OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation<TInstrumentation>(System.Func<TInstrumentation> instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder
override OpenTelemetry.Trace.TracerProviderBuilderBase.AddLegacySource(string operationName) -> OpenTelemetry.Trace.TracerProviderBuilder
override OpenTelemetry.Trace.TracerProviderBuilderBase.AddSource(params string[] names) -> OpenTelemetry.Trace.TracerProviderBuilder

View File

@ -35,6 +35,13 @@ namespace OpenTelemetry
maxExportBatchSize)
{
}
public override void OnEnd(LogRecord data)
{
data.BufferLogScopes();
base.OnEnd(data);
}
}
}
#endif

View File

@ -13,6 +13,13 @@ please check the latest changes
public API
([#2019](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2019))
* Fixed an issue causing inconsistent log scopes when using
`BatchLogRecordExportProcessor`. To make parsing scopes easier the
`LogRecord.ForEachScope` signature has been changed to receive instances of
`LogRecordScope` (a new type which implements
`IEnumerator<KeyValuePair<string, object>>` for accessing scope items)
([#2026](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2026))
## 1.1.0-beta2
Released 2021-Apr-23

View File

@ -18,16 +18,22 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Logging;
namespace OpenTelemetry.Logs
{
/// <summary>
/// Log record base class.
/// Stores details about a log message.
/// </summary>
public sealed class LogRecord
{
private readonly IExternalScopeProvider scopeProvider;
private static readonly Action<object, List<object>> AddScopeToBufferedList = (object scope, List<object> state) =>
{
state.Add(scope);
};
private List<object> bufferedScopes;
internal LogRecord(
IExternalScopeProvider scopeProvider,
@ -40,7 +46,7 @@ namespace OpenTelemetry.Logs
Exception exception,
IReadOnlyList<KeyValuePair<string, object>> stateValues)
{
this.scopeProvider = scopeProvider;
this.ScopeProvider = scopeProvider;
var activity = Activity.Current;
if (activity != null)
@ -79,23 +85,92 @@ namespace OpenTelemetry.Logs
public string FormattedMessage { get; }
/// <summary>
/// Gets the raw state attached to the log. Set to <see
/// langword="null"/> when <see
/// cref="OpenTelemetryLoggerOptions.ParseStateValues"/> is enabled.
/// </summary>
public object State { get; }
/// <summary>
/// Gets the parsed state values attached to the log. Set when <see
/// cref="OpenTelemetryLoggerOptions.ParseStateValues"/> is enabled
/// otherwise <see langword="null"/>.
/// </summary>
public IReadOnlyList<KeyValuePair<string, object>> StateValues { get; }
public Exception Exception { get; }
internal IExternalScopeProvider ScopeProvider { get; set; }
/// <summary>
/// Executes callback for each currently active scope objects in order
/// of creation. All callbacks are guaranteed to be called inline from
/// this method.
/// </summary>
/// <remarks>
/// Note: Scopes are only available during the lifecycle of the log
/// message being written. If you need to capture scopes to be used
/// later (for example in batching scenarios), call <see
/// cref="BufferLogScopes"/> to safely capture the values (incurs
/// allocation).
/// </remarks>
/// <typeparam name="TState">State.</typeparam>
/// <param name="callback">The callback to be executed for every scope object.</param>
/// <param name="state">The state object to be passed into the callback.</param>
public void ForEachScope<TState>(Action<object, TState> callback, TState state)
public void ForEachScope<TState>(Action<LogRecordScope, TState> callback, TState state)
{
this.scopeProvider?.ForEachScope(callback, state);
var forEachScopeState = new ScopeForEachState<TState>(callback, state);
if (this.bufferedScopes != null)
{
foreach (object scope in this.bufferedScopes)
{
ScopeForEachState<TState>.ForEachScope(scope, forEachScopeState);
}
}
else if (this.ScopeProvider != null)
{
this.ScopeProvider.ForEachScope(ScopeForEachState<TState>.ForEachScope, forEachScopeState);
}
}
/// <summary>
/// Buffers the scopes attached to the log into a list so that they can
/// be safely processed after the log message lifecycle has ended.
/// </summary>
internal void BufferLogScopes()
{
if (this.ScopeProvider == null || this.bufferedScopes != null)
{
return;
}
List<object> scopes = new List<object>();
this.ScopeProvider?.ForEachScope(AddScopeToBufferedList, scopes);
this.bufferedScopes = scopes;
}
private readonly struct ScopeForEachState<TState>
{
public static readonly Action<object, ScopeForEachState<TState>> ForEachScope = (object scope, ScopeForEachState<TState> state) =>
{
LogRecordScope logRecordScope = new LogRecordScope(scope);
state.Callback(logRecordScope, state.UserState);
};
public readonly Action<LogRecordScope, TState> Callback;
public readonly TState UserState;
public ScopeForEachState(Action<LogRecordScope, TState> callback, TState state)
{
this.Callback = callback;
this.UserState = state;
}
}
}
}

View File

@ -0,0 +1,100 @@
// <copyright file="LogRecordScope.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>
#if NET461 || NETSTANDARD2_0
using System;
using System.Collections;
using System.Collections.Generic;
namespace OpenTelemetry.Logs
{
/// <summary>
/// Stores details about a scope attached to a log message.
/// </summary>
public readonly struct LogRecordScope
{
internal LogRecordScope(object scope)
{
this.Scope = scope;
}
/// <summary>
/// Gets the raw scope value.
/// </summary>
public object Scope { get; }
/// <summary>
/// Gets an <see cref="IEnumerator"/> for looping over the inner values
/// of the scope.
/// </summary>
/// <returns><see cref="Enumerator"/>.</returns>
public Enumerator GetEnumerator() => new Enumerator(this.Scope);
/// <summary>
/// LogRecordScope enumerator.
/// </summary>
public struct Enumerator : IEnumerator<KeyValuePair<string, object>>
{
private readonly IReadOnlyList<KeyValuePair<string, object>> scope;
private int position;
public Enumerator(object scope)
{
if (scope is IReadOnlyList<KeyValuePair<string, object>> scopeList)
{
this.scope = scopeList;
}
else if (scope is IEnumerable<KeyValuePair<string, object>> scopeEnumerable)
{
this.scope = new List<KeyValuePair<string, object>>(scopeEnumerable);
}
else
{
this.scope = new List<KeyValuePair<string, object>>
{
new KeyValuePair<string, object>(string.Empty, scope),
};
}
this.position = 0;
this.Current = default;
}
public KeyValuePair<string, object> Current { get; private set; }
object IEnumerator.Current => this.Current;
public bool MoveNext()
{
if (this.position < this.scope.Count)
{
this.Current = this.scope[this.position++];
return true;
}
return false;
}
public void Dispose()
{
}
public void Reset()
=> throw new NotSupportedException();
}
}
}
#endif

View File

@ -60,6 +60,8 @@ namespace OpenTelemetry.Logs
options.ParseStateValues ? this.ParseState(state) : null);
processor.OnEnd(record);
record.ScopeProvider = null;
}
}

View File

@ -0,0 +1,80 @@
// <copyright file="LogScopeBenchmarks.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>
#if NETCOREAPP3_1
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using BenchmarkDotNet.Attributes;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Logs;
namespace Benchmarks.Logs
{
[MemoryDiagnoser]
public class LogScopeBenchmarks
{
private readonly LoggerExternalScopeProvider scopeProvider = new LoggerExternalScopeProvider();
private readonly Action<LogRecordScope, object> callback = (LogRecordScope scope, object state) =>
{
foreach (KeyValuePair<string, object> scopeItem in scope)
{
}
};
private readonly LogRecord logRecord;
public LogScopeBenchmarks()
{
this.scopeProvider.Push(new ReadOnlyCollection<KeyValuePair<string, object>>(
new List<KeyValuePair<string, object>>
{
new KeyValuePair<string, object>("item1", "value1"),
new KeyValuePair<string, object>("item2", "value2"),
}));
this.scopeProvider.Push(new ReadOnlyCollection<KeyValuePair<string, object>>(
new List<KeyValuePair<string, object>>
{
new KeyValuePair<string, object>("item3", "value3"),
}));
this.scopeProvider.Push(new ReadOnlyCollection<KeyValuePair<string, object>>(
new List<KeyValuePair<string, object>>
{
new KeyValuePair<string, object>("item4", "value4"),
new KeyValuePair<string, object>("item5", "value5"),
}));
this.logRecord = new LogRecord(
this.scopeProvider,
DateTime.UtcNow,
"Benchmark",
LogLevel.Information,
0,
"Message",
null,
null,
null);
}
[Benchmark]
public void ForEachScope()
{
this.logRecord.ForEachScope(this.callback, null);
}
}
}
#endif

View File

@ -32,7 +32,7 @@ using Xunit;
namespace OpenTelemetry.Tests.Logs
{
public class LogRecordTest : IDisposable
public sealed class LogRecordTest : IDisposable
{
private readonly ILogger logger;
private readonly List<LogRecord> exportedItems = new List<LogRecord>();
@ -48,7 +48,7 @@ namespace OpenTelemetry.Tests.Logs
public LogRecordTest()
{
this.exporter = new InMemoryExporter<LogRecord>(this.exportedItems);
this.processor = new SimpleLogRecordExportProcessor(this.exporter);
this.processor = new TestLogRecordProcessor(this.exporter);
#if NETCOREAPP2_1
var serviceCollection = new ServiceCollection().AddLogging(builder =>
#else
@ -348,7 +348,7 @@ namespace OpenTelemetry.Tests.Logs
var logRecord = this.exportedItems[0];
List<object> scopes = new List<object>();
logRecord.ForEachScope<object>((scope, state) => scopes.Add(scope), null);
logRecord.ForEachScope<object>((scope, state) => scopes.Add(scope.Scope), null);
Assert.Empty(scopes);
this.options.IncludeScopes = true;
@ -357,21 +357,85 @@ namespace OpenTelemetry.Tests.Logs
this.logger.LogInformation("OpenTelemetry!");
logRecord = this.exportedItems[1];
logRecord.ForEachScope<object>((scope, state) => scopes.Add(scope), null);
int reachedDepth = -1;
logRecord.ForEachScope<object>(
(scope, state) =>
{
reachedDepth++;
scopes.Add(scope.Scope);
foreach (KeyValuePair<string, object> item in scope)
{
Assert.Equal(string.Empty, item.Key);
Assert.Equal("string_scope", item.Value);
}
},
null);
Assert.Single(scopes);
Assert.Equal(0, reachedDepth);
Assert.Equal("string_scope", scopes[0]);
scopes.Clear();
using var scope2 = this.logger.BeginScope(2021);
List<KeyValuePair<string, object>> expectedScope2 = new List<KeyValuePair<string, object>>
{
new KeyValuePair<string, object>("item1", "value1"),
new KeyValuePair<string, object>("item2", "value2"),
};
using var scope2 = this.logger.BeginScope(expectedScope2);
this.logger.LogInformation("OpenTelemetry!");
logRecord = this.exportedItems[2];
logRecord.ForEachScope<object>((scope, state) => scopes.Add(scope), null);
reachedDepth = -1;
logRecord.ForEachScope<object>(
(scope, state) =>
{
scopes.Add(scope.Scope);
if (reachedDepth++ == 1)
{
foreach (KeyValuePair<string, object> item in scope)
{
Assert.Contains(item, expectedScope2);
}
}
},
null);
Assert.Equal(2, scopes.Count);
Assert.Equal(1, reachedDepth);
Assert.Equal("string_scope", scopes[0]);
Assert.Equal(2021, scopes[1]);
Assert.Same(expectedScope2, scopes[1]);
scopes.Clear();
KeyValuePair<string, object>[] expectedScope3 = new KeyValuePair<string, object>[]
{
new KeyValuePair<string, object>("item3", "value3"),
new KeyValuePair<string, object>("item4", "value4"),
};
using var scope3 = this.logger.BeginScope(expectedScope3);
this.logger.LogInformation("OpenTelemetry!");
logRecord = this.exportedItems[3];
reachedDepth = -1;
logRecord.ForEachScope<object>(
(scope, state) =>
{
scopes.Add(scope.Scope);
if (reachedDepth++ == 2)
{
foreach (KeyValuePair<string, object> item in scope)
{
Assert.Contains(item, expectedScope3);
}
}
},
null);
Assert.Equal(3, scopes.Count);
Assert.Equal(2, reachedDepth);
Assert.Equal("string_scope", scopes[0]);
Assert.Same(expectedScope2, scopes[1]);
Assert.Same(expectedScope3, scopes[2]);
}
finally
{
@ -596,6 +660,21 @@ namespace OpenTelemetry.Tests.Logs
{
public string Property { get; set; }
}
private class TestLogRecordProcessor : SimpleExportProcessor<LogRecord>
{
public TestLogRecordProcessor(BaseExporter<LogRecord> exporter)
: base(exporter)
{
}
public override void OnEnd(LogRecord data)
{
data.BufferLogScopes();
base.OnEnd(data);
}
}
}
}
#endif