Browse Source
- more modular - nullability - supports arbitrary filtering instead of just PIDdavidfowl/ingress-changes
18 changed files with 1088 additions and 821 deletions
@ -0,0 +1,80 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
|
||||
|
namespace Microsoft.Tye |
||||
|
{ |
||||
|
public class DiagnosticsProvider |
||||
|
{ |
||||
|
public static readonly IReadOnlyDictionary<string, WellKnownProvider> WellKnownProviders = new Dictionary<string, WellKnownProvider>() |
||||
|
{ |
||||
|
{ "ai", new WellKnownProvider("ai", ProviderKind.Logging, "logs: Using ApplicationInsights instrumentation key {InstrumentationKey}") }, |
||||
|
{ "elastic", new WellKnownProvider("elastic", ProviderKind.Logging, "logs: Using ElasticSearch at {URL}") }, |
||||
|
{ "console", new WellKnownProvider("console", ProviderKind.Logging, "logs: Using console logs") }, |
||||
|
{ "seq", new WellKnownProvider("seq", ProviderKind.Logging, "logs: Using Seq at {URL}") }, |
||||
|
|
||||
|
{ "zipkin", new WellKnownProvider("zipkin", ProviderKind.Tracing, "dtrace: Using Zipkin at URL {URL}") }, |
||||
|
}; |
||||
|
|
||||
|
public static bool TryParse(string text, ProviderKind kind, [MaybeNullWhen(false)] out DiagnosticsProvider provider) |
||||
|
{ |
||||
|
if (string.IsNullOrEmpty(text)) |
||||
|
{ |
||||
|
provider = null!; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
var pair = text.Split('='); |
||||
|
if (pair.Length < 2) |
||||
|
{ |
||||
|
provider = new DiagnosticsProvider(pair[0].Trim().ToLowerInvariant(), null, kind); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
provider = new DiagnosticsProvider(pair[0].Trim().ToLowerInvariant(), pair[1].Trim().ToLowerInvariant(), kind); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public DiagnosticsProvider(string key, string? value, ProviderKind kind) |
||||
|
{ |
||||
|
Key = key; |
||||
|
Value = value; |
||||
|
Kind = kind; |
||||
|
} |
||||
|
|
||||
|
public string Key { get; } |
||||
|
|
||||
|
public bool HasValue => !string.IsNullOrEmpty(Value); |
||||
|
|
||||
|
public string? Value { get; } |
||||
|
|
||||
|
public ProviderKind Kind { get; } |
||||
|
|
||||
|
public enum ProviderKind |
||||
|
{ |
||||
|
Logging, |
||||
|
Metrics, |
||||
|
Tracing, |
||||
|
Unknown, |
||||
|
} |
||||
|
|
||||
|
public class WellKnownProvider |
||||
|
{ |
||||
|
public WellKnownProvider(string key, ProviderKind kind, string logFormat) |
||||
|
{ |
||||
|
Key = key; |
||||
|
Kind = kind; |
||||
|
LogFormat = logFormat; |
||||
|
} |
||||
|
|
||||
|
public string Key { get; } |
||||
|
|
||||
|
public ProviderKind Kind { get; } |
||||
|
|
||||
|
public string LogFormat { get; } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,236 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text.Json; |
||||
|
using Microsoft.Diagnostics.Tracing; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Microsoft.Tye.Hosting.Diagnostics.Logging; |
||||
|
using Serilog; |
||||
|
using static Microsoft.Tye.Hosting.Diagnostics.WellKnownEventSources; |
||||
|
|
||||
|
namespace Microsoft.Tye.Hosting.Diagnostics |
||||
|
{ |
||||
|
public class LoggingSink |
||||
|
{ |
||||
|
private readonly Microsoft.Extensions.Logging.ILogger _logger; |
||||
|
private readonly DiagnosticsProvider _provider; |
||||
|
|
||||
|
public LoggingSink(Microsoft.Extensions.Logging.ILogger logger, DiagnosticsProvider provider) |
||||
|
{ |
||||
|
_logger = logger; |
||||
|
_provider = provider; |
||||
|
} |
||||
|
|
||||
|
public IDisposable Attach(EventPipeEventSource source, ReplicaInfo replicaInfo) |
||||
|
{ |
||||
|
using var loggerFactory = LoggerFactory.Create(builder => ConfigureLogging(replicaInfo.Service, replicaInfo.Replica, builder)); |
||||
|
|
||||
|
var lastFormattedMessage = ""; |
||||
|
|
||||
|
var logActivities = new Dictionary<Guid, LogActivityItem>(); |
||||
|
var stack = new Stack<Guid>(); |
||||
|
|
||||
|
source.Dynamic.AddCallbackForProviderEvent(MicrosoftExtensionsLoggingProviderName, "ActivityJsonStart/Start", (traceEvent) => |
||||
|
{ |
||||
|
var factoryId = (int)traceEvent.PayloadByName("FactoryID"); |
||||
|
var categoryName = (string)traceEvent.PayloadByName("LoggerName"); |
||||
|
var argsJson = (string)traceEvent.PayloadByName("ArgumentsJson"); |
||||
|
|
||||
|
// TODO: Store this information by logger factory id
|
||||
|
var item = new LogActivityItem(traceEvent.ActivityID, new LogObject(JsonDocument.Parse(argsJson).RootElement)); |
||||
|
if (stack.TryPeek(out var parentId) && logActivities.TryGetValue(parentId, out var parentItem)) |
||||
|
{ |
||||
|
item.Parent = parentItem; |
||||
|
} |
||||
|
|
||||
|
stack.Push(traceEvent.ActivityID); |
||||
|
logActivities[traceEvent.ActivityID] = item; |
||||
|
}); |
||||
|
|
||||
|
source.Dynamic.AddCallbackForProviderEvent(MicrosoftExtensionsLoggingProviderName, "ActivityJsonStop/Stop", (traceEvent) => |
||||
|
{ |
||||
|
var factoryId = (int)traceEvent.PayloadByName("FactoryID"); |
||||
|
var categoryName = (string)traceEvent.PayloadByName("LoggerName"); |
||||
|
|
||||
|
stack.Pop(); |
||||
|
logActivities.Remove(traceEvent.ActivityID); |
||||
|
}); |
||||
|
|
||||
|
source.Dynamic.AddCallbackForProviderEvent(MicrosoftExtensionsLoggingProviderName, "MessageJson", (traceEvent) => |
||||
|
{ |
||||
|
// Level, FactoryID, LoggerName, EventID, EventName, ExceptionJson, ArgumentsJson
|
||||
|
var logLevel = (LogLevel)traceEvent.PayloadByName("Level"); |
||||
|
var factoryId = (int)traceEvent.PayloadByName("FactoryID"); |
||||
|
var categoryName = (string)traceEvent.PayloadByName("LoggerName"); |
||||
|
var eventId = (int)traceEvent.PayloadByName("EventId"); |
||||
|
var eventName = (string)traceEvent.PayloadByName("EventName"); |
||||
|
var exceptionJson = (string)traceEvent.PayloadByName("ExceptionJson"); |
||||
|
var argsJson = (string)traceEvent.PayloadByName("ArgumentsJson"); |
||||
|
|
||||
|
// There's a bug that causes some of the columns to get mixed up
|
||||
|
if (eventName.StartsWith("{")) |
||||
|
{ |
||||
|
argsJson = exceptionJson; |
||||
|
exceptionJson = eventName; |
||||
|
eventName = null; |
||||
|
} |
||||
|
|
||||
|
if (string.IsNullOrEmpty(argsJson)) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
Exception? exception = null; |
||||
|
|
||||
|
var logger = loggerFactory.CreateLogger(categoryName); |
||||
|
|
||||
|
var scopes = new List<IDisposable>(); |
||||
|
|
||||
|
if (logActivities.TryGetValue(traceEvent.ActivityID, out var logActivityItem)) |
||||
|
{ |
||||
|
// REVIEW: Does order matter here? We're combining everything anyways.
|
||||
|
while (logActivityItem != null) |
||||
|
{ |
||||
|
scopes.Add(logger.BeginScope(logActivityItem.ScopedObject)); |
||||
|
|
||||
|
logActivityItem = logActivityItem.Parent; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
if (exceptionJson != "{}") |
||||
|
{ |
||||
|
var exceptionMessage = JsonSerializer.Deserialize<JsonElement>(exceptionJson); |
||||
|
exception = new LoggerException(exceptionMessage); |
||||
|
} |
||||
|
|
||||
|
var message = JsonSerializer.Deserialize<JsonElement>(argsJson); |
||||
|
if (message.TryGetProperty("{OriginalFormat}", out var formatElement)) |
||||
|
{ |
||||
|
var formatString = formatElement.GetString(); |
||||
|
var formatter = new LogValuesFormatter(formatString); |
||||
|
var args = new object[formatter.ValueNames.Count]; |
||||
|
for (var i = 0; i < args.Length; i++) |
||||
|
{ |
||||
|
if (!message.TryGetProperty(formatter.ValueNames[i], out var argValue)) |
||||
|
{ |
||||
|
// We couldn't find the parsed property in the original message, it's likely that this is a JSON object or something else
|
||||
|
// being logged, or some other format that just looks like the formatted message. Stop here and log the formatted message
|
||||
|
var obj = new LogObject(message, lastFormattedMessage); |
||||
|
logger.Log(logLevel, new EventId(eventId, eventName), obj, exception, LogObject.Callback); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
args[i] = argValue.GetString(); |
||||
|
} |
||||
|
|
||||
|
logger.Log(logLevel, new EventId(eventId, eventName), exception, formatString, args); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var obj = new LogObject(message, lastFormattedMessage); |
||||
|
logger.Log(logLevel, new EventId(eventId, eventName), obj, exception, LogObject.Callback); |
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
_logger.LogDebug(ex, "Error processing log entry for {ServiceName}", replicaInfo.Replica); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
scopes.ForEach(d => d.Dispose()); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
source.Dynamic.AddCallbackForProviderEvent(MicrosoftExtensionsLoggingProviderName, "FormattedMessage", (traceEvent) => |
||||
|
{ |
||||
|
// Level, FactoryID, LoggerName, EventID, EventName, FormattedMessage
|
||||
|
var logLevel = (LogLevel)traceEvent.PayloadByName("Level"); |
||||
|
var factoryId = (int)traceEvent.PayloadByName("FactoryID"); |
||||
|
var categoryName = (string)traceEvent.PayloadByName("LoggerName"); |
||||
|
var eventId = (int)traceEvent.PayloadByName("EventId"); |
||||
|
var eventName = (string)traceEvent.PayloadByName("EventName"); |
||||
|
var formattedMessage = (string)traceEvent.PayloadByName("FormattedMessage"); |
||||
|
|
||||
|
if (string.IsNullOrEmpty(formattedMessage)) |
||||
|
{ |
||||
|
formattedMessage = eventName; |
||||
|
eventName = ""; |
||||
|
} |
||||
|
|
||||
|
lastFormattedMessage = formattedMessage; |
||||
|
}); |
||||
|
|
||||
|
return loggerFactory; // the logger factory will be cleaned up when collection ends.
|
||||
|
} |
||||
|
|
||||
|
// This is the logger factory for application logs. It allows re-routing event pipe collected logs (structured logs)
|
||||
|
// to any of the supported sinks, currently (elastic search and app insights)
|
||||
|
private void ConfigureLogging(string serviceName, string replicaName, ILoggingBuilder builder) |
||||
|
{ |
||||
|
if (string.Equals(_provider.Key, "elastic", StringComparison.OrdinalIgnoreCase) && |
||||
|
!string.IsNullOrEmpty(_provider.Value)) |
||||
|
{ |
||||
|
var loggerConfiguration = new LoggerConfiguration() |
||||
|
.Enrich.WithProperty("Application", serviceName) |
||||
|
.Enrich.WithProperty("Instance", replicaName) |
||||
|
.Enrich.FromLogContext() |
||||
|
.WriteTo.Elasticsearch(_provider.Value); |
||||
|
|
||||
|
builder.AddSerilog(loggerConfiguration.CreateLogger()); |
||||
|
} |
||||
|
|
||||
|
if (string.Equals(_provider.Key, "console", StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
var loggerConfiguration = new LoggerConfiguration() |
||||
|
.Enrich.WithProperty("Application", serviceName) |
||||
|
.Enrich.WithProperty("Instance", replicaName) |
||||
|
.Enrich.FromLogContext() |
||||
|
.WriteTo.Console(outputTemplate: "[{Instance}]: [{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"); |
||||
|
|
||||
|
builder.AddSerilog(loggerConfiguration.CreateLogger()); |
||||
|
} |
||||
|
|
||||
|
if (string.Equals(_provider.Key, "seq", StringComparison.OrdinalIgnoreCase) && |
||||
|
!string.IsNullOrEmpty(_provider.Value)) |
||||
|
{ |
||||
|
var loggerConfiguration = new LoggerConfiguration() |
||||
|
.Enrich.WithProperty("Application", serviceName) |
||||
|
.Enrich.WithProperty("Instance", replicaName) |
||||
|
.Enrich.FromLogContext() |
||||
|
.WriteTo.Seq(_provider.Value); |
||||
|
|
||||
|
builder.AddSerilog(loggerConfiguration.CreateLogger()); |
||||
|
} |
||||
|
|
||||
|
if (string.Equals(_provider.Key, "ai", StringComparison.OrdinalIgnoreCase) && |
||||
|
!string.IsNullOrEmpty(_provider.Value)) |
||||
|
{ |
||||
|
builder.AddApplicationInsights(_provider.Value); |
||||
|
} |
||||
|
|
||||
|
// REVIEW: How are log levels controlled on the outside?
|
||||
|
builder.SetMinimumLevel(LogLevel.Information); |
||||
|
} |
||||
|
|
||||
|
private class LogActivityItem |
||||
|
{ |
||||
|
public LogActivityItem(Guid activityID, LogObject scopedObject) |
||||
|
{ |
||||
|
ActivityID = activityID; |
||||
|
ScopedObject = scopedObject; |
||||
|
} |
||||
|
|
||||
|
public Guid ActivityID { get; } |
||||
|
|
||||
|
public LogObject ScopedObject { get; } |
||||
|
|
||||
|
public LogActivityItem? Parent { get; set; } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Microsoft.Diagnostics.Tracing; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Microsoft.Tye.Hosting.Diagnostics.Metrics; |
||||
|
|
||||
|
namespace Microsoft.Tye.Hosting.Diagnostics |
||||
|
{ |
||||
|
public class MetricSink |
||||
|
{ |
||||
|
private readonly ILogger _logger; |
||||
|
|
||||
|
public MetricSink(ILogger logger) |
||||
|
{ |
||||
|
_logger = logger; |
||||
|
} |
||||
|
|
||||
|
public IDisposable Attach(EventPipeEventSource source, ReplicaInfo replicaInfo) |
||||
|
{ |
||||
|
var store = replicaInfo.Metrics; |
||||
|
|
||||
|
source.Dynamic.All += traceEvent => |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
// Metrics
|
||||
|
if (traceEvent.EventName.Equals("EventCounters")) |
||||
|
{ |
||||
|
var value = (IDictionary<string, object>)traceEvent.PayloadValue(0); |
||||
|
var eventPayload = (IDictionary<string, object>)value["Payload"]; |
||||
|
|
||||
|
var payload = CounterPayload.FromPayload(eventPayload); |
||||
|
|
||||
|
store[traceEvent.ProviderName + "/" + payload.Name] = payload.Value; |
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
_logger.LogError(ex, "Error processing counter for {ProviderName}:{EventName}", traceEvent.ProviderName, traceEvent.EventName); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
return new NullDisposable(); |
||||
|
} |
||||
|
|
||||
|
private class NullDisposable : IDisposable |
||||
|
{ |
||||
|
public void Dispose() { } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Concurrent; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics; |
||||
|
|
||||
|
namespace Microsoft.Tye.Hosting.Diagnostics |
||||
|
{ |
||||
|
public class ReplicaInfo |
||||
|
{ |
||||
|
public ReplicaInfo( |
||||
|
Func<IReadOnlyList<Process>, Process?> selector, |
||||
|
string assemblyName, |
||||
|
string service, |
||||
|
string replica, |
||||
|
ConcurrentDictionary<string, string> metrics) |
||||
|
{ |
||||
|
Selector = selector; |
||||
|
AssemblyName = assemblyName; |
||||
|
Service = service; |
||||
|
Replica = replica; |
||||
|
Metrics = metrics; |
||||
|
} |
||||
|
|
||||
|
public Func<IReadOnlyList<Process>, Process?> Selector { get; } |
||||
|
|
||||
|
public string AssemblyName { get; } |
||||
|
|
||||
|
public string Service { get; } |
||||
|
|
||||
|
public string Replica { get; } |
||||
|
|
||||
|
// TODO - this isn't a great way to pass metrics around.
|
||||
|
public ConcurrentDictionary<string, string> Metrics { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,371 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.Linq; |
||||
|
using Microsoft.Diagnostics.Tracing; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using OpenTelemetry.Exporter.Zipkin; |
||||
|
using OpenTelemetry.Trace; |
||||
|
using OpenTelemetry.Trace.Export; |
||||
|
|
||||
|
namespace Microsoft.Tye.Hosting.Diagnostics |
||||
|
{ |
||||
|
public class TracingSink |
||||
|
{ |
||||
|
private readonly ILogger _logger; |
||||
|
private readonly DiagnosticsProvider _provider; |
||||
|
|
||||
|
public TracingSink(ILogger logger, DiagnosticsProvider provider) |
||||
|
{ |
||||
|
_logger = logger; |
||||
|
_provider = provider; |
||||
|
} |
||||
|
|
||||
|
public IDisposable Attach(EventPipeEventSource source, ReplicaInfo replicaInfo) |
||||
|
{ |
||||
|
var exporter = CreateSpanExporter(replicaInfo); |
||||
|
if (exporter is null) |
||||
|
{ |
||||
|
return new NullDisposable(); |
||||
|
} |
||||
|
|
||||
|
var processor = new SimpleSpanProcessor(exporter); |
||||
|
var activities = new Dictionary<string, ActivityItem>(); |
||||
|
|
||||
|
source.Dynamic.All += traceEvent => |
||||
|
{ |
||||
|
|
||||
|
if (traceEvent.EventName == "Activity1Start/Start") |
||||
|
{ |
||||
|
var listenerEventName = (string)traceEvent.PayloadByName("EventName"); |
||||
|
|
||||
|
if (traceEvent.PayloadByName("Arguments") is IDictionary<string, object>[] arguments) |
||||
|
{ |
||||
|
if (TryCreateActivity(arguments, out var item)) |
||||
|
{ |
||||
|
string? method = null; |
||||
|
string? path = null; |
||||
|
string? host = null; |
||||
|
string? pathBase = null; |
||||
|
string? queryString = null; |
||||
|
string? scheme = null; |
||||
|
|
||||
|
foreach (var arg in arguments) |
||||
|
{ |
||||
|
var key = (string)arg["Key"]; |
||||
|
var value = (string)arg["Value"]; |
||||
|
|
||||
|
if (key == "Path") |
||||
|
{ |
||||
|
path = value; |
||||
|
} |
||||
|
else if (key == "Method") |
||||
|
{ |
||||
|
method = value; |
||||
|
} |
||||
|
else if (key == "Host") |
||||
|
{ |
||||
|
host = value; |
||||
|
} |
||||
|
else if (key == "PathBase") |
||||
|
{ |
||||
|
pathBase = value; |
||||
|
} |
||||
|
else if (key == "Scheme") |
||||
|
{ |
||||
|
scheme = value; |
||||
|
} |
||||
|
else if (key == "QueryString") |
||||
|
{ |
||||
|
queryString = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
item.Name = path; |
||||
|
item.Kind = SpanKind.Server; |
||||
|
item.Attributes[SpanAttributeConstants.HttpUrlKey] = scheme + "://" + host + pathBase + path + queryString; |
||||
|
item.Attributes[SpanAttributeConstants.HttpMethodKey] = method; |
||||
|
item.Attributes[SpanAttributeConstants.HttpPathKey] = path; |
||||
|
|
||||
|
activities[item.Id] = item; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (traceEvent.EventName == "Activity1Stop/Stop") |
||||
|
{ |
||||
|
var listenerEventName = (string)traceEvent.PayloadByName("EventName"); |
||||
|
|
||||
|
if (traceEvent.PayloadByName("Arguments") is IDictionary<string, object>[] arguments) |
||||
|
{ |
||||
|
var (activityId, duration) = GetActivityStop(arguments); |
||||
|
|
||||
|
var statusCode = 0; |
||||
|
|
||||
|
foreach (var arg in arguments) |
||||
|
{ |
||||
|
var key = (string)arg["Key"]; |
||||
|
var value = (string)arg["Value"]; |
||||
|
|
||||
|
if (key == "StatusCode") |
||||
|
{ |
||||
|
statusCode = int.Parse(value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (activityId != null && activities.TryGetValue(activityId, out var item)) |
||||
|
{ |
||||
|
item.Attributes[SpanAttributeConstants.HttpStatusCodeKey] = statusCode; |
||||
|
|
||||
|
item.EndTime = item.StartTime + duration; |
||||
|
|
||||
|
var spanData = new SpanData(item.Name, |
||||
|
new SpanContext(item.TraceId, item.SpanId, ActivityTraceFlags.Recorded), |
||||
|
item.ParentSpanId, |
||||
|
item.Kind, |
||||
|
item.StartTime, |
||||
|
item.Attributes, |
||||
|
Enumerable.Empty<Event>(), |
||||
|
Enumerable.Empty<Link>(), |
||||
|
null, |
||||
|
Status.Ok, |
||||
|
item.EndTime); |
||||
|
|
||||
|
processor.OnEnd(spanData); |
||||
|
|
||||
|
activities.Remove(activityId); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
if (traceEvent.EventName == "Activity2Start/Start") |
||||
|
{ |
||||
|
var listenerEventName = (string)traceEvent.PayloadByName("EventName"); |
||||
|
|
||||
|
if (traceEvent.PayloadByName("Arguments") is IDictionary<string, object>[] arguments) |
||||
|
{ |
||||
|
string? uri = null; |
||||
|
string? method = null; |
||||
|
|
||||
|
foreach (var arg in arguments) |
||||
|
{ |
||||
|
var key = (string)arg["Key"]; |
||||
|
var value = (string)arg["Value"]; |
||||
|
|
||||
|
if (key == "RequestUri") |
||||
|
{ |
||||
|
uri = value; |
||||
|
} |
||||
|
else if (key == "Method") |
||||
|
{ |
||||
|
method = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (TryCreateActivity(arguments, out var item)) |
||||
|
{ |
||||
|
item.Name = uri; |
||||
|
item.Kind = SpanKind.Client; |
||||
|
|
||||
|
item.Attributes[SpanAttributeConstants.HttpUrlKey] = uri; |
||||
|
item.Attributes[SpanAttributeConstants.HttpMethodKey] = method; |
||||
|
|
||||
|
activities[item.Id] = item; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
if (traceEvent.EventName == "Activity2Stop/Stop") |
||||
|
{ |
||||
|
var listenerEventName = (string)traceEvent.PayloadByName("EventName"); |
||||
|
|
||||
|
if (traceEvent.PayloadByName("Arguments") is IDictionary<string, object>[] arguments) |
||||
|
{ |
||||
|
var (activityId, duration) = GetActivityStop(arguments); |
||||
|
|
||||
|
if (activityId != null && activities.TryGetValue(activityId, out var item)) |
||||
|
{ |
||||
|
item.EndTime = item.StartTime + duration; |
||||
|
|
||||
|
var spanData = new SpanData( |
||||
|
item.Name, |
||||
|
new SpanContext(item.TraceId, item.SpanId, ActivityTraceFlags.Recorded), |
||||
|
item.ParentSpanId, |
||||
|
item.Kind, |
||||
|
item.StartTime, |
||||
|
item.Attributes, |
||||
|
Enumerable.Empty<Event>(), |
||||
|
Enumerable.Empty<Link>(), |
||||
|
null, |
||||
|
Status.Ok, |
||||
|
item.EndTime); |
||||
|
|
||||
|
processor.OnEnd(spanData); |
||||
|
|
||||
|
activities.Remove(activityId); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
return new NullDisposable(); |
||||
|
} |
||||
|
|
||||
|
private static bool TryCreateActivity(IDictionary<string, object>[] arguments, [MaybeNullWhen(false)] out ActivityItem item) |
||||
|
{ |
||||
|
string? activityId = null; |
||||
|
string? operationName = null; |
||||
|
string? spanId = null; |
||||
|
string? parentSpanId = null; |
||||
|
string? traceId = null; |
||||
|
DateTime startTime = default; |
||||
|
ActivityIdFormat idFormat = default; |
||||
|
|
||||
|
foreach (var arg in arguments) |
||||
|
{ |
||||
|
var key = (string)arg["Key"]; |
||||
|
var value = (string)arg["Value"]; |
||||
|
|
||||
|
if (key == "ActivityId") |
||||
|
{ |
||||
|
activityId = value; |
||||
|
} |
||||
|
else if (key == "ActivityOperationName") |
||||
|
{ |
||||
|
operationName = value; |
||||
|
} |
||||
|
else if (key == "ActivitySpanId") |
||||
|
{ |
||||
|
spanId = value; |
||||
|
} |
||||
|
else if (key == "ActivityTraceId") |
||||
|
{ |
||||
|
traceId = value; |
||||
|
} |
||||
|
else if (key == "ActivityParentSpanId") |
||||
|
{ |
||||
|
parentSpanId = value; |
||||
|
} |
||||
|
else if (key == "ActivityStartTime") |
||||
|
{ |
||||
|
startTime = new DateTime(long.Parse(value), DateTimeKind.Utc); |
||||
|
} |
||||
|
else if (key == "ActivityIdFormat") |
||||
|
{ |
||||
|
idFormat = Enum.Parse<ActivityIdFormat>(value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (string.IsNullOrEmpty(activityId)) |
||||
|
{ |
||||
|
// Not a 3.1 application (we can detect this earlier)
|
||||
|
item = null!; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (idFormat == ActivityIdFormat.Hierarchical) |
||||
|
{ |
||||
|
// We need W3C to make it work
|
||||
|
item = null!; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// This is what open telemetry currently does
|
||||
|
// https://github.com/open-telemetry/opentelemetry-dotnet/blob/4ba732af062ddc2759c02aebbc91335aaa3f7173/src/OpenTelemetry.Collector.AspNetCore/Implementation/HttpInListener.cs#L65-L92
|
||||
|
|
||||
|
item = new ActivityItem(activityId) |
||||
|
{ |
||||
|
Name = operationName, |
||||
|
SpanId = ActivitySpanId.CreateFromString(spanId), |
||||
|
TraceId = ActivityTraceId.CreateFromString(traceId), |
||||
|
ParentSpanId = parentSpanId == "0000000000000000" ? default : ActivitySpanId.CreateFromString(parentSpanId), |
||||
|
StartTime = startTime, |
||||
|
}; |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private static (string? ActivityId, TimeSpan Duration) GetActivityStop(IDictionary<string, object>[] arguments) |
||||
|
{ |
||||
|
var activityId = (string?)null; |
||||
|
var duration = default(TimeSpan); |
||||
|
|
||||
|
foreach (var arg in arguments) |
||||
|
{ |
||||
|
var key = (string)arg["Key"]; |
||||
|
var value = (string)arg["Value"]; |
||||
|
|
||||
|
if (key == "ActivityId") |
||||
|
{ |
||||
|
activityId = value; |
||||
|
} |
||||
|
else if (key == "ActivityDuration") |
||||
|
{ |
||||
|
duration = new TimeSpan(long.Parse(value)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return (activityId, duration); |
||||
|
} |
||||
|
|
||||
|
private SpanExporter? CreateSpanExporter(ReplicaInfo replicaInfo) |
||||
|
{ |
||||
|
if (string.Equals(_provider.Key, "zipkin", StringComparison.OrdinalIgnoreCase) && |
||||
|
!string.IsNullOrEmpty(_provider.Value)) |
||||
|
{ |
||||
|
var zipkin = new ZipkinTraceExporter(new ZipkinTraceExporterOptions() |
||||
|
{ |
||||
|
ServiceName = replicaInfo.Service, |
||||
|
Endpoint = new Uri($"{_provider.Value.TrimEnd('/')}/api/v2/spans") |
||||
|
}); |
||||
|
|
||||
|
return zipkin; |
||||
|
} |
||||
|
|
||||
|
// TODO: Support Jaegar
|
||||
|
// TODO: Support ApplicationInsights
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
private class ActivityItem |
||||
|
{ |
||||
|
public ActivityItem(string id) |
||||
|
{ |
||||
|
Id = id; |
||||
|
} |
||||
|
|
||||
|
public string Id { get; } |
||||
|
public string? Name { get; set; } |
||||
|
public ActivityTraceId TraceId { get; set; } |
||||
|
public ActivitySpanId SpanId { get; set; } |
||||
|
public Dictionary<string, object?> Attributes { get; } = new Dictionary<string, object?>(); |
||||
|
public DateTime StartTime { get; set; } |
||||
|
public DateTime EndTime { get; set; } |
||||
|
public SpanKind Kind { get; set; } |
||||
|
|
||||
|
public ActivitySpanId ParentSpanId { get; set; } |
||||
|
} |
||||
|
|
||||
|
internal static class SpanAttributeConstants |
||||
|
{ |
||||
|
public static readonly string ComponentKey = "component"; |
||||
|
|
||||
|
public static readonly string HttpMethodKey = "http.method"; |
||||
|
public static readonly string HttpStatusCodeKey = "http.status_code"; |
||||
|
public static readonly string HttpUserAgentKey = "http.user_agent"; |
||||
|
public static readonly string HttpPathKey = "http.path"; |
||||
|
public static readonly string HttpHostKey = "http.host"; |
||||
|
public static readonly string HttpUrlKey = "http.url"; |
||||
|
public static readonly string HttpRouteKey = "http.route"; |
||||
|
public static readonly string HttpFlavorKey = "http.flavor"; |
||||
|
} |
||||
|
|
||||
|
private class NullDisposable : IDisposable |
||||
|
{ |
||||
|
public void Dispose() { } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,139 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics.Tracing; |
||||
|
using Microsoft.Diagnostics.NETCore.Client; |
||||
|
using Microsoft.Diagnostics.Tracing.Parsers; |
||||
|
|
||||
|
namespace Microsoft.Tye.Hosting.Diagnostics |
||||
|
{ |
||||
|
internal static class WellKnownEventSources |
||||
|
{ |
||||
|
// This list of event sources needs to be extensible
|
||||
|
public static readonly string MicrosoftExtensionsLoggingProviderName = "Microsoft-Extensions-Logging"; |
||||
|
public static readonly string SystemRuntimeEventSourceName = "System.Runtime"; |
||||
|
public static readonly string MicrosoftAspNetCoreHostingEventSourceName = "Microsoft.AspNetCore.Hosting"; |
||||
|
public static readonly string GrpcAspNetCoreServer = "Grpc.AspNetCore.Server"; |
||||
|
public static readonly string DiagnosticSourceEventSource = "Microsoft-Diagnostics-DiagnosticSource"; |
||||
|
public static readonly string TplEventSource = "System.Threading.Tasks.TplEventSource"; |
||||
|
|
||||
|
// This is the list of events for distributed tracing
|
||||
|
public static readonly string DiagnosticFilterString = "\"" + |
||||
|
"Microsoft.AspNetCore/Microsoft.AspNetCore.Hosting.HttpRequestIn.Start@Activity1Start:-" + |
||||
|
"Request.Scheme" + |
||||
|
";Request.Host" + |
||||
|
";Request.PathBase" + |
||||
|
";Request.QueryString" + |
||||
|
";Request.Path" + |
||||
|
";Request.Method" + |
||||
|
";ActivityStartTime=*Activity.StartTimeUtc.Ticks" + |
||||
|
";ActivityParentId=*Activity.ParentId" + |
||||
|
";ActivityId=*Activity.Id" + |
||||
|
";ActivitySpanId=*Activity.SpanId" + |
||||
|
";ActivityTraceId=*Activity.TraceId" + |
||||
|
";ActivityParentSpanId=*Activity.ParentSpanId" + |
||||
|
";ActivityIdFormat=*Activity.IdFormat" + |
||||
|
"\r\n" + |
||||
|
"Microsoft.AspNetCore/Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop@Activity1Stop:-" + |
||||
|
"Response.StatusCode" + |
||||
|
";ActivityDuration=*Activity.Duration.Ticks" + |
||||
|
";ActivityId=*Activity.Id" + |
||||
|
"\r\n" + |
||||
|
"HttpHandlerDiagnosticListener/System.Net.Http.HttpRequestOut@Event:-" + |
||||
|
"\r\n" + |
||||
|
"HttpHandlerDiagnosticListener/System.Net.Http.HttpRequestOut.Start@Activity2Start:-" + |
||||
|
"Request.RequestUri" + |
||||
|
";Request.Method" + |
||||
|
";Request.RequestUri.Host" + |
||||
|
";Request.RequestUri.Port" + |
||||
|
";ActivityStartTime=*Activity.StartTimeUtc.Ticks" + |
||||
|
";ActivityId=*Activity.Id" + |
||||
|
";ActivitySpanId=*Activity.SpanId" + |
||||
|
";ActivityTraceId=*Activity.TraceId" + |
||||
|
";ActivityParentSpanId=*Activity.ParentSpanId" + |
||||
|
";ActivityIdFormat=*Activity.IdFormat" + |
||||
|
";ActivityId=*Activity.Id" + |
||||
|
"\r\n" + |
||||
|
"HttpHandlerDiagnosticListener/System.Net.Http.HttpRequestOut.Stop@Activity2Stop:-" + |
||||
|
";ActivityDuration=*Activity.Duration.Ticks" + |
||||
|
";ActivityId=*Activity.Id" + |
||||
|
"\r\n" + |
||||
|
|
||||
|
"\""; |
||||
|
|
||||
|
public static List<EventPipeProvider> CreateDefaultProviders() |
||||
|
{ |
||||
|
return new List<EventPipeProvider>() |
||||
|
{ |
||||
|
// Runtime Metrics
|
||||
|
CreateStandardProvider(SystemRuntimeEventSourceName), |
||||
|
CreateStandardProvider(MicrosoftAspNetCoreHostingEventSourceName), |
||||
|
CreateStandardProvider(GrpcAspNetCoreServer), |
||||
|
|
||||
|
// Logging
|
||||
|
new EventPipeProvider( |
||||
|
MicrosoftExtensionsLoggingProviderName, |
||||
|
EventLevel.LogAlways, |
||||
|
(long)(LoggingEventSource.Keywords.JsonMessage | LoggingEventSource.Keywords.FormattedMessage)), |
||||
|
|
||||
|
// Activity correlation
|
||||
|
new EventPipeProvider( |
||||
|
TplEventSource, |
||||
|
keywords: 0x80, |
||||
|
eventLevel: EventLevel.LogAlways), |
||||
|
|
||||
|
// Diagnostic source events
|
||||
|
new EventPipeProvider( |
||||
|
DiagnosticSourceEventSource, |
||||
|
keywords: 0x1 | 0x2, |
||||
|
eventLevel: EventLevel.Verbose, |
||||
|
arguments: new Dictionary<string,string> |
||||
|
{ |
||||
|
{ "FilterAndPayloadSpecs", DiagnosticFilterString } |
||||
|
}) |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public static EventPipeProvider CreateStandardProvider(string providerName) |
||||
|
{ |
||||
|
return |
||||
|
new EventPipeProvider( |
||||
|
providerName, |
||||
|
EventLevel.Informational, |
||||
|
(long)ClrTraceEventParser.Keywords.None, |
||||
|
new Dictionary<string, string>() |
||||
|
{ |
||||
|
{ "EventCounterIntervalSec", "1" } |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private static class LoggingEventSource |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// This is public from an EventSource consumer point of view, but since these definitions
|
||||
|
/// are not needed outside this class
|
||||
|
/// </summary>
|
||||
|
public static class Keywords |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Meta events are events about the LoggingEventSource itself (that is they did not come from ILogger
|
||||
|
/// </summary>
|
||||
|
public const EventKeywords Meta = (EventKeywords)1; |
||||
|
/// <summary>
|
||||
|
/// Turns on the 'Message' event when ILogger.Log() is called. It gives the information in a programmatic (not formatted) way
|
||||
|
/// </summary>
|
||||
|
public const EventKeywords Message = (EventKeywords)2; |
||||
|
/// <summary>
|
||||
|
/// Turns on the 'FormatMessage' event when ILogger.Log() is called. It gives the formatted string version of the information.
|
||||
|
/// </summary>
|
||||
|
public const EventKeywords FormattedMessage = (EventKeywords)4; |
||||
|
/// <summary>
|
||||
|
/// Turns on the 'MessageJson' event when ILogger.Log() is called. It gives JSON representation of the Arguments.
|
||||
|
/// </summary>
|
||||
|
public const EventKeywords JsonMessage = (EventKeywords)8; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue