Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ActivityEventAttachingLogProcessor to build ActivityEvents from logs #1833

Closed
21 changes: 18 additions & 3 deletions examples/AspNetCore/Controllers/WeatherForecastController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using System.Net.Http;
using Examples.AspNetCore.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace Examples.AspNetCore.Controllers
{
Expand All @@ -32,23 +33,37 @@ public class WeatherForecastController : ControllerBase
"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()
{
// 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,
new { forecast });

return forecast;
}
}
}
18 changes: 18 additions & 0 deletions examples/AspNetCore/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
// </copyright>

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Examples.AspNetCore
{
Expand All @@ -31,6 +33,22 @@ public static IHostBuilder CreateHostBuilder(string[] args) =>
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.ConfigureLogging((context, builder) =>
{
builder.ClearProviders();
builder.AddConsole();

var useLogging = context.Configuration.GetValue<bool>("UseLogging");
if (useLogging)
CodeBlanch marked this conversation as resolved.
Show resolved Hide resolved
{
builder.AddOpenTelemetry(options
=> options.AddActivityEventAttachingLogProcessor(logOptions =>
{
logOptions.IncludeScopes = true;
logOptions.IncludeState = true;
}));
}
});
}
}
1 change: 1 addition & 0 deletions examples/AspNetCore/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
}
},
"AllowedHosts": "*",
"UseLogging": true,
CodeBlanch marked this conversation as resolved.
Show resolved Hide resolved
"UseExporter": "console",
"Jaeger": {
"ServiceName": "jaeger-test",
Expand Down
11 changes: 11 additions & 0 deletions src/OpenTelemetry/.publicApi/net461/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions.ActivityEventAttachingLogProcessorOptions() -> void
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions.IncludeScopes.get -> bool
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions.IncludeScopes.set -> void
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions.IncludeState.get -> bool
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions.IncludeState.set -> void
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions.ScopeConverter.get -> System.Action<System.Diagnostics.ActivityTagsCollection, int, object>
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions.ScopeConverter.set -> void
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions.StateConverter.get -> System.Action<System.Diagnostics.ActivityTagsCollection, object>
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions.StateConverter.set -> void
static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.AddActivityEventAttachingLogProcessor(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions loggerOptions, System.Action<OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions> configure = null) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions.ActivityEventAttachingLogProcessorOptions() -> void
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions.IncludeScopes.get -> bool
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions.IncludeScopes.set -> void
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions.IncludeState.get -> bool
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions.IncludeState.set -> void
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions.ScopeConverter.get -> System.Action<System.Diagnostics.ActivityTagsCollection, int, object>
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions.ScopeConverter.set -> void
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions.StateConverter.get -> System.Action<System.Diagnostics.ActivityTagsCollection, object>
OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions.StateConverter.set -> void
static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.AddActivityEventAttachingLogProcessor(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions loggerOptions, System.Action<OpenTelemetry.Logs.ActivityEventAttachingLogProcessorOptions> configure = null) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions
15 changes: 15 additions & 0 deletions src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ public void MetricControllerException(Exception ex)
}
}

[NonEvent]
public void LogProcessorException(string @event, Exception ex)
{
if (this.IsEnabled(EventLevel.Error, (EventKeywords)(-1)))
{
this.LogProcessorException(@event, ex.ToInvariantString());
}
}

[NonEvent]
public void ActivityStarted(Activity activity)
{
Expand Down Expand Up @@ -287,6 +296,12 @@ public void TracerProviderException(string evnt, string ex)
this.WriteEvent(28, evnt, ex);
}

[Event(29, Message = "Unknown error in LogProcessor event '{0}': '{1}'.", Level = EventLevel.Error)]
public void LogProcessorException(string @event, string exception)
{
this.WriteEvent(29, @event, exception);
}

#if DEBUG
public class OpenTelemetryEventListener : EventListener
{
Expand Down
117 changes: 117 additions & 0 deletions src/OpenTelemetry/Logs/ActivityEventAttachingLogProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// <copyright file="ActivityEventAttachingLogProcessor.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.Diagnostics;
using OpenTelemetry.Internal;
using OpenTelemetry.Trace;

namespace OpenTelemetry.Logs
{
internal class ActivityEventAttachingLogProcessor : BaseProcessor<LogRecord>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm struggling with this name, meanwhile I don't have a good proposal...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya I know it's a mouthful. Open to suggestions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

open-telemetry/opentelemetry-specification#1461
This PR suggests that span events are conceptually the same as logs.

I guess we could consider something like this?

  • TracerProvider.LogRecordToActivityEventProcessor() (inherited from LogProcessor): Converts LogRecord to ActivityEvent, and uses Activity.AddEvent to attach it to the current Activity. If there there is no "current activity", this processor will simply ignore the log record.
  • LoggerProvider.ActivityEventToLogRecordProcessor(logger) (inherited from ActivityProcessor): Converts the ActivityEvents objects to LogRecords.

{
private static readonly Action<object, State> ProcessScopeRef = ProcessScope;

private readonly ActivityEventAttachingLogProcessorOptions options;

public ActivityEventAttachingLogProcessor(ActivityEventAttachingLogProcessorOptions options)
{
this.options = options ?? throw new ArgumentNullException(nameof(options));
}

public override void OnEnd(LogRecord data)
{
Activity activity = Activity.Current;

if (activity?.IsAllDataRequested == true)
{
var tags = new ActivityTagsCollection
{
{ nameof(data.CategoryName), data.CategoryName },
{ nameof(data.LogLevel), data.LogLevel },
};

if (data.EventId != 0)
{
tags[nameof(data.EventId)] = data.EventId;
}

var activityEvent = new ActivityEvent("log", data.Timestamp, tags);

if (this.options.IncludeScopes)
{
data.ForEachScope(
ProcessScopeRef,
new State
{
Tags = tags,
Processor = this,
});
}

if (data.State != null)
{
if (this.options.IncludeState)
{
try
{
this.options.StateConverter?.Invoke(tags, data.State);
}
catch (Exception ex)
{
OpenTelemetrySdkEventSource.Log.LogProcessorException($"Processing state of type [{data.State.GetType().FullName}]", ex);
}
}
else
{
tags["Message"] = data.State.ToString();
}
}

activity.AddEvent(activityEvent);

if (data.Exception != null)
{
activity.RecordException(data.Exception);
}
}
}

private static void ProcessScope(object scope, State state)
{
if (scope != null)
{
try
{
state.Processor.options.ScopeConverter?.Invoke(state.Tags, state.Index++, scope);
}
catch (Exception ex)
{
OpenTelemetrySdkEventSource.Log.LogProcessorException($"Processing scope of type [{scope.GetType().FullName}]", ex);
}
}
}

private class State
{
public ActivityTagsCollection Tags;
public int Index;
public ActivityEventAttachingLogProcessor Processor;
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// <copyright file="ActivityEventAttachingLogProcessorOptions.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.Diagnostics;

namespace OpenTelemetry.Logs
{
public class ActivityEventAttachingLogProcessorOptions
{
/// <summary>
/// Gets or sets a value indicating whether or not log scopes should be included on generated <see cref="ActivityEvent"/>s. Default value: False.
/// </summary>
public bool IncludeScopes { get; set; }

/// <summary>
/// Gets or sets a value indicating whether or not log state should be included on generated <see cref="ActivityEvent"/>s. Default value: False.
/// </summary>
public bool IncludeState { get; set; }

/// <summary>
/// Gets or sets the callback action used to convert log state to tags.
/// </summary>
public Action<ActivityTagsCollection, object> StateConverter { get; set; } = DefaultLogStateConverter.ConvertState;

/// <summary>
/// Gets or sets the callback action used to convert log scopes to tags.
/// </summary>
public Action<ActivityTagsCollection, int, object> ScopeConverter { get; set; } = DefaultLogStateConverter.ConvertScope;
}
}
#endif
Loading