Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
101 changes: 101 additions & 0 deletions src/Elastic.OpenTelemetry.Core/Diagnostics/DeferredLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// 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;

/// <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);
_configuredLogLevel = options.LogLevel;
Comment thread
JeremyBessonElastic marked this conversation as resolved.
Outdated

if (!_isEnabled)
return;

_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)
{
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);

_additionalLogger?.Log(logLevel, eventId, state, exception, formatter);
}

internal void DrainLogQueue(StreamWriter streamWriter)
{
while (_logQueue.TryDequeue(out var deferredLog))
{
streamWriter.WriteLine(deferredLog);
}
}

private static DeferredLogger? Instance;

internal static ILogger GetOrCreate(CompositeElasticOpenTelemetryOptions options)
{
// 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;
}

internal static void ReleaseInstance() => Instance = null;

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

public void Dispose()
{
// No-op
}
}
}
17 changes: 12 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,21 @@ 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.DrainLogQueue(_streamWriter);
DeferredLogger.ReleaseInstance();
}

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