Skip to content
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
4 changes: 2 additions & 2 deletions examples/Example.Console/Example.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.3" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.StackExchangeRedis" Version="1.12.0-beta.1" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.13.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.StackExchangeRedis" Version="1.13.0-beta.1" />
</ItemGroup>

<ItemGroup>
Expand Down
18 changes: 18 additions & 0 deletions examples/Example.MinimalApi/CustomProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System.Diagnostics;
using OpenTelemetry;

namespace Example.MinimalApi;

public class CustomProcessor : BaseProcessor<Activity>
{
public override void OnEnd(Activity data)
{
Thread.Sleep(1000); // Enough time for the export to have happened

data.SetTag("custom-processor-tag", "ProcessedByCustomProcessor");
}
}
5 changes: 3 additions & 2 deletions examples/Example.MinimalApi/Example.MinimalApi.csproj
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>4b1d6eb4-a323-4148-8195-1ec759984c7d</UserSecretsId>
</PropertyGroup>

<ItemGroup>
Expand All @@ -12,7 +13,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
</ItemGroup>

</Project>
37 changes: 30 additions & 7 deletions examples/Example.MinimalApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,41 @@
using System.Diagnostics;
using Example.MinimalApi;
using OpenTelemetry;
using OpenTelemetry.Exporter;
using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

//builder.AddElasticOpenTelemetry();
var options = new Elastic.OpenTelemetry.ElasticOpenTelemetryOptions()
{
// You can customize options here if needed
};

// This will add the OpenTelemetry services using Elastic defaults
builder.AddServiceDefaults();
//// Force small batch size and no delay for quicker exports in this example
builder.Services.Configure<OtlpExporterOptions>(o =>
{
o.BatchExportProcessorOptions.MaxExportBatchSize = 1;
o.BatchExportProcessorOptions.ScheduledDelayMilliseconds = 1;
});

//builder.Services.AddOpenTelemetry()
// .WithTracing(t => t
// .WithElasticDefaults(builder.Configuration, t => t
// .AddSource(Api.ActivitySourceName)
// .AddProcessor(new CustomProcessor())));

builder.Services.AddOpenTelemetry()
.WithElasticTracing(builder.Configuration, t => t
.AddSource(Api.ActivitySourceName)
.AddProcessor(new CustomProcessor()));

// Preferred method to add Elastic OpenTelemetry integration
//builder.AddElasticOpenTelemetry(options, otelBuilder => otelBuilder
// .WithTracing(t => t
// .AddSource(Api.ActivitySourceName)
// .AddProcessor(new CustomProcessor())));

builder.Services
.AddHttpClient() // Adds IHttpClientFactory
.AddElasticOpenTelemetry() // Adds app specific tracing
.WithTracing(t => t.AddSource(Api.ActivitySourceName));
builder.AddServiceDefaults();

var app = builder.Build();

Expand Down
4 changes: 1 addition & 3 deletions examples/ServiceDefaults/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public static class Extensions
{
public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
{
builder.ConfigureOpenTelemetry();
//builder.ConfigureOpenTelemetry();

builder.AddDefaultHealthChecks();

Expand Down Expand Up @@ -49,8 +49,6 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati
}
});

builder.AddOpenTelemetryExporters();

return builder;
}

Expand Down
12 changes: 6 additions & 6 deletions examples/ServiceDefaults/ServiceDefaults.csproj
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireSharedProject>true</IsAspireSharedProject>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />

<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.1.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.0.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.12.0-beta.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Process" Version="1.12.0-beta.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.13.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.13.0-beta.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Process" Version="1.13.0-beta.1" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public TracerProviderBuilder BeforeConfigureTracerProvider(TracerProviderBuilder
builder.ConfigureResource(r => r.WithElasticDefaultsCore(_components, null, null));

builder.ConfigureServices(sc => sc.Configure<OtlpExporterOptions>(OtlpExporterDefaults.OtlpExporterOptions));
logger.LogConfiguredOtlpExporterOptions();

TracerProvderBuilderExtensions.AddActivitySourceWithLogging(builder, logger, "Elastic.Transport", "<n/a>");
TracerProvderBuilderExtensions.AddElasticProcessorsCore(builder, null, _components, null);
Expand Down Expand Up @@ -74,6 +75,7 @@ public MeterProviderBuilder BeforeConfigureMeterProvider(MeterProviderBuilder bu
builder.ConfigureServices(sc => sc
.Configure<OtlpExporterOptions>(OtlpExporterDefaults.OtlpExporterOptions)
.Configure<MetricReaderOptions>(o => o.TemporalityPreference = MetricReaderTemporalityPreference.Delta));
logger.LogConfiguredOtlpExporterOptions();

logger.LogConfiguredSignalProvider(nameof(Signals.Metrics), nameof(MeterProviderBuilder), "<n/a>");

Expand All @@ -91,8 +93,7 @@ public MeterProviderBuilder BeforeConfigureMeterProvider(MeterProviderBuilder bu
/// <summary>
/// To configure logs SDK (the method name is the same as for other logs options).
/// </summary>
public void ConfigureLogsOptions(OpenTelemetryLoggerOptions options) =>
options.WithElasticDefaults(_components.Logger);
public void ConfigureLogsOptions(OpenTelemetryLoggerOptions options) => options.WithElasticDefaults(_components.Logger);

/// <summary>
/// To configure Resource.
Expand Down
21 changes: 21 additions & 0 deletions src/Elastic.OpenTelemetry.Core/BuilderContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using Microsoft.Extensions.DependencyInjection;

namespace Elastic.OpenTelemetry.Core;

/// <summary>
/// This is used internally to pass context around during the fluent builder configuration process.
/// </summary>
internal sealed class BuilderContext<T> where T : class
{
internal required T Builder { get; init; }

internal required BuilderState BuilderState { get; init; }

internal required BuilderOptions<T> BuilderOptions { get; init; }

internal IServiceCollection? Services { get; init; }
}
9 changes: 9 additions & 0 deletions src/Elastic.OpenTelemetry.Core/BuilderOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

namespace Elastic.OpenTelemetry.Core;

internal readonly record struct BuilderOptions<T>(
Action<T>? UserProvidedConfigureBuilder,
bool DeferAddOtlpExporter) where T : class;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

using Elastic.OpenTelemetry.Configuration;
using Elastic.OpenTelemetry.Core;
using Elastic.OpenTelemetry.Diagnostics;
using Microsoft.Extensions.Logging;

namespace Elastic.OpenTelemetry.Diagnostics;
Expand Down
109 changes: 109 additions & 0 deletions src/Elastic.OpenTelemetry.Core/Diagnostics/DeferredLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Elastic.OpenTelemetry.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace Elastic.OpenTelemetry.Diagnostics;

/// <summary>
/// Used to capture log entries before the file logger is initialized.
/// </summary>
/// <remarks>
/// Log entries are stored in a concurrent queue and drained to the first file logger that is available.
/// Log entries are only captured if file logging is enabled in the options.
/// </remarks>
internal sealed class DeferredLogger : ILogger
{
private readonly bool _isEnabled = false;
private readonly LogLevel _configuredLogLevel;
private readonly ConcurrentQueue<string> _logQueue = new();
private readonly ILogger? _additionalLogger;

private static readonly Lock Lock = new();

/// <summary>
/// Create an instance of <see cref="DeferredLogger"/>.
/// </summary>
/// <param name="options">The options used to configure the logger.</param>
/// <param name="additionalLogger">An optional additional logger to which log entries will be written. For example,
/// this may be available in ASP.NET Core scenarios and ensures the early log entries are sent to other sinks, such
/// as the console, when available.</param>
private DeferredLogger(CompositeElasticOpenTelemetryOptions options, ILogger? additionalLogger = null)
{
_isEnabled = options.GlobalLogEnabled && options.LogTargets.HasFlag(LogTargets.File);

if (!_isEnabled)
return;

_configuredLogLevel = options.LogLevel;
_additionalLogger = additionalLogger;
}

public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;

public bool IsEnabled(LogLevel logLevel) => _isEnabled && _configuredLogLevel <= logLevel;

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
_additionalLogger?.Log(logLevel, eventId, state, exception, formatter);

if (!IsEnabled(logLevel))
return;

var logLine = LogFormatter.Format(logLevel, eventId, state, exception, formatter);

if (exception is not null)
logLine = $"{logLine}{Environment.NewLine}{exception}";

_logQueue.Enqueue(logLine);
}

internal void DrainAndRelease(StreamWriter streamWriter)
{
using (Lock.EnterScope())
{
while (_logQueue.TryDequeue(out var deferredLog))
{
streamWriter.WriteLine(deferredLog);
}

Instance = null;
}
}

private static DeferredLogger? Instance;

internal static ILogger GetOrCreate(CompositeElasticOpenTelemetryOptions options)
{
using (Lock.EnterScope())
{
// We want, at most, one instance of DeferredLogger for the application.
// We only create a DeferredFileLogger if file logging is enabled
if (options.GlobalLogEnabled && options.LogTargets.HasFlag(LogTargets.File))
return Instance ??= new DeferredLogger(options, options.AdditionalLogger);

return NullLogger.Instance;
}
}

internal static bool TryGetInstance([NotNullWhen(true)] out DeferredLogger? instance)
{
instance = Instance;
return instance is not null;
}

private class NullScope : IDisposable
{
public static readonly NullScope Instance = new();

public void Dispose()
{
// No-op
}
}
}
16 changes: 11 additions & 5 deletions src/Elastic.OpenTelemetry.Core/Diagnostics/FileLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@ internal sealed class FileLogger : IDisposable, IAsyncDisposable, ILogger
private readonly CancellationTokenSource _cancellationTokenSource = new();
private readonly StreamWriter _streamWriter;
private readonly LogLevel _configuredLogLevel;
private readonly LoggerExternalScopeProvider _scopeProvider;

private bool _disposed;

public bool FileLoggingEnabled { get; }

private readonly LoggerExternalScopeProvider _scopeProvider;

public FileLogger(CompositeElasticOpenTelemetryOptions options)
{
_scopeProvider = new LoggerExternalScopeProvider();
Expand All @@ -40,8 +39,8 @@ public FileLogger(CompositeElasticOpenTelemetryOptions options)
{
var process = Process.GetCurrentProcess();

// When ordered by filename, we see logs from the same process grouped, then ordered by oldest to newest, then the PID for that instance
var logFileName = $"EDOT.{process.ProcessName}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}_{process.Id}.log";
// This naming resembles the naming structure for OpenTelemetry log files.
var logFileName = $"edot-dotnet-{process.Id}-{process.ProcessName}-{DateTimeOffset.UtcNow:yyyyMMdd-hhMMssfffZ}.log";
var logDirectory = options.LogDirectory;

LogFilePath = Path.Combine(logDirectory, logFileName);
Expand All @@ -50,13 +49,20 @@ public FileLogger(CompositeElasticOpenTelemetryOptions options)
Directory.CreateDirectory(logDirectory);

// StreamWriter.Dispose disposes underlying stream too.
var stream = new FileStream(LogFilePath, FileMode.OpenOrCreate, FileAccess.Write);
var stream = new FileStream(LogFilePath, FileMode.Append, FileAccess.Write, FileShare.Read);

_streamWriter = new StreamWriter(stream, Encoding.UTF8);

_streamWriter.WriteLine("DateTime (UTC) Thread SpanId Level Message");
_streamWriter.WriteLine();

// Drain any deferred log entries captured before the file logger was initialized.
// These appear before the preamble to ensure correct timestamping order.
if (DeferredLogger.TryGetInstance(out var deferredLogger))
Comment thread
JeremyBessonElastic marked this conversation as resolved.
{
deferredLogger.DrainAndRelease(_streamWriter);
}

WritingTask = Task.Run(async () =>
{
var cancellationToken = _cancellationTokenSource.Token;
Expand Down
Loading