Commit 71e577e9 authored by Michael Herndon's avatar Michael Herndon

WORK: add logging extensions

parent 12a395d4
......@@ -48,6 +48,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NerdyMishka.Agent.Core", "s
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NerdyMishka.Extensions.DependencyInjection.Modules", "src\Extensions\DependencyInjection.Modules\NerdyMishka.Extensions.DependencyInjection.Modules.csproj", "{D7C731AA-3A47-42A8-A9E6-EBAC2D9AC845}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NerdyMishka.Extensions.Serilog", "src\Extensions\Serilog\NerdyMishka.Extensions.Serilog.csproj", "{70060B63-843B-4DC7-A53F-92AE4B66878B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
......@@ -106,6 +108,10 @@ Global
{D7C731AA-3A47-42A8-A9E6-EBAC2D9AC845}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D7C731AA-3A47-42A8-A9E6-EBAC2D9AC845}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D7C731AA-3A47-42A8-A9E6-EBAC2D9AC845}.Release|Any CPU.Build.0 = Release|Any CPU
{70060B63-843B-4DC7-A53F-92AE4B66878B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{70060B63-843B-4DC7-A53F-92AE4B66878B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{70060B63-843B-4DC7-A53F-92AE4B66878B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{70060B63-843B-4DC7-A53F-92AE4B66878B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
......@@ -132,6 +138,7 @@ Global
{9A92EBB7-F59F-46A3-A956-FA00C4093EEC} = {07B4A7C1-57E6-48D2-810D-BB8F234EECFF}
{9AB3DAE3-D669-42D4-A3BB-50187F94CDE1} = {07B4A7C1-57E6-48D2-810D-BB8F234EECFF}
{D7C731AA-3A47-42A8-A9E6-EBAC2D9AC845} = {6444BF0F-2EED-404A-AC00-CC756F4F4ED0}
{70060B63-843B-4DC7-A53F-92AE4B66878B} = {6444BF0F-2EED-404A-AC00-CC756F4F4ED0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {16666A01-3CC0-4111-8A58-9A6CA34CD76C}
......
......@@ -109,8 +109,6 @@ namespace NerdyMishka.Management.Automation
}
}
using (var pipeline = runspace.CreatePipeline())
{
pipeline.Output.DataReady += (sender, args) =>
......
......@@ -25,7 +25,7 @@ namespace NerdyMishka.Console
}
public ConsoleEngine(ConsoleEngineOptions options = null, ServiceProvider provider)
public ConsoleEngine(ConsoleEngineOptions options = null)
{
options = options ?? new ConsoleEngineOptions() {
Assemblies = new List<Assembly>(){ typeof(ConsoleEngine).Assembly }
......
......@@ -25,8 +25,6 @@ namespace NerdyMishka.Console
bool CanExecute(Type argumentType);
void ApplyServices(ServiceProvider provider);
/// <summary>
/// Executes a command line action.
/// </summary>
......
......@@ -25,7 +25,6 @@ namespace NerdyMishka.Extensions.DependencyInjection
if(s_modules.TryGetValue(services, out IList<IModule> set))
set.Clear();
}
}
/// <summary>
......
......@@ -61,7 +61,6 @@ namespace NerdyMishka.Identity
TUser user,
CancellationToken cancellationToken = default(CancellationToken))
{
var permissions = await GetPermissionAsync(user);
return permissions.Select(o => o.ToClaim())
.ToArray();
......@@ -464,8 +463,6 @@ namespace NerdyMishka.Identity
}
user.Pseudonym = userName.ToLowerInvariant();
return Task.CompletedTask;
}
......@@ -520,8 +517,6 @@ namespace NerdyMishka.Identity
await this.Db.SaveChangesAsync(cancellationToken);
return IdentityResult.Success;
}
......
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
namespace NerdyMishka.Extensions.Logging
{
public interface ICachedMetric
{
ICachedMetric TrackValue(double value);
ICachedMetric TrackValue(double value, string dimension);
}
public class ExceptionTelemetry : ITelemetry, ISupportProperties
{
public ExceptionTelemetry(Exception exception)
{
this.Exception = exception;
}
public virtual Exception Exception { get; set; }
public virtual string Sequence { get; set; }
public virtual DateTimeOffset Timestamp { get; set; }
public virtual IDictionary<string, string> Properties { get; set; }
public virtual LogLevel LogLevel { get; set; }
}
public class EventTelemetry : ITelemetry, ISupportProperties, ISupportMetrics
{
public EventTelemetry(string name = null)
{
this.Name = name;
}
public virtual string Name { get; set; }
public virtual string Sequence { get; set; }
public virtual DateTimeOffset Timestamp { get; set; }
public virtual IDictionary<string, string> Properties { get; set; }
public virtual IDictionary<string, double> Metrics { get; set; }
}
public class TraceTelemetry : ITelemetry, ISupportProperties
{
public TraceTelemetry(string message = null)
{
this.Message = message;
}
public virtual string Message { get; set; }
public virtual string Sequence { get; set; }
public virtual DateTimeOffset Timestamp { get; set; }
public virtual LogLevel LogLevel { get; set; }
public virtual IDictionary<string, string> Properties { get; set; }
}
public interface ISupportMetrics
{
IDictionary<string, double> Metrics { get; set; }
}
public interface ISupportProperties
{
IDictionary<string, string> Properties { get; set; }
}
public interface ITelemetry
{
string Sequence { get; set; }
DateTimeOffset Timestamp { get; set; }
}
public interface ITelemetryClient
{
void Flush();
ICachedMetric GetMetric(string metricId);
ICachedMetric GetMetric(string metricId, string dimension1);
ICachedMetric GetMetric(string metricId, string dimension1, string dimension2);
ICachedMetric GetMetric(string metricId, string dimension1, string dimension2, string dimension3);
ICachedMetric GetMetric(string metricId, string dimension1, string dimension2, string dimension3, string dimension4);
ITelemetryClient TrackAvailability (
string name,
DateTimeOffset timeStamp,
TimeSpan duration,
string runLocation,
bool success,
string message = null,
IDictionary<string,string> properties = null,
IDictionary<string,double> metrics = null);
ITelemetryClient TrackDependency(
string dependencyTypeName,
string dependencyName,
string data,
DateTimeOffset startTime,
TimeSpan duration,
bool success);
ITelemetryClient TrackDependency(
string dependencyTypeName,
string target,
string dependencyName,
string data,
DateTimeOffset startTime,
TimeSpan duration,
string resultCode,
bool success);
ITelemetryClient TrackEvent (
string eventName,
IDictionary<string,string> properties = null,
IDictionary<string,double> metrics = null,
DateTimeOffset? timeStamp = null);
ITelemetryClient Track(ITelemetry telemetry);
ITelemetryClient TrackException(
Exception exception,
IDictionary<string,string> properties = null,
IDictionary<string,double> metrics = null);
ITelemetryClient TrackMetric(string metricId, double value);
ITelemetryClient TrackTrace(string message,
LogLevel logLevel = LogLevel.Information,
System.Collections.Generic.IDictionary<string,string> properties = null);
ITelemetryClient TrackTrace(string message,
int logLevel = -1,
System.Collections.Generic.IDictionary<string,string> properties = null);
ITelemetryClient TrackPageRequest(string name);
ITelemetryClient TrackRequest(
string name,
System.DateTimeOffset startTime,
TimeSpan duration,
string responseCode = "200",
bool success = true);
}
}
\ No newline at end of file
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Serilog.Debugging;
namespace NerdyMishka.Extensions.Logging
{
public class LogAnalyticsWorkspace
{
public string Id { get; set; }
public string Key { get; set; }
public string Uri { get; set; }
public string LogType { get; set; } = "NerdyMishkaLogV1";
public bool Debug { get; set; } = false;
}
public class LogAnalyticsWriter : IDisposable
{
private LogAnalyticsWorkspace options;
private HttpClient httpClient;
private ILogger logger;
private bool dispose = false;
public LogAnalyticsWriter(
LogAnalyticsWorkspace workspace,
HttpClient httpClient = null,
ILogger<LogAnalyticsWriter> logger = null)
{
this.options = workspace ?? new LogAnalyticsWorkspace() {
Debug = true
};
if(this.options.Uri == null && this.options.Id != null)
{
this.options.Uri = $"https://{this.options.Id}.ods.opinsights.azure.com/api/logs?api-version=2016-04-01";
}
this.httpClient = httpClient ?? new HttpClient();
this.logger = logger;
}
public void Write(string json, string logType = null)
{
Task.Run(async() => {
await this.WriteAsync(logType, json);
});
}
public async Task WriteAsync(string json, string logType = null)
{
logType = logType ?? this.options.LogType;
var response = await this.SendAsync(logType, json);
if(response.StatusCode != System.Net.HttpStatusCode.OK)
{
string context = await response.Content.ReadAsStringAsync();
logger?.LogError($"LogAnalytics write failed: {response.StatusCode} - {context}.");
SelfLog.WriteLine($"LogAnalytics write failed: {response.StatusCode} - {context}.");
}
}
public async Task<HttpResponseMessage> SendAsync(string json, string logType = null)
{
logType = logType ?? this.options.LogType;
string date = DateTime.UtcNow.ToString("r");
if(this.options == null || this.options.Key == null)
{
this.logger?.LogDebug(json);
}
string authSignature = GetAuthSignature(json, date, this.options.Id, this.options.Key);
using (var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, this.options.Uri))
{
httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("SharedKey", authSignature);
httpRequestMessage.Headers.Add("Log-Type", logType);
httpRequestMessage.Headers.Add("Accept", "application/json");
httpRequestMessage.Headers.Add("x-ms-date", date);
httpRequestMessage.Content = new System.Net.Http.StringContent(json);
httpRequestMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
return await this.httpClient.SendAsync(httpRequestMessage);
}
}
private static string GetAuthSignature(
string serializedJsonObject,
string dateString,
string workspaceId,
string workspaceKey)
{
var sb = new System.Text.StringBuilder();
sb.Append($"POST")
.Append($"\n{serializedJsonObject.Length}")
.Append("\napplication/json")
.Append($"\nx-ms-date:{dateString}")
.Append("\n/api/logs");
var messageInfo = System.Text.Encoding.UTF8.GetBytes(sb.ToString());
var key = Convert.FromBase64String(workspaceKey);
using (var hasher = new HMACSHA256(key))
{
var messageHash = Convert.ToBase64String(hasher.ComputeHash(messageInfo));
return $"{workspaceId}:{messageHash}";
}
}
public void Dispose()
{
if(!this.dispose)
{
this.httpClient.Dispose();
this.httpClient = null;
this.dispose = true;
}
}
}
}
\ No newline at end of file
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.0" />
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="System.Text.Json" Version="4.7.0" />
</ItemGroup>
</Project>
// Copyright 2016 Serilog Contributors
//
// 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.
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Threading;
using Serilog.Core;
using Serilog.Events;
using Serilog.Sinks.ApplicationInsights.Sinks.ApplicationInsights.TelemetryConverters;
namespace NerdyMishka.Extensions.Logging.Sinks.ApplicationInsights
{
/// <summary>
/// Base class for Microsoft Azure Application Insights based Sinks.
/// Inspired by their NLog Appender implementation.
/// </summary>
public class ApplicationInsightsSink : ILogEventSink, IDisposable
{
private long isDisposing = 0;
private long isDisposed = 0;
private ITelemetryClient telemetryClient;
/// <summary>
/// Gets or sets a value indicating whether this instance is being disposed.
/// </summary>
/// <value>
/// <c>true</c> if this instance is being disposed; otherwise, <c>false</c>.
/// </value>
public bool IsDisposing
{
get => Interlocked.Read(ref isDisposing) == 1;
protected set => Interlocked.Exchange(ref isDisposing, value ? 1 : 0);
}
/// <summary>
/// Gets a value indicating whether this instance has been disposed.
/// </summary>
/// <value>
/// <c>true</c> if this instance has been disposed; otherwise, <c>false</c>.
/// </value>
public bool IsDisposed
{
get => Interlocked.Read(ref isDisposed) == 1;
protected set => Interlocked.Exchange(ref isDisposed, value ? 1 : 0);
}
private readonly IFormatProvider formatProvider;
private readonly ITelemetryConverter telemetryConverter;
/// <summary>
/// Creates a sink that saves logs to the Application Insights account for the given <paramref name="telemetryClient" /> instance.
/// </summary>
/// <param name="telemetryClient">Required Application Insights <paramref name="telemetryClient" />.</param>
/// <param name="telemetryConverter">The <see cref="LogEvent"/> to <see cref="ITelemetry"/> converter.</param>
/// <param name="formatProvider">Supplies culture-specific formatting information, or null for default provider.</param>
/// <exception cref="ArgumentNullException"><paramref name="telemetryClient" /> cannot be null</exception>
public ApplicationInsightsSink(
NerdyMishka.Extensions.Logging.ITelemetryClient telemetryClient,
ITelemetryConverter telemetryConverter,
IFormatProvider formatProvider = null)
{
this.telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient));
this.telemetryConverter = telemetryConverter ?? throw new ArgumentNullException(nameof(telemetryConverter));
this.formatProvider = formatProvider;
}
#region AI specifc Helper methods
/// <summary>
/// Hands over the <paramref name="telemetry" /> to the AI telemetry client.
/// </summary>
/// <param name="telemetry">The telemetry.</param>
/// <exception cref="System.ArgumentNullException">telemetry</exception>
protected virtual void TrackTelemetry(ITelemetry telemetry)
{
if (telemetry == null) throw new ArgumentNullException(nameof(telemetry));
CheckForAndThrowIfDisposed();
// the .Track() method is safe to use (even though documented otherwise)
// see https://github.com/Microsoft/ApplicationInsights-dotnet/issues/244
this.telemetryClient?.Track(telemetry);
}
#endregion AI specifc Helper methods
#region Implementation of ILogEventSink
/// <summary>
/// Emit the provided log event to the sink.
/// </summary>
/// <param name="logEvent">The log event to write.</param>
/// <exception cref="Exception">A delegate callback throws an exception.</exception>
/// <exception cref="TargetInvocationException">A delegate callback throws an exception.</exception>
public virtual void Emit(LogEvent logEvent)
{
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
CheckForAndThrowIfDisposed();
try
{
IEnumerable<ITelemetry> telemetries = this.telemetryConverter.Convert(logEvent, this.formatProvider);
// if 'null' is returned (& we therefore there's nothing to track), the logEvent is basically skipped
if (telemetries != null)
{
foreach (ITelemetry telemetry in telemetries)
{
if (telemetry != null)
{
TrackTelemetry(telemetry);
}
}
}
}
catch (TargetInvocationException targetInvocationException)
{
// rethrow original exception (inside the TargetInvocationException) if any
if (targetInvocationException.InnerException != null)
{
ExceptionDispatchInfo.Capture(targetInvocationException.InnerException).Throw();
}
else
{
throw;
}
}
}
#endregion
#region Implementation of IDisposable
/// <summary>
/// Checks whether this instance has been disposed and if so, throws an <see cref="ObjectDisposedException"/>.
/// </summary>
/// <exception cref="ObjectDisposedException"></exception>
protected void CheckForAndThrowIfDisposed()
{
if (IsDisposed)
{
throw new ObjectDisposedException(this.GetType().Name);
}
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposeManagedResources"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposeManagedResources)
{
if (IsDisposing || IsDisposed)
return;
try
{
IsDisposing = true;
// we only have managed resources to dispose of
if (disposeManagedResources)
{
// attempt to free managed resources
try
{
this.telemetryClient?.Flush();
}
finally
{
this.telemetryClient = null;
}
}
}
finally
{
IsDisposed = true;
IsDisposing = false;
}
}
#endregion Implementation of IDisposable
}
}
using System;
using System.Collections.Generic;
using System.Globalization;
using Serilog.Debugging;
using Serilog.Events;
namespace Serilog.Sinks.ApplicationInsights.Sinks.ApplicationInsights.Formatters
{
public class ApplicationInsightsDottedValueFormatter : IValueFormatter
{
private static readonly IDictionary<Type, Action<string, object, IDictionary<string, string>>> LiteralWriters = new Dictionary
<Type, Action<string, object, IDictionary<string, string>>>
{
{typeof (SequenceValue), (k, v, p) => WriteSequenceValue(k, (SequenceValue) v, p)},
{typeof (DictionaryValue), (k, v, p) => WriteDictionaryValue(k, (DictionaryValue) v, p)},
{typeof (StructureValue), (k, v, p) => WriteStructureValue(k, (StructureValue) v, p)},
{typeof (ScalarValue), (k, v, p) => WriteValue(k,((ScalarValue)v).Value,p)},
{typeof (DateTime), (k, v, p) => AppendProperty(p,k,((DateTime)v).ToString("o"))},
{typeof (DateTimeOffset), (k, v, p) => AppendProperty(p,k,((DateTimeOffset)v).ToString("o"))},
{typeof (float), (k, v, p) => AppendProperty(p,k,((float)v).ToString("R", CultureInfo.InvariantCulture))},
{typeof (double), (k, v, p) => AppendProperty(p,k,((double)v).ToString("R", CultureInfo.InvariantCulture))},
};
public void Format(string propertyName, LogEventPropertyValue propertyValue, IDictionary<string, string> properties)
{
WriteValue(propertyName, propertyValue, properties);
}
private static void WriteStructureValue(string key, StructureValue structureValue, IDictionary<string, string> properties)
{
foreach (var eventProperty in structureValue.Properties)
{
WriteValue(key + "." + eventProperty.Name, eventProperty.Value, properties);
}
}
private static void WriteDictionaryValue(string key, DictionaryValue dictionaryValue, IDictionary<string, string> properties)
{
foreach (var eventProperty in dictionaryValue.Elements)
{
WriteValue(key + "." + eventProperty.Key.Value, eventProperty.Value, properties);
}
}
private static void WriteSequenceValue(string key, SequenceValue sequenceValue, IDictionary<string, string> properties)
{
int index = 0;
foreach (var eventProperty in sequenceValue.Elements)
{
WriteValue(key + "." + index, eventProperty, properties);
index++;
}
AppendProperty(properties, key + ".Count", index.ToString());
}
public static void WriteValue(string key, object value, IDictionary<string, string> properties)
{