From: Wiktor Kopec Date: Mon, 9 Mar 2020 23:41:31 +0000 (-0700) Subject: Create dotnet-monitor tool (#878) X-Git-Tag: submit/tizen_5.5/20200504.045052~11^2^2~78 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=c47d36d8ec3070962524f4012ebc86ddd8c04694;p=platform%2Fcore%2Fdotnet%2Fdiagnostics.git Create dotnet-monitor tool (#878) * Create dotnet-monitor tool * Listens for TraceEvent for logging and metrics. * Currently supports a console sink, and a Log Analytics metric sink * Designed to be used in an Aks sidecar to produce logging/metrics. --- diff --git a/diagnostics.sln b/diagnostics.sln index 5f400e0e9..e273acca7 100644 --- a/diagnostics.sln +++ b/diagnostics.sln @@ -71,6 +71,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Commands", "Commands", "{C4 src\Tools\Common\Commands\ProcessStatus.cs = src\Tools\Common\Commands\ProcessStatus.cs EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet-monitor", "src\Tools\dotnet-monitor\dotnet-monitor.csproj", "{C57F7656-6663-4A3C-BE38-B75C6C57E77D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Monitoring", "src\Microsoft.Diagnostics.Monitoring\Microsoft.Diagnostics.Monitoring.csproj", "{CFCF90E5-91CF-44FD-819D-97F530AEF769}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Monitoring.LogAnalytics", "src\Microsoft.Diagnostics.Monitoring.LogAnalytics\Microsoft.Diagnostics.Monitoring.LogAnalytics.csproj", "{E3629433-C28E-4D37-887D-4F244C55510B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Checked|Any CPU = Checked|Any CPU @@ -1024,6 +1030,126 @@ Global {CED9ABBA-861E-4C0A-9359-22351208EF27}.RelWithDebInfo|x64.Build.0 = Release|Any CPU {CED9ABBA-861E-4C0A-9359-22351208EF27}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU {CED9ABBA-861E-4C0A-9359-22351208EF27}.RelWithDebInfo|x86.Build.0 = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Checked|Any CPU.Build.0 = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Checked|ARM.ActiveCfg = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Checked|ARM.Build.0 = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Checked|ARM64.ActiveCfg = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Checked|ARM64.Build.0 = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Checked|x64.ActiveCfg = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Checked|x64.Build.0 = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Checked|x86.ActiveCfg = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Checked|x86.Build.0 = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Debug|ARM.ActiveCfg = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Debug|ARM.Build.0 = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Debug|ARM64.Build.0 = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Debug|x64.ActiveCfg = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Debug|x64.Build.0 = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Debug|x86.ActiveCfg = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Debug|x86.Build.0 = Debug|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Release|Any CPU.Build.0 = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Release|ARM.ActiveCfg = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Release|ARM.Build.0 = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Release|ARM64.ActiveCfg = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Release|ARM64.Build.0 = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Release|x64.ActiveCfg = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Release|x64.Build.0 = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Release|x86.ActiveCfg = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.Release|x86.Build.0 = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.RelWithDebInfo|ARM.ActiveCfg = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.RelWithDebInfo|ARM.Build.0 = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.RelWithDebInfo|ARM64.ActiveCfg = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.RelWithDebInfo|ARM64.Build.0 = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.RelWithDebInfo|x64.Build.0 = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU + {C57F7656-6663-4A3C-BE38-B75C6C57E77D}.RelWithDebInfo|x86.Build.0 = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Checked|Any CPU.Build.0 = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Checked|ARM.ActiveCfg = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Checked|ARM.Build.0 = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Checked|ARM64.ActiveCfg = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Checked|ARM64.Build.0 = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Checked|x64.ActiveCfg = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Checked|x64.Build.0 = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Checked|x86.ActiveCfg = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Checked|x86.Build.0 = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Debug|ARM.ActiveCfg = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Debug|ARM.Build.0 = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Debug|ARM64.Build.0 = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Debug|x64.ActiveCfg = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Debug|x64.Build.0 = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Debug|x86.ActiveCfg = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Debug|x86.Build.0 = Debug|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Release|Any CPU.Build.0 = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Release|ARM.ActiveCfg = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Release|ARM.Build.0 = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Release|ARM64.ActiveCfg = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Release|ARM64.Build.0 = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Release|x64.ActiveCfg = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Release|x64.Build.0 = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Release|x86.ActiveCfg = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.Release|x86.Build.0 = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.RelWithDebInfo|ARM.ActiveCfg = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.RelWithDebInfo|ARM.Build.0 = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.RelWithDebInfo|ARM64.ActiveCfg = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.RelWithDebInfo|ARM64.Build.0 = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.RelWithDebInfo|x64.Build.0 = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU + {CFCF90E5-91CF-44FD-819D-97F530AEF769}.RelWithDebInfo|x86.Build.0 = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Checked|Any CPU.Build.0 = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Checked|ARM.ActiveCfg = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Checked|ARM.Build.0 = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Checked|ARM64.ActiveCfg = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Checked|ARM64.Build.0 = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Checked|x64.ActiveCfg = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Checked|x64.Build.0 = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Checked|x86.ActiveCfg = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Checked|x86.Build.0 = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Debug|ARM.ActiveCfg = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Debug|ARM.Build.0 = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Debug|ARM64.Build.0 = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Debug|x64.ActiveCfg = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Debug|x64.Build.0 = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Debug|x86.ActiveCfg = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Debug|x86.Build.0 = Debug|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Release|Any CPU.Build.0 = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Release|ARM.ActiveCfg = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Release|ARM.Build.0 = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Release|ARM64.ActiveCfg = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Release|ARM64.Build.0 = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Release|x64.ActiveCfg = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Release|x64.Build.0 = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Release|x86.ActiveCfg = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.Release|x86.Build.0 = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.RelWithDebInfo|ARM.ActiveCfg = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.RelWithDebInfo|ARM.Build.0 = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.RelWithDebInfo|ARM64.ActiveCfg = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.RelWithDebInfo|ARM64.Build.0 = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.RelWithDebInfo|x64.Build.0 = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU + {E3629433-C28E-4D37-887D-4F244C55510B}.RelWithDebInfo|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1059,6 +1185,9 @@ Global {CED9ABBA-861E-4C0A-9359-22351208EF27} = {03479E19-3F18-49A6-910A-F5041E27E7C0} {298AE119-6625-4604-BDE5-0765DC34C856} = {B62728C8-1267-4043-B46F-5537BBAEC692} {C457CBCD-3A8D-4402-9A2B-693A0390D3F9} = {298AE119-6625-4604-BDE5-0765DC34C856} + {C57F7656-6663-4A3C-BE38-B75C6C57E77D} = {B62728C8-1267-4043-B46F-5537BBAEC692} + {CFCF90E5-91CF-44FD-819D-97F530AEF769} = {19FAB78C-3351-4911-8F0C-8C6056401740} + {E3629433-C28E-4D37-887D-4F244C55510B} = {19FAB78C-3351-4911-8F0C-8C6056401740} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {46465737-C938-44FC-BE1A-4CE139EBB5E0} diff --git a/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/AuthModel.cs b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/AuthModel.cs new file mode 100644 index 000000000..7b61da2a4 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/AuthModel.cs @@ -0,0 +1,17 @@ +// 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; +using System.Text.Json.Serialization; + +namespace Microsoft.Diagnostics.Monitoring.LogAnalytics +{ + internal sealed class AuthResult + { + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/AuthenticatingHandler.cs b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/AuthenticatingHandler.cs new file mode 100644 index 000000000..07f9e7131 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/AuthenticatingHandler.cs @@ -0,0 +1,69 @@ +// 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.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.LogAnalytics +{ + internal sealed class AuthenticationDelegatingHandler : DelegatingHandler + { + //TODO Storing a high value bearer token in plain text in memory + private AuthenticationHeaderValue _cachedBearerToken; + private readonly MetricsConfiguration _metricsConfiguration; + + public AuthenticationDelegatingHandler(MetricsConfiguration configuration) : base(new HttpClientHandler()) + { + _metricsConfiguration = configuration; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request.Headers.Authorization == null) + { + if (_cachedBearerToken == null) + { + _cachedBearerToken = await AcquireBearerToken(cancellationToken); + } + request.Headers.Authorization = _cachedBearerToken; + } + + HttpResponseMessage response = await base.SendAsync(request, cancellationToken); + //Possible Token expired + if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + _cachedBearerToken = await AcquireBearerToken(cancellationToken); + request.Headers.Authorization = _cachedBearerToken; + response = await base.SendAsync(request, cancellationToken); + } + return response; + } + + private async Task AcquireBearerToken(CancellationToken cancellationToken) + { + using var httpclient = new HttpClient(); + + var formValues = new Dictionary(); + formValues.Add("grant_type", "client_credentials"); + formValues.Add("client_id", _metricsConfiguration.AadClientId); + formValues.Add("client_secret", _metricsConfiguration.AadClientSecret); + formValues.Add("resource", "https://monitoring.azure.com/"); + + FormUrlEncodedContent content = new FormUrlEncodedContent(formValues); + using HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, FormattableString.Invariant($"https://login.microsoftonline.com/{_metricsConfiguration.TenantId}/oauth2/token")); + requestMessage.Content = content; + + using HttpResponseMessage result = await httpclient.SendAsync(requestMessage, cancellationToken); + result.EnsureSuccessStatusCode(); + + AuthResult auth = await JsonSerializer.DeserializeAsync(await result.EnsureSuccessStatusCode().Content.ReadAsStreamAsync(), cancellationToken: cancellationToken); + return new AuthenticationHeaderValue("Bearer", auth.AccessToken); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/LogAnalyticsLogger.cs b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/LogAnalyticsLogger.cs new file mode 100644 index 000000000..b4d31e320 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/LogAnalyticsLogger.cs @@ -0,0 +1,35 @@ +// 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 Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Diagnostics.Monitoring.LogAnalytics +{ + internal sealed class LogAnalyticsLogger : ILogger + { + private sealed class EmptyScopes : IDisposable + { + public void Dispose() {} + } + + + public IDisposable BeginScope(TState state) + { + return new EmptyScopes(); + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + return; + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/LogAnalyticsLoggerProvider.cs b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/LogAnalyticsLoggerProvider.cs new file mode 100644 index 000000000..e7c2da1b7 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/LogAnalyticsLoggerProvider.cs @@ -0,0 +1,23 @@ +// 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 Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Diagnostics.Monitoring.LogAnalytics +{ + public sealed class LogAnalyticsLoggerProvider : ILoggerProvider + { + public ILogger CreateLogger(string categoryName) + { + return new LogAnalyticsLogger(); + } + + public void Dispose() + { + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/LogRestClient.cs b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/LogRestClient.cs new file mode 100644 index 000000000..a527b3fce --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/LogRestClient.cs @@ -0,0 +1,13 @@ +// 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.Net.Http; + +namespace Microsoft.Diagnostics.Monitoring.LogAnalytics +{ + internal sealed class LogRestClient + { + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/MetricsConfiguration.cs b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/MetricsConfiguration.cs new file mode 100644 index 000000000..fdc547d20 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/MetricsConfiguration.cs @@ -0,0 +1,27 @@ +// 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.IO; +using System.Text; + +namespace Microsoft.Diagnostics.Monitoring.LogAnalytics +{ + /// + /// Do not rename these fields. These are used to bind to the app's configuration. + /// + public sealed class MetricsConfiguration + { + public string TenantId { get; set; } + public string AadClientId { get; set; } + public string AadClientSecret { get; set; } + } + + public sealed class ResourceConfiguration + { + public string AzureResourceId { get; set; } + public string AzureRegion { get; set; } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/MetricsLogger.cs b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/MetricsLogger.cs new file mode 100644 index 000000000..4cee6aa2b --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/MetricsLogger.cs @@ -0,0 +1,135 @@ +// 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 Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.LogAnalytics +{ + public sealed class MetricsLogger : IMetricsLogger, IAsyncDisposable + { + private readonly ILogger _logger; + private readonly MetricsConfiguration _metricConfig; + private readonly ResourceConfiguration _resourceConfig; + + private readonly Channel _metricChannel; + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly MetricsRestClient _metricsRestClient; + private readonly Task _processingTask; + + private int _disposed = 0; + + public MetricsLogger(ILogger logger, + IOptions metricsConfig, + IOptions resourceConfig) + { + _logger = logger; + + _metricConfig = metricsConfig.Value; + if (string.IsNullOrEmpty(_metricConfig.AadClientId) || + string.IsNullOrEmpty(_metricConfig.AadClientSecret) || + string.IsNullOrEmpty(_metricConfig.TenantId)) + { + _logger.LogError("Failed to bind metrics configuration. Metrics will not be collected."); + return; + } + _resourceConfig = resourceConfig.Value; + + if (string.IsNullOrEmpty(_resourceConfig.AzureRegion) || + string.IsNullOrEmpty(_resourceConfig.AzureResourceId) || + string.IsNullOrEmpty(_metricConfig.TenantId)) + { + _logger.LogError("Failed to bind azure resource configuration. Metrics will not be collected."); + return; + } + + //TODO Limit this + _metricChannel = Channel.CreateUnbounded(); + _cancellationTokenSource = new CancellationTokenSource(); + _metricsRestClient = new MetricsRestClient(_metricConfig, _resourceConfig); + + _processingTask = Task.Run(() => ProcessAllData(_cancellationTokenSource.Token), _cancellationTokenSource.Token); + } + + public void LogMetrics(Metric metric) + { + //Sink was not configured properly, we do not log any data. + if (_processingTask == null) + { + return; + } + + //We're not locking here so it's possible we won't throw even if the object has begun disposal. + //We handle this gracefully. + ThrowIfDisposed(); + + //If the channel is complete, we will not be able to write to it. + _metricChannel.Writer.TryWrite(metric); + } + + private async Task ProcessAllData(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + Metric metric = null; + try + { + metric = await _metricChannel.Reader.ReadAsync(token); + } + catch (ChannelClosedException) + { + return; + } + + try + { + await _metricsRestClient.SendMetric(metric, token); + } + catch (Exception e) when ((!(e is OperationCanceledException)) && (!(e is ObjectDisposedException))) + { + _logger.LogError(e, e.Message); + } + } + token.ThrowIfCancellationRequested(); + } + + private void ThrowIfDisposed() + { + if (Interlocked.CompareExchange(ref _disposed, value: 1, comparand: 1) == 1) + { + throw new ObjectDisposedException(nameof(MetricsLogger)); + } + } + + public void Dispose() + { + _ = DisposeAsync(); + } + + public async ValueTask DisposeAsync() + { + if (Interlocked.CompareExchange(ref _disposed, value: 1, comparand: 0) == 1) + { + return; + } + + //Do not allow any more entries. This should force ReadAsync to throw. + _metricChannel?.Writer.TryComplete(); + + //Finish processing + //TODO Consider limiting this to a certain amount of time. + if (_processingTask != null) + { + await _processingTask; + } + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + _metricsRestClient?.Dispose(); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/MetricsModel.cs b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/MetricsModel.cs new file mode 100644 index 000000000..310393429 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/MetricsModel.cs @@ -0,0 +1,38 @@ +// 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; + +namespace Microsoft.Diagnostics.Monitoring.LogAnalytics +{ + internal sealed class MetricSeries + { + public IReadOnlyList DimValues { get; set; } + public double Min { get; set; } + public double Max { get; set; } + public double Sum { get; set; } + public int Count { get; set; } + } + + internal sealed class MetricBaseData + { + public string Metric { get; set; } + public string Namespace { get; set; } + public IReadOnlyList DimNames { get; set; } + public IList Series { get; set; } = new List(); + } + + internal sealed class MetricData + { + public MetricBaseData BaseData { get; set; } = new MetricBaseData(); + } + + internal sealed class AggregatedMetric + { + public string Time { get; set; } + public MetricData Data { get; set; } = new MetricData(); + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/MetricsRestClient.cs b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/MetricsRestClient.cs new file mode 100644 index 000000000..d178f5840 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/MetricsRestClient.cs @@ -0,0 +1,70 @@ +// 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.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.LogAnalytics +{ + internal class MetricsRestClient : IDisposable + { + private readonly HttpClient _client; + private readonly ResourceConfiguration _resourceConfig; + + private CancellationTokenSource _tokenSource = new CancellationTokenSource(); + + public MetricsRestClient(MetricsConfiguration config, ResourceConfiguration resourceConfig) + { + _resourceConfig = resourceConfig; + _client = new HttpClient(new AuthenticationDelegatingHandler(config)); + } + + public async Task SendMetric(Metric metric, CancellationToken token) + { + string uri = FormattableString.Invariant($"https://{_resourceConfig.AzureRegion}.monitoring.azure.com{_resourceConfig.AzureResourceId}/metrics"); + + using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri); + + var aggregatedMetric = new AggregatedMetric(); + + aggregatedMetric.Data.BaseData.Namespace = metric.Namespace; + aggregatedMetric.Data.BaseData.Metric = metric.DisplayName + (string.IsNullOrEmpty(metric.Unit) ? string.Empty : $" ({metric.Unit})"); + + aggregatedMetric.Time = metric.Timestamp.ToString("o"); + aggregatedMetric.Data.BaseData.DimNames = metric.DimNames; + + var series = new MetricSeries + { + Count = 1, + Sum = metric.Value, + Min = metric.Value, + Max = metric.Value, + DimValues = metric.DimValues + }; + + aggregatedMetric.Data.BaseData.Series.Add(series); + + using var memoryStream = new MemoryStream(); + await JsonSerializer.SerializeAsync(memoryStream, aggregatedMetric, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }, token); + memoryStream.Position = 0L; + + StreamContent streamContent = new StreamContent(memoryStream); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + request.Content = streamContent; + + await _client.SendAsync(request, token); + } + + public void Dispose() + { + _client?.Dispose(); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/Microsoft.Diagnostics.Monitoring.LogAnalytics.csproj b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/Microsoft.Diagnostics.Monitoring.LogAnalytics.csproj new file mode 100644 index 000000000..baaeb84aa --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/Microsoft.Diagnostics.Monitoring.LogAnalytics.csproj @@ -0,0 +1,23 @@ + + + + netstandard2.0 + ;1591;1701 + Log Analytics Sink for dotnet monitoring + true + Diagnostic + $(Description) + false + true + true + + + + + + + + + + + diff --git a/src/Microsoft.Diagnostics.Monitoring/ContextConfiguration.cs b/src/Microsoft.Diagnostics.Monitoring/ContextConfiguration.cs new file mode 100644 index 000000000..4823f9f64 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring/ContextConfiguration.cs @@ -0,0 +1,21 @@ +// 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; + +namespace Microsoft.Diagnostics.Monitoring +{ + /// + /// Do not rename these fields. These are used to bind to the app's configuration. + /// + public class ContextConfiguration + { + public string Namespace { get; set; } + + public string Node { get; set; } + + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring/DiagnosticsMonitor.cs b/src/Microsoft.Diagnostics.Monitoring/DiagnosticsMonitor.cs new file mode 100644 index 000000000..1a8dee382 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring/DiagnosticsMonitor.cs @@ -0,0 +1,377 @@ +// 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 Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Diagnostics.Tracing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring +{ + public sealed class DiagnosticsMonitor : IAsyncDisposable + { + private readonly IServiceProvider _services; + private readonly Microsoft.Extensions.Logging.ILogger _logger; + private readonly MonitoringSourceConfiguration _sourceConfig; + private readonly IEnumerable _metricLoggers; + + //These values don't change so we compute them only once + private readonly List _dimValues; + + public const string NamespaceName = "Namespace"; + public const string NodeName = "Node"; + private static readonly List DimNames = new List{ NamespaceName, NodeName}; + + private int _disposeState = 0; + + public DiagnosticsMonitor(IServiceProvider services, MonitoringSourceConfiguration sourceConfig) + { + _services = services; + _sourceConfig = sourceConfig; + IOptions contextConfig = _services.GetService>(); + _dimValues = new List { contextConfig.Value.Namespace, contextConfig.Value.Node }; + _metricLoggers = _services.GetServices(); + _logger = _services.GetService>(); + } + + public async Task ProcessEvents(int processId, CancellationToken cancellationToken) + { + var hasEventPipe = false; + + for (int i = 0; i < 10; ++i) + { + if (DiagnosticsClient.GetPublishedProcesses().Contains(processId)) + { + hasEventPipe = true; + break; + } + + cancellationToken.ThrowIfCancellationRequested(); + + await Task.Delay(500); + } + + if (!hasEventPipe) + { + _logger.LogInformation("Process id {PID}, does not support event pipe", processId); + return; + } + + _logger.LogInformation("Listening for event pipe events for {ServiceName} on process id {PID}", _dimValues[1], processId); + + while (!cancellationToken.IsCancellationRequested) + { + EventPipeSession session = null; + var client = new DiagnosticsClient(processId); + + try + { + session = client.StartEventPipeSession(_sourceConfig.GetProviders()); + } + catch (EndOfStreamException) + { + break; + } + catch (Exception ex) + { + if (!cancellationToken.IsCancellationRequested) + { + _logger.LogDebug(0, ex, "Failed to start the event pipe session"); + } + + // We can't even start the session, wait until the process boots up again to start another metrics thread + break; + } + + void StopSession() + { + try + { + session.Stop(); + } + catch (EndOfStreamException) + { + // If the app we're monitoring exits abruptly, this may throw in which case we just swallow the exception and exit gracefully. + } + // We may time out if the process ended before we sent StopTracing command. We can just exit in that case. + catch (TimeoutException) + { + } + // On Unix platforms, we may actually get a PNSE since the pipe is gone with the process, and Runtime Client Library + // does not know how to distinguish a situation where there is no pipe to begin with, or where the process has exited + // before dotnet-counters and got rid of a pipe that once existed. + // Since we are catching this in StopMonitor() we know that the pipe once existed (otherwise the exception would've + // been thrown in StartMonitor directly) + catch (PlatformNotSupportedException) + { + } + } + + using var _ = cancellationToken.Register(() => StopSession()); + + try + { + var source = new EventPipeEventSource(session.EventStream); + + // Metrics + HandleEventCounters(source); + + // Logging + HandleLoggingEvents(source); + + source.Process(); + } + catch (DiagnosticsClientException ex) + { + _logger.LogDebug(0, ex, "Failed to start the event pipe session"); + } + catch (Exception) + { + // This fails if stop is called or if the process dies + } + finally + { + session?.Dispose(); + } + } + + _logger.LogInformation("Event pipe collection completed for {ServiceName} on process id {PID}", _dimValues[1], processId); + } + + private void HandleLoggingEvents(EventPipeEventSource source) + { + string lastFormattedMessage = string.Empty; + + var logActivities = new Dictionary(); + var stack = new Stack(); + + source.Dynamic.AddCallbackForProviderEvent(MonitoringSourceConfiguration.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 + { + ActivityID = traceEvent.ActivityID, + ScopedObject = new LogObject(JsonDocument.Parse(argsJson).RootElement), + }; + + if (stack.Count > 0) + { + Guid parentId = stack.Peek(); + if (logActivities.TryGetValue(parentId, out var parentItem)) + { + item.Parent = parentItem; + } + } + + stack.Push(traceEvent.ActivityID); + logActivities[traceEvent.ActivityID] = item; + }); + + source.Dynamic.AddCallbackForProviderEvent(MonitoringSourceConfiguration.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(MonitoringSourceConfiguration.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; + + ILogger logger = _services.GetService().CreateLogger(categoryName); + + var scopes = new List(); + + 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(exceptionJson); + exception = new LoggerException(exceptionMessage); + } + + var message = JsonSerializer.Deserialize(argsJson); + if (message.TryGetProperty("{OriginalFormat}", out var formatElement)) + { + var formatString = formatElement.GetString(); + var formatter = new LogValuesFormatter(formatString); + object[] args = new object[formatter.ValueNames.Count]; + for (int i = 0; i < args.Length; i++) + { + args[i] = message.GetProperty(formatter.ValueNames[i]).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}", _dimValues[1]); + } + finally + { + scopes.ForEach(d => d.Dispose()); + } + }); + + source.Dynamic.AddCallbackForProviderEvent(MonitoringSourceConfiguration.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 = string.Empty; + } + + lastFormattedMessage = formattedMessage; + }); + } + + private void HandleEventCounters(EventPipeEventSource source) + { + source.Dynamic.All += traceEvent => + { + try + { + // Metrics + if (traceEvent.EventName.Equals("EventCounters")) + { + IDictionary payloadVal = (IDictionary)(traceEvent.PayloadValue(0)); + IDictionary payloadFields = (IDictionary)(payloadVal["Payload"]); + + string counterName = payloadFields["Name"].ToString(); + string displayName = payloadFields["DisplayName"].ToString(); + string displayUnits = payloadFields["DisplayUnits"].ToString(); + double value = 0; + if (payloadFields["CounterType"].Equals("Mean")) + { + value = (double)payloadFields["Mean"]; + } + else if (payloadFields["CounterType"].Equals("Sum")) + { + value = (double)payloadFields["Increment"]; + if (string.IsNullOrEmpty(displayUnits)) + { + displayUnits = "count"; + } + displayUnits += "/sec"; + } + + PostMetric(new Metric(traceEvent.TimeStamp, traceEvent.ProviderName, counterName, displayName, displayUnits, value, dimNames: DimNames, dimValues: _dimValues)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing counter for {ProviderName}:{EventName}", traceEvent.ProviderName, traceEvent.EventName); + } + }; + } + + private void PostMetric(Metric metric) + { + foreach(IMetricsLogger metricLogger in _metricLoggers) + { + try + { + metricLogger.LogMetrics(metric); + } + catch (ObjectDisposedException) + { + } + catch (Exception e) + { + _logger.LogError($"Error from {metricLogger.GetType()}: {e.Message}"); + } + } + } + + public async ValueTask DisposeAsync() + { + if (Interlocked.CompareExchange(ref _disposeState, 1, 0) == 1) + { + return; + } + + foreach(IMetricsLogger logger in _metricLoggers) + { + if (logger is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + else + { + logger?.Dispose(); + } + } + } + + private class LogActivityItem + { + public Guid ActivityID { get; set; } + + public LogObject ScopedObject { get; set; } + + public LogActivityItem Parent { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.Monitoring/IMetricsLogger.cs b/src/Microsoft.Diagnostics.Monitoring/IMetricsLogger.cs new file mode 100644 index 000000000..2279ec413 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring/IMetricsLogger.cs @@ -0,0 +1,17 @@ +// 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; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring +{ + public interface IMetricsLogger : IDisposable + { + void LogMetrics(Metric metric); + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring/LogObject.cs b/src/Microsoft.Diagnostics.Monitoring/LogObject.cs new file mode 100644 index 000000000..974504a74 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring/LogObject.cs @@ -0,0 +1,72 @@ +// 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; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; + +namespace Microsoft.Diagnostics.Monitoring +{ + public class LogObject : IReadOnlyList> + { + public static readonly Func Callback = (state, exception) => ((LogObject)state).ToString(); + + private readonly string _formattedMessage; + private List> _items = new List>(); + + public LogObject(JsonElement element, string formattedMessage = null) + { + foreach (var item in element.EnumerateObject()) + { + switch (item.Value.ValueKind) + { + case JsonValueKind.Undefined: + break; + case JsonValueKind.Object: + break; + case JsonValueKind.Array: + break; + case JsonValueKind.String: + _items.Add(new KeyValuePair(item.Name, item.Value.GetString())); + break; + case JsonValueKind.Number: + _items.Add(new KeyValuePair(item.Name, item.Value.GetInt32())); + break; + case JsonValueKind.False: + case JsonValueKind.True: + _items.Add(new KeyValuePair(item.Name, item.Value.GetBoolean())); + break; + case JsonValueKind.Null: + _items.Add(new KeyValuePair(item.Name, null)); + break; + default: + break; + } + } + + _formattedMessage = formattedMessage; + } + + public KeyValuePair this[int index] => _items[index]; + + public int Count => _items.Count; + + public IEnumerator> GetEnumerator() + { + return _items.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public override string ToString() + { + return _formattedMessage ?? string.Empty; + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring/LogValuesFormatter.cs b/src/Microsoft.Diagnostics.Monitoring/LogValuesFormatter.cs new file mode 100644 index 000000000..59000d9de --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring/LogValuesFormatter.cs @@ -0,0 +1,200 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Globalization; +using System.Text; + +namespace Microsoft.Diagnostics.Monitoring +{ + /// + /// Formatter to convert the named format items like {NamedformatItem} to format. + /// + public class LogValuesFormatter + { + private const string NullValue = "(null)"; + private static readonly object[] EmptyArray = new object[0]; + private static readonly char[] FormatDelimiters = { ',', ':' }; + private readonly string _format; + private readonly List _valueNames = new List(); + + public LogValuesFormatter(string format) + { + OriginalFormat = format; + + var sb = new StringBuilder(); + var scanIndex = 0; + var endIndex = format.Length; + + while (scanIndex < endIndex) + { + var openBraceIndex = FindBraceIndex(format, '{', scanIndex, endIndex); + var closeBraceIndex = FindBraceIndex(format, '}', openBraceIndex, endIndex); + + if (closeBraceIndex == endIndex) + { + sb.Append(format, scanIndex, endIndex - scanIndex); + scanIndex = endIndex; + } + else + { + // Format item syntax : { index[,alignment][ :formatString] }. + var formatDelimiterIndex = FindIndexOfAny(format, FormatDelimiters, openBraceIndex, closeBraceIndex); + + sb.Append(format, scanIndex, openBraceIndex - scanIndex + 1); + sb.Append(_valueNames.Count.ToString(CultureInfo.InvariantCulture)); + _valueNames.Add(format.Substring(openBraceIndex + 1, formatDelimiterIndex - openBraceIndex - 1)); + sb.Append(format, formatDelimiterIndex, closeBraceIndex - formatDelimiterIndex + 1); + + scanIndex = closeBraceIndex + 1; + } + } + + _format = sb.ToString(); + } + + public string OriginalFormat { get; private set; } + public List ValueNames => _valueNames; + + private static int FindBraceIndex(string format, char brace, int startIndex, int endIndex) + { + // Example: {{prefix{{{Argument}}}suffix}}. + var braceIndex = endIndex; + var scanIndex = startIndex; + var braceOccurenceCount = 0; + + while (scanIndex < endIndex) + { + if (braceOccurenceCount > 0 && format[scanIndex] != brace) + { + if (braceOccurenceCount % 2 == 0) + { + // Even number of '{' or '}' found. Proceed search with next occurence of '{' or '}'. + braceOccurenceCount = 0; + braceIndex = endIndex; + } + else + { + // An unescaped '{' or '}' found. + break; + } + } + else if (format[scanIndex] == brace) + { + if (brace == '}') + { + if (braceOccurenceCount == 0) + { + // For '}' pick the first occurence. + braceIndex = scanIndex; + } + } + else + { + // For '{' pick the last occurence. + braceIndex = scanIndex; + } + + braceOccurenceCount++; + } + + scanIndex++; + } + + return braceIndex; + } + + private static int FindIndexOfAny(string format, char[] chars, int startIndex, int endIndex) + { + var findIndex = format.IndexOfAny(chars, startIndex, endIndex - startIndex); + return findIndex == -1 ? endIndex : findIndex; + } + + public string Format(object[] values) + { + if (values != null) + { + for (int i = 0; i < values.Length; i++) + { + values[i] = FormatArgument(values[i]); + } + } + + return string.Format(CultureInfo.InvariantCulture, _format, values ?? EmptyArray); + } + + internal string Format() + { + return _format; + } + + internal string Format(object arg0) + { + return string.Format(CultureInfo.InvariantCulture, _format, FormatArgument(arg0)); + } + + internal string Format(object arg0, object arg1) + { + return string.Format(CultureInfo.InvariantCulture, _format, FormatArgument(arg0), FormatArgument(arg1)); + } + + internal string Format(object arg0, object arg1, object arg2) + { + return string.Format(CultureInfo.InvariantCulture, _format, FormatArgument(arg0), FormatArgument(arg1), FormatArgument(arg2)); + } + + public KeyValuePair GetValue(object[] values, int index) + { + if (index < 0 || index > _valueNames.Count) + { + throw new IndexOutOfRangeException(nameof(index)); + } + + if (_valueNames.Count > index) + { + return new KeyValuePair(_valueNames[index], values[index]); + } + + return new KeyValuePair("{OriginalFormat}", OriginalFormat); + } + + public IEnumerable> GetValues(object[] values) + { + var valueArray = new KeyValuePair[values.Length + 1]; + for (var index = 0; index != _valueNames.Count; ++index) + { + valueArray[index] = new KeyValuePair(_valueNames[index], values[index]); + } + + valueArray[valueArray.Length - 1] = new KeyValuePair("{OriginalFormat}", OriginalFormat); + return valueArray; + } + + private object FormatArgument(object value) + { + if (value == null) + { + return NullValue; + } + + // since 'string' implements IEnumerable, special case it + if (value is string) + { + return value; + } + + // if the value implements IEnumerable, build a comma separated string. + var enumerable = value as IEnumerable; + if (enumerable != null) + { + return string.Join(", ", enumerable.Cast().Select(o => o ?? NullValue)); + } + + return value; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.Monitoring/LoggerException.cs b/src/Microsoft.Diagnostics.Monitoring/LoggerException.cs new file mode 100644 index 000000000..6869fb387 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring/LoggerException.cs @@ -0,0 +1,48 @@ +// 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; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Text; +using System.Text.Json; + +namespace Microsoft.Diagnostics.Monitoring +{ + public class LoggerException : Exception + { + private readonly JsonElement _exceptionMessage; + + public LoggerException(JsonElement exceptionMessage) + { + _exceptionMessage = exceptionMessage; + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue("ClassName", _exceptionMessage.GetProperty("TypeName").GetString(), typeof(string)); // Do not rename (binary serialization) + info.AddValue("Message", Message, typeof(string)); // Do not rename (binary serialization) + info.AddValue("Data", Data, typeof(IDictionary)); // Do not rename (binary serialization) + info.AddValue("InnerException", null, typeof(Exception)); // Do not rename (binary serialization) + info.AddValue("HelpURL", null, typeof(string)); // Do not rename (binary serialization) + info.AddValue("StackTraceString", StackTrace, typeof(string)); // Do not rename (binary serialization) + info.AddValue("RemoteStackTraceString", StackTrace, typeof(string)); // Do not rename (binary serialization) + info.AddValue("RemoteStackIndex", 0, typeof(int)); // Do not rename (binary serialization) + info.AddValue("ExceptionMethod", null, typeof(string)); // Do not rename (binary serialization) + info.AddValue("HResult", int.Parse(_exceptionMessage.GetProperty("HResult").GetString())); // Do not rename (binary serialization) + info.AddValue("Source", Source, typeof(string)); // Do not rename (binary serialization + info.AddValue("WatsonBuckets", null, typeof(byte[])); // Do not rename (binary serialization) + } + + public override string Message => _exceptionMessage.GetProperty("Message").GetString(); + + public override string StackTrace => _exceptionMessage.GetProperty("VerboseMessage").GetString(); + + public override string ToString() + { + return _exceptionMessage.GetProperty("VerboseMessage").GetString(); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring/Metric.cs b/src/Microsoft.Diagnostics.Monitoring/Metric.cs new file mode 100644 index 000000000..7e40b6478 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring/Metric.cs @@ -0,0 +1,53 @@ +// 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; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Microsoft.Diagnostics.Monitoring +{ + public enum MetricType + { + Avg, + Sum, + Min, + Max + } + + public class Metric + { + public Metric(DateTime timestamp, string metricNamespace, string name, string displayName, string unit, double value, IReadOnlyList dimNames, IReadOnlyList dimValues, MetricType metricType = MetricType.Avg) + { + Timestamp = timestamp; + Name = name; + DisplayName = displayName; + Unit = unit; + Value = value; + MetricType = metricType; + Namespace = metricNamespace; + DimNames = dimNames; + DimValues = dimValues; + } + + public IReadOnlyList DimNames { get; } + + public IReadOnlyList DimValues { get; } + + public string Namespace { get; } + + public MetricType MetricType { get; } + + public string Name { get; } + + public string DisplayName { get; } + + public string Unit { get; } + + public double Value { get; } + + public DateTime Timestamp { get; } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring/Microsoft.Diagnostics.Monitoring.csproj b/src/Microsoft.Diagnostics.Monitoring/Microsoft.Diagnostics.Monitoring.csproj new file mode 100644 index 000000000..4df6b32f6 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring/Microsoft.Diagnostics.Monitoring.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.0 + ;1591;1701 + Monitoring for dotnet + true + Diagnostic + $(Description) + false + true + true + + + + + + + + + + + + diff --git a/src/Microsoft.Diagnostics.Monitoring/MonitoringSourceConfiguration.cs b/src/Microsoft.Diagnostics.Monitoring/MonitoringSourceConfiguration.cs new file mode 100644 index 000000000..82ae464ee --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring/MonitoringSourceConfiguration.cs @@ -0,0 +1,166 @@ +// 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 Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Diagnostics.Tracing.Parsers; +using System; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Globalization; +using System.Text; + +namespace Microsoft.Diagnostics.Monitoring +{ + public class MonitoringSourceConfiguration + { + public const string MicrosoftExtensionsLoggingProviderName = "Microsoft-Extensions-Logging"; + public const string SystemRuntimeEventSourceName = "System.Runtime"; + public const string MicrosoftAspNetCoreHostingEventSourceName = "Microsoft.AspNetCore.Hosting"; + public const string GrpcAspNetCoreServer = "Grpc.AspNetCore.Server"; + public const string DiagnosticSourceEventSource = "Microsoft-Diagnostics-DiagnosticSource"; + public const string TplEventSource = "System.Threading.Tasks.TplEventSource"; + + public const 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 MonitoringSourceConfiguration(int metricIntervalSeconds = 60) + { + MetricIntervalSeconds = metricIntervalSeconds.ToString(CultureInfo.InvariantCulture); + } + + protected virtual string MetricIntervalSeconds { get; } + + public virtual IList GetProviders() + { + var providers = new List() + { + // Runtime Metrics + new EventPipeProvider( + SystemRuntimeEventSourceName, + EventLevel.Informational, + (long)ClrTraceEventParser.Keywords.None, + new Dictionary() { + { "EventCounterIntervalSec", MetricIntervalSeconds } + } + ), + new EventPipeProvider( + MicrosoftAspNetCoreHostingEventSourceName, + EventLevel.Informational, + (long)ClrTraceEventParser.Keywords.None, + new Dictionary() { + { "EventCounterIntervalSec", MetricIntervalSeconds } + } + ), + new EventPipeProvider( + GrpcAspNetCoreServer, + EventLevel.Informational, + (long)ClrTraceEventParser.Keywords.None, + new Dictionary() { + { "EventCounterIntervalSec", MetricIntervalSeconds } + } + ), + + // Application Metrics + //new EventPipeProvider( + // applicationName, + // EventLevel.Informational, + // (long)ClrTraceEventParser.Keywords.None, + // new Dictionary() { + // { "EventCounterIntervalSec", MetricIntervalSeconds } + // } + //), + + // 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 + { + { "FilterAndPayloadSpecs", DiagnosticFilterString } + }) + }; + + return providers; + } + + internal sealed class LoggingEventSource + { + /// + /// This is public from an EventSource consumer point of view, but since these defintions + /// are not needed outside this class + /// + public static class Keywords + { + /// + /// Meta events are events about the LoggingEventSource itself (that is they did not come from ILogger + /// + public const EventKeywords Meta = (EventKeywords)1; + /// + /// Turns on the 'Message' event when ILogger.Log() is called. It gives the information in a programmatic (not formatted) way + /// + public const EventKeywords Message = (EventKeywords)2; + /// + /// Turns on the 'FormatMessage' event when ILogger.Log() is called. It gives the formatted string version of the information. + /// + public const EventKeywords FormattedMessage = (EventKeywords)4; + /// + /// Turns on the 'MessageJson' event when ILogger.Log() is called. It gives JSON representation of the Arguments. + /// + public const EventKeywords JsonMessage = (EventKeywords)8; + } + } + } +} diff --git a/src/Tools/dotnet-monitor/ConsoleMetricsLogger.cs b/src/Tools/dotnet-monitor/ConsoleMetricsLogger.cs new file mode 100644 index 000000000..aa7f8a02c --- /dev/null +++ b/src/Tools/dotnet-monitor/ConsoleMetricsLogger.cs @@ -0,0 +1,23 @@ +// 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 Microsoft.Diagnostics.Monitoring; +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + internal sealed class ConsoleMetricsLogger : IMetricsLogger + { + public void LogMetrics(Metric metric) + { + string json = JsonSerializer.Serialize(metric, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true }); + Console.WriteLine(json); + } + public void Dispose() + { + } + } +} diff --git a/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs b/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs new file mode 100644 index 000000000..35cd99d0a --- /dev/null +++ b/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs @@ -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 Microsoft.Diagnostics.Monitoring; +using Microsoft.Diagnostics.Monitoring.LogAnalytics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + internal sealed class DiagnosticsMonitorCommandHandler + { + private sealed class ConsoleLoggerAdapter : ILogger + { + private readonly IConsole _console; + + private sealed class EmptyScope : IDisposable + { + public static EmptyScope Instance { get; } = new EmptyScope(); + + public void Dispose() { } + } + + public ConsoleLoggerAdapter(IConsole console) + { + _console = console; + } + + public IDisposable BeginScope(TState state) + { + return EmptyScope.Instance; + } + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + _console.Out.WriteLine((formatter != null) ? formatter.Invoke(state, exception) : state?.ToString()); + } + } + + public async Task Start(CancellationToken token, IConsole console, int processId, int refreshInterval, SinkType sink, IEnumerable jsonConfigs, IEnumerable keyFileConfigs) + { + //CONSIDER The console sink uses the standard AddConsole, and therefore disregards IConsole. + + ServiceCollection services = new ServiceCollection(); + ConfigurationBuilder builder = new ConfigurationBuilder(); + + if (jsonConfigs != null) + { + foreach (FileInfo jsonFile in jsonConfigs) + { + builder.SetBasePath(jsonFile.DirectoryName).AddJsonFile(jsonFile.Name, optional: true); + } + } + if (keyFileConfigs != null) + { + foreach (FileInfo keyFileConfig in keyFileConfigs) + { + console.Out.WriteLine(keyFileConfig.FullName); + builder.AddKeyPerFile(keyFileConfig.FullName, optional: true); + } + } + + ConfigureNames(builder); + + IConfigurationRoot config = builder.Build(); + services.AddSingleton(config); + + //Specialized logger for diagnostic output from the service itself rather than as a sink for the data + services.AddSingleton>((sp) => new ConsoleLoggerAdapter(console)); + + if (sink.HasFlag(SinkType.Console)) + { + services.AddSingleton(); + } + if (sink.HasFlag(SinkType.LogAnalytics)) + { + services.AddSingleton(); + } + + services.AddLogging(builder => + { + if (sink.HasFlag(SinkType.Console)) + { + builder.AddConsole(); + } + if (sink.HasFlag(SinkType.LogAnalytics)) + { + builder.AddProvider(new LogAnalyticsLoggerProvider()); + } + }); + services.Configure(config); + if (sink.HasFlag(SinkType.LogAnalytics)) + { + services.Configure(config); + services.Configure(config); + } + + await using var monitor = new DiagnosticsMonitor(services.BuildServiceProvider(), new MonitoringSourceConfiguration(refreshInterval)); + await monitor.ProcessEvents(processId, token); + + return 0; + } + + private void ConfigureNames(IConfigurationBuilder builder) + { + string hostName = Environment.GetEnvironmentVariable("HOSTNAME"); + if (string.IsNullOrEmpty(hostName)) + { + hostName = Environment.MachineName; + } + string namespaceName = null; + try + { + namespaceName = File.ReadAllText(@"/var/run/secrets/kubernetes.io/serviceaccount/namespace"); + } + catch + { + } + + if (string.IsNullOrEmpty(namespaceName)) + { + namespaceName = "default"; + } + + builder.AddInMemoryCollection(new Dictionary { { DiagnosticsMonitor.NamespaceName, namespaceName }, { DiagnosticsMonitor.NodeName, hostName } }); + + } + } +} diff --git a/src/Tools/dotnet-monitor/Program.cs b/src/Tools/dotnet-monitor/Program.cs new file mode 100644 index 000000000..7977cd745 --- /dev/null +++ b/src/Tools/dotnet-monitor/Program.cs @@ -0,0 +1,93 @@ +// 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 Microsoft.Internal.Common.Commands; +using Microsoft.Tools.Common; +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Builder; +using System.CommandLine.Invocation; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + [Flags] + internal enum SinkType + { + None = 0, + Console = 1, + LogAnalytics = 2, + All = 0xff + } + + class Program + { + private static Command CollectCommand() => + new Command( + name: "collect", + description: "Monitor logs and metrics in a .NET application send the results to a chosen destination.") + { + // Handler + CommandHandler.Create, IEnumerable>(new DiagnosticsMonitorCommandHandler().Start), + // Arguments and Options + ProcessIdOption(), RefreshIntervalOption(), SinkOption(), JsonConfigOption(), FileConfigOption() + }; + + private static Option ProcessIdOption() => + new Option( + aliases: new[] { "-p", "--process-id" }, + description: "The process id that will be monitored.") + { + Argument = new Argument(name: "processId") + }; + + private static Option RefreshIntervalOption() => + new Option( + alias: "--refresh-interval", + description: "The number of seconds to delay between updating the counters.") + { + Argument = new Argument(name: "refreshInterval", defaultValue: 10) + }; + + private static Option SinkOption() => + new Option( + alias: "--sink", + description: "Where to send the data") + { + Argument = new Argument(name: "sink", defaultValue: SinkType.Console) + }; + + private static Option JsonConfigOption() => + new Option( + alias: "--json-configs", + description: "Additonal configuration") + { + Argument = new Argument>(name: "jsonConfigs"), + Required = false, + }; + + private static Option FileConfigOption() => + new Option( + alias: "--keyfile-configs", + description: "Additonal configuration") + { + Argument = new Argument>(name: "keyFileConfigs"), + Required = false + }; + + + public static Task Main(string[] args) + { + var parser = new CommandLineBuilder() + .AddCommand(CollectCommand()) + .AddCommand(ProcessStatusCommandHandler.ProcessStatusCommand("Lists the dotnet processes that can be monitored")) + .UseDefaults() + .Build(); + return parser.InvokeAsync(args); + } + } +} diff --git a/src/Tools/dotnet-monitor/dotnet-monitor.csproj b/src/Tools/dotnet-monitor/dotnet-monitor.csproj new file mode 100644 index 000000000..99d9b65fd --- /dev/null +++ b/src/Tools/dotnet-monitor/dotnet-monitor.csproj @@ -0,0 +1,29 @@ + + + + netcoreapp2.1 + Microsoft.Diagnostics.Tools.Monitor + dotnet-monitor + .NET Core Diagnostic Monitoring Tool + Diagnostic + $(Description) + + + + + + + + + + + + + + + + + + + + diff --git a/src/Tools/dotnet-monitor/runtimeconfig.template.json b/src/Tools/dotnet-monitor/runtimeconfig.template.json new file mode 100644 index 000000000..f022b7ffc --- /dev/null +++ b/src/Tools/dotnet-monitor/runtimeconfig.template.json @@ -0,0 +1,3 @@ +{ + "rollForwardOnNoCandidateFx": 2 +} \ No newline at end of file