| | | 1 | | using System.Collections; |
| | | 2 | | using System.Diagnostics; |
| | | 3 | | using Elsa.Diagnostics.StructuredLogs.Contracts; |
| | | 4 | | using Elsa.Diagnostics.StructuredLogs.Models; |
| | | 5 | | using Elsa.Diagnostics.StructuredLogs.Options; |
| | | 6 | | using Microsoft.Extensions.Logging; |
| | | 7 | | |
| | | 8 | | namespace Elsa.Diagnostics.StructuredLogs.Logging; |
| | | 9 | | |
| | 5 | 10 | | public class StructuredLogLogger( |
| | 5 | 11 | | string categoryName, |
| | 5 | 12 | | IStructuredLogProvider logProvider, |
| | 5 | 13 | | IStructuredLogRedactor redactor, |
| | 5 | 14 | | IStructuredLogSourceRegistry sourceRegistry, |
| | 5 | 15 | | StructuredLogsOptions options, |
| | 5 | 16 | | Func<IExternalScopeProvider> getScopeProvider) : ILogger |
| | | 17 | | { |
| | | 18 | | private long _sequence; |
| | | 19 | | |
| | 2 | 20 | | public IDisposable? BeginScope<TState>(TState state) where TState : notnull => getScopeProvider().Push(state); |
| | | 21 | | |
| | | 22 | | public bool IsEnabled(LogLevel logLevel) |
| | | 23 | | { |
| | 5 | 24 | | return logLevel != LogLevel.None && (options.IncludeStructuredLogsInternalLogs || !categoryName.StartsWith("Elsa |
| | | 25 | | } |
| | | 26 | | |
| | | 27 | | public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Excepti |
| | | 28 | | { |
| | 5 | 29 | | if (!IsEnabled(logLevel)) |
| | 1 | 30 | | return; |
| | | 31 | | |
| | 4 | 32 | | var now = DateTimeOffset.UtcNow; |
| | 4 | 33 | | var currentActivity = Activity.Current; |
| | 4 | 34 | | var properties = ExtractProperties(state); |
| | 4 | 35 | | var scopes = ExtractScopes(); |
| | 4 | 36 | | var logEvent = new StructuredLogEvent |
| | 4 | 37 | | { |
| | 4 | 38 | | Sequence = Interlocked.Increment(ref _sequence), |
| | 4 | 39 | | Timestamp = now, |
| | 4 | 40 | | ReceivedAt = now, |
| | 4 | 41 | | Level = ToStructuredLogLevel(logLevel), |
| | 4 | 42 | | Category = categoryName, |
| | 4 | 43 | | EventId = eventId.Id, |
| | 4 | 44 | | EventName = eventId.Name, |
| | 4 | 45 | | Message = formatter(state, exception), |
| | 4 | 46 | | MessageTemplate = ExtractMessageTemplate(state), |
| | 4 | 47 | | Exception = exception == null ? null : new StructuredLogException(exception.GetType().FullName ?? exception. |
| | 4 | 48 | | TraceId = currentActivity?.TraceId.ToString(), |
| | 4 | 49 | | SpanId = currentActivity?.SpanId.ToString(), |
| | 4 | 50 | | CorrelationId = currentActivity?.RootId ?? GetContextValue(properties, scopes, nameof(StructuredLogEvent.Cor |
| | 4 | 51 | | TenantId = GetContextValue(properties, scopes, nameof(StructuredLogEvent.TenantId)), |
| | 4 | 52 | | WorkflowDefinitionId = GetContextValue(properties, scopes, nameof(StructuredLogEvent.WorkflowDefinitionId)), |
| | 4 | 53 | | WorkflowInstanceId = GetContextValue(properties, scopes, nameof(StructuredLogEvent.WorkflowInstanceId)), |
| | 4 | 54 | | SourceId = sourceRegistry.Current.Id, |
| | 4 | 55 | | Scopes = scopes, |
| | 4 | 56 | | Properties = properties |
| | 4 | 57 | | }; |
| | | 58 | | |
| | 4 | 59 | | var redacted = redactor.Redact(logEvent); |
| | 4 | 60 | | _ = logProvider.PublishAsync(redacted); |
| | 4 | 61 | | } |
| | | 62 | | |
| | | 63 | | private static StructuredLogLevel ToStructuredLogLevel(LogLevel logLevel) |
| | | 64 | | { |
| | 4 | 65 | | return logLevel switch |
| | 4 | 66 | | { |
| | 0 | 67 | | LogLevel.Trace => StructuredLogLevel.Trace, |
| | 0 | 68 | | LogLevel.Debug => StructuredLogLevel.Debug, |
| | 3 | 69 | | LogLevel.Information => StructuredLogLevel.Information, |
| | 1 | 70 | | LogLevel.Warning => StructuredLogLevel.Warning, |
| | 0 | 71 | | LogLevel.Error => StructuredLogLevel.Error, |
| | 0 | 72 | | LogLevel.Critical => StructuredLogLevel.Critical, |
| | 0 | 73 | | _ => StructuredLogLevel.None |
| | 4 | 74 | | }; |
| | | 75 | | } |
| | | 76 | | |
| | | 77 | | private static string? ExtractMessageTemplate<TState>(TState state) |
| | | 78 | | { |
| | 4 | 79 | | return state is IEnumerable<KeyValuePair<string, object?>> values |
| | 7 | 80 | | ? values.FirstOrDefault(x => x.Key == "{OriginalFormat}").Value?.ToString() |
| | 4 | 81 | | : null; |
| | | 82 | | } |
| | | 83 | | |
| | | 84 | | private static Dictionary<string, string?> ExtractProperties<TState>(TState state) |
| | | 85 | | { |
| | 4 | 86 | | if (state is not IEnumerable<KeyValuePair<string, object?>> values) |
| | 0 | 87 | | return new(); |
| | | 88 | | |
| | 4 | 89 | | return values |
| | 7 | 90 | | .Where(x => x.Key != "{OriginalFormat}") |
| | 10 | 91 | | .ToDictionary(x => x.Key, x => x.Value?.ToString(), StringComparer.OrdinalIgnoreCase); |
| | | 92 | | } |
| | | 93 | | |
| | | 94 | | private Dictionary<string, string?> ExtractScopes() |
| | | 95 | | { |
| | 4 | 96 | | var scopes = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase); |
| | 4 | 97 | | var scopeValues = new List<object?>(); |
| | | 98 | | |
| | 6 | 99 | | getScopeProvider().ForEachScope((scope, state) => state.Add(scope), scopeValues); |
| | | 100 | | |
| | 12 | 101 | | for (var i = 0; i < scopeValues.Count; i++) |
| | 2 | 102 | | AddScope(scopes, scopeValues[i], i); |
| | | 103 | | |
| | 4 | 104 | | return scopes; |
| | | 105 | | } |
| | | 106 | | |
| | | 107 | | private static void AddScope(IDictionary<string, string?> scopes, object? scope, int index) |
| | | 108 | | { |
| | 2 | 109 | | if (scope == null) |
| | 0 | 110 | | return; |
| | | 111 | | |
| | 2 | 112 | | if (TryAddKeyValueScope(scopes, scope)) |
| | 1 | 113 | | return; |
| | | 114 | | |
| | 1 | 115 | | var key = index == 0 ? "Scope" : $"Scope{index + 1}"; |
| | 1 | 116 | | AddValue(scopes, key, scope); |
| | 1 | 117 | | } |
| | | 118 | | |
| | | 119 | | private static bool TryAddKeyValueScope(IDictionary<string, string?> scopes, object scope) |
| | | 120 | | { |
| | 2 | 121 | | if (scope is not IEnumerable values) |
| | 0 | 122 | | return false; |
| | | 123 | | |
| | 2 | 124 | | var added = false; |
| | 30 | 125 | | foreach (var value in values) |
| | | 126 | | { |
| | 13 | 127 | | if (value == null) |
| | | 128 | | continue; |
| | | 129 | | |
| | 13 | 130 | | var type = value.GetType(); |
| | 13 | 131 | | if (!type.IsGenericType || type.GetGenericTypeDefinition() != typeof(KeyValuePair<,>)) |
| | | 132 | | continue; |
| | | 133 | | |
| | 2 | 134 | | var key = type.GetProperty(nameof(KeyValuePair<string, object?>.Key))?.GetValue(value) as string; |
| | 2 | 135 | | if (key is null or "{OriginalFormat}") |
| | | 136 | | continue; |
| | | 137 | | |
| | 2 | 138 | | var itemValue = type.GetProperty(nameof(KeyValuePair<string, object?>.Value))?.GetValue(value); |
| | 2 | 139 | | AddValue(scopes, key, itemValue); |
| | 2 | 140 | | added = true; |
| | | 141 | | } |
| | | 142 | | |
| | 2 | 143 | | return added; |
| | | 144 | | } |
| | | 145 | | |
| | | 146 | | private static void AddValue(IDictionary<string, string?> values, string key, object? value) |
| | | 147 | | { |
| | 3 | 148 | | var uniqueKey = key; |
| | 3 | 149 | | var suffix = 2; |
| | | 150 | | |
| | 3 | 151 | | while (values.ContainsKey(uniqueKey)) |
| | 0 | 152 | | uniqueKey = $"{key}{suffix++}"; |
| | | 153 | | |
| | 3 | 154 | | values[uniqueKey] = value?.ToString(); |
| | 3 | 155 | | } |
| | | 156 | | |
| | | 157 | | private static string? GetContextValue(IReadOnlyDictionary<string, string?> properties, IReadOnlyDictionary<string, |
| | | 158 | | { |
| | 16 | 159 | | return properties.GetValueOrDefault(key) ?? scopes.GetValueOrDefault(key); |
| | | 160 | | } |
| | | 161 | | } |