Create dotnet-monitor tool (#878)
authorWiktor Kopec <wiktork@microsoft.com>
Mon, 9 Mar 2020 23:41:31 +0000 (16:41 -0700)
committerGitHub <noreply@github.com>
Mon, 9 Mar 2020 23:41:31 +0000 (16:41 -0700)
* 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.

25 files changed:
diagnostics.sln
src/Microsoft.Diagnostics.Monitoring.LogAnalytics/AuthModel.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring.LogAnalytics/AuthenticatingHandler.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring.LogAnalytics/LogAnalyticsLogger.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring.LogAnalytics/LogAnalyticsLoggerProvider.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring.LogAnalytics/LogRestClient.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring.LogAnalytics/MetricsConfiguration.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring.LogAnalytics/MetricsLogger.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring.LogAnalytics/MetricsModel.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring.LogAnalytics/MetricsRestClient.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring.LogAnalytics/Microsoft.Diagnostics.Monitoring.LogAnalytics.csproj [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring/ContextConfiguration.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring/DiagnosticsMonitor.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring/IMetricsLogger.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring/LogObject.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring/LogValuesFormatter.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring/LoggerException.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring/Metric.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring/Microsoft.Diagnostics.Monitoring.csproj [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring/MonitoringSourceConfiguration.cs [new file with mode: 0644]
src/Tools/dotnet-monitor/ConsoleMetricsLogger.cs [new file with mode: 0644]
src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs [new file with mode: 0644]
src/Tools/dotnet-monitor/Program.cs [new file with mode: 0644]
src/Tools/dotnet-monitor/dotnet-monitor.csproj [new file with mode: 0644]
src/Tools/dotnet-monitor/runtimeconfig.template.json [new file with mode: 0644]

index 5f400e0e97f8bb24a7bb1243bac1654c0d5320d1..e273acca748f147b895e959a22f61090dbfa41b3 100644 (file)
@@ -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 (file)
index 0000000..7b61da2
--- /dev/null
@@ -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 (file)
index 0000000..07f9e71
--- /dev/null
@@ -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<HttpResponseMessage> 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<AuthenticationHeaderValue> AcquireBearerToken(CancellationToken cancellationToken)
+        {
+            using var httpclient = new HttpClient();
+
+            var formValues = new Dictionary<string, string>();
+            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<AuthResult>(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 (file)
index 0000000..b4d31e3
--- /dev/null
@@ -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>(TState state)
+        {
+            return new EmptyScopes();
+        }
+
+        public bool IsEnabled(LogLevel logLevel)
+        {
+            return true;
+        }
+
+        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
+        {
+            return;
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/LogAnalyticsLoggerProvider.cs b/src/Microsoft.Diagnostics.Monitoring.LogAnalytics/LogAnalyticsLoggerProvider.cs
new file mode 100644 (file)
index 0000000..e7c2da1
--- /dev/null
@@ -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 (file)
index 0000000..a527b3f
--- /dev/null
@@ -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 (file)
index 0000000..fdc547d
--- /dev/null
@@ -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
+{
+    /// <summary>
+    /// Do not rename these fields. These are used to bind to the app's configuration.
+    /// </summary>
+    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 (file)
index 0000000..4cee6aa
--- /dev/null
@@ -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<DiagnosticsMonitor> _logger;
+        private readonly MetricsConfiguration _metricConfig;
+        private readonly ResourceConfiguration _resourceConfig;
+
+        private readonly Channel<Metric> _metricChannel;
+        private readonly CancellationTokenSource _cancellationTokenSource;
+        private readonly MetricsRestClient _metricsRestClient;
+        private readonly Task _processingTask;
+
+        private int _disposed = 0;
+
+        public MetricsLogger(ILogger<DiagnosticsMonitor> logger,
+            IOptions<MetricsConfiguration> metricsConfig,
+            IOptions<ResourceConfiguration> 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<Metric>();
+            _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 (file)
index 0000000..3103934
--- /dev/null
@@ -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<string> 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<string> DimNames { get; set; }
+        public IList<MetricSeries> Series { get; set; } = new List<MetricSeries>();
+    }
+
+    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 (file)
index 0000000..d178f58
--- /dev/null
@@ -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 (file)
index 0000000..baaeb84
--- /dev/null
@@ -0,0 +1,23 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <NoWarn>;1591;1701</NoWarn>
+    <Description>Log Analytics Sink for dotnet monitoring</Description>
+    <IsPackable>true</IsPackable>
+    <PackageTags>Diagnostic</PackageTags>
+    <PackageReleaseNotes>$(Description)</PackageReleaseNotes>
+    <GenerateDocumentationFile>false</GenerateDocumentationFile>
+    <IncludeSymbols>true</IncludeSymbols>
+    <IsShippingAssembly>true</IsShippingAssembly>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="System.Text.Json" Version="4.7.1" />
+    <PackageReference Include="System.Threading.Channels" Version="4.7.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Microsoft.Diagnostics.Monitoring\Microsoft.Diagnostics.Monitoring.csproj" />
+  </ItemGroup>
+</Project>
diff --git a/src/Microsoft.Diagnostics.Monitoring/ContextConfiguration.cs b/src/Microsoft.Diagnostics.Monitoring/ContextConfiguration.cs
new file mode 100644 (file)
index 0000000..4823f9f
--- /dev/null
@@ -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
+{
+    /// <summary>
+    /// Do not rename these fields. These are used to bind to the app's configuration.
+    /// </summary>
+    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 (file)
index 0000000..1a8dee3
--- /dev/null
@@ -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<DiagnosticsMonitor> _logger;
+        private readonly MonitoringSourceConfiguration _sourceConfig;
+        private readonly IEnumerable<IMetricsLogger> _metricLoggers;
+
+        //These values don't change so we compute them only once
+        private readonly List<string> _dimValues;
+
+        public const string NamespaceName = "Namespace";
+        public const string NodeName = "Node";
+        private static readonly List<string> DimNames = new List<string>{ NamespaceName, NodeName};
+
+        private int _disposeState = 0;
+
+        public DiagnosticsMonitor(IServiceProvider services, MonitoringSourceConfiguration sourceConfig)
+        {
+            _services = services;
+            _sourceConfig = sourceConfig;
+            IOptions<ContextConfiguration> contextConfig = _services.GetService<IOptions<ContextConfiguration>>();
+            _dimValues = new List<string> { contextConfig.Value.Namespace, contextConfig.Value.Node };
+            _metricLoggers = _services.GetServices<IMetricsLogger>();
+            _logger = _services.GetService<ILogger<DiagnosticsMonitor>>();
+        }
+
+        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<Guid, LogActivityItem>();
+            var stack = new Stack<Guid>();
+
+            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<ILoggerFactory>().CreateLogger(categoryName);
+
+                var scopes = new List<IDisposable>();
+
+                if (logActivities.TryGetValue(traceEvent.ActivityID, out var logActivityItem))
+                {
+                    // REVIEW: Does order matter here? We're combining everything anyways.
+                    while (logActivityItem != null)
+                    {
+                        scopes.Add(logger.BeginScope(logActivityItem.ScopedObject));
+
+                        logActivityItem = logActivityItem.Parent;
+                    }
+                }
+
+                try
+                {
+                    if (exceptionJson != "{}")
+                    {
+                        var exceptionMessage = JsonSerializer.Deserialize<JsonElement>(exceptionJson);
+                        exception = new LoggerException(exceptionMessage);
+                    }
+
+                    var message = JsonSerializer.Deserialize<JsonElement>(argsJson);
+                    if (message.TryGetProperty("{OriginalFormat}", out var formatElement))
+                    {
+                        var formatString = formatElement.GetString();
+                        var formatter = new LogValuesFormatter(formatString);
+                        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<string, object> payloadVal = (IDictionary<string, object>)(traceEvent.PayloadValue(0));
+                        IDictionary<string, object> payloadFields = (IDictionary<string, object>)(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 (file)
index 0000000..2279ec4
--- /dev/null
@@ -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 (file)
index 0000000..974504a
--- /dev/null
@@ -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<KeyValuePair<string, object>>
+    {
+        public static readonly Func<object, Exception, string> Callback = (state, exception) => ((LogObject)state).ToString();
+
+        private readonly string _formattedMessage;
+        private List<KeyValuePair<string, object>> _items = new List<KeyValuePair<string, object>>();
+
+        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<string, object>(item.Name, item.Value.GetString()));
+                        break;
+                    case JsonValueKind.Number:
+                        _items.Add(new KeyValuePair<string, object>(item.Name, item.Value.GetInt32()));
+                        break;
+                    case JsonValueKind.False:
+                    case JsonValueKind.True:
+                        _items.Add(new KeyValuePair<string, object>(item.Name, item.Value.GetBoolean()));
+                        break;
+                    case JsonValueKind.Null:
+                        _items.Add(new KeyValuePair<string, object>(item.Name, null));
+                        break;
+                    default:
+                        break;
+                }
+            }
+
+            _formattedMessage = formattedMessage;
+        }
+
+        public KeyValuePair<string, object> this[int index] => _items[index];
+
+        public int Count => _items.Count;
+
+        public IEnumerator<KeyValuePair<string, object>> 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 (file)
index 0000000..59000d9
--- /dev/null
@@ -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
+{
+    /// <summary>
+    /// Formatter to convert the named format items like {NamedformatItem} to <see cref="M:string.Format"/> format.
+    /// </summary>
+    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<string> _valueNames = new List<string>();
+
+        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<string> 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<string, object> GetValue(object[] values, int index)
+        {
+            if (index < 0 || index > _valueNames.Count)
+            {
+                throw new IndexOutOfRangeException(nameof(index));
+            }
+
+            if (_valueNames.Count > index)
+            {
+                return new KeyValuePair<string, object>(_valueNames[index], values[index]);
+            }
+
+            return new KeyValuePair<string, object>("{OriginalFormat}", OriginalFormat);
+        }
+
+        public IEnumerable<KeyValuePair<string, object>> GetValues(object[] values)
+        {
+            var valueArray = new KeyValuePair<string, object>[values.Length + 1];
+            for (var index = 0; index != _valueNames.Count; ++index)
+            {
+                valueArray[index] = new KeyValuePair<string, object>(_valueNames[index], values[index]);
+            }
+
+            valueArray[valueArray.Length - 1] = new KeyValuePair<string, object>("{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<object>().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 (file)
index 0000000..6869fb3
--- /dev/null
@@ -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 (file)
index 0000000..7e40b64
--- /dev/null
@@ -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<string> dimNames, IReadOnlyList<string> 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<string> DimNames { get; }
+
+        public IReadOnlyList<string> 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 (file)
index 0000000..4df6b32
--- /dev/null
@@ -0,0 +1,24 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <NoWarn>;1591;1701</NoWarn>
+    <Description>Monitoring for dotnet</Description>
+    <IsPackable>true</IsPackable>
+    <PackageTags>Diagnostic</PackageTags>
+    <PackageReleaseNotes>$(Description)</PackageReleaseNotes>
+    <GenerateDocumentationFile>false</GenerateDocumentationFile>
+    <IncludeSymbols>true</IncludeSymbols>
+    <IsShippingAssembly>true</IsShippingAssembly>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="$(MicrosoftDiagnosticsTracingTraceEventVersion)" />
+    <PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.2" />
+    <PackageReference Include="System.Text.Json" Version="4.7.1" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Microsoft.Diagnostics.NETCore.Client\Microsoft.Diagnostics.NETCore.Client.csproj" />
+  </ItemGroup>
+</Project>
diff --git a/src/Microsoft.Diagnostics.Monitoring/MonitoringSourceConfiguration.cs b/src/Microsoft.Diagnostics.Monitoring/MonitoringSourceConfiguration.cs
new file mode 100644 (file)
index 0000000..82ae464
--- /dev/null
@@ -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<EventPipeProvider> GetProviders()
+        {
+            var providers = new List<EventPipeProvider>()
+            {
+                // Runtime Metrics
+                new EventPipeProvider(
+                    SystemRuntimeEventSourceName,
+                    EventLevel.Informational,
+                    (long)ClrTraceEventParser.Keywords.None,
+                    new Dictionary<string, string>() {
+                        { "EventCounterIntervalSec", MetricIntervalSeconds }
+                    }
+                ),
+                new EventPipeProvider(
+                    MicrosoftAspNetCoreHostingEventSourceName,
+                    EventLevel.Informational,
+                    (long)ClrTraceEventParser.Keywords.None,
+                    new Dictionary<string, string>() {
+                        { "EventCounterIntervalSec", MetricIntervalSeconds }
+                    }
+                ),
+                new EventPipeProvider(
+                    GrpcAspNetCoreServer,
+                    EventLevel.Informational,
+                    (long)ClrTraceEventParser.Keywords.None,
+                    new Dictionary<string, string>() {
+                        { "EventCounterIntervalSec", MetricIntervalSeconds }
+                    }
+                ),
+                
+                // Application Metrics
+                //new EventPipeProvider(
+                //    applicationName,
+                //    EventLevel.Informational,
+                //    (long)ClrTraceEventParser.Keywords.None,
+                //    new Dictionary<string, string>() {
+                //        { "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<string,string>
+                        {
+                            { "FilterAndPayloadSpecs", DiagnosticFilterString }
+                        })
+            };
+
+            return providers;
+        }
+
+        internal sealed class LoggingEventSource
+        {
+            /// <summary>
+            /// This is public from an EventSource consumer point of view, but since these defintions
+            /// are not needed outside this class
+            /// </summary>
+            public static class Keywords
+            {
+                /// <summary>
+                /// Meta events are events about the LoggingEventSource itself (that is they did not come from ILogger
+                /// </summary>
+                public const EventKeywords Meta = (EventKeywords)1;
+                /// <summary>
+                /// Turns on the 'Message' event when ILogger.Log() is called.   It gives the information in a programmatic (not formatted) way
+                /// </summary>
+                public const EventKeywords Message = (EventKeywords)2;
+                /// <summary>
+                /// Turns on the 'FormatMessage' event when ILogger.Log() is called.  It gives the formatted string version of the information.
+                /// </summary>
+                public const EventKeywords FormattedMessage = (EventKeywords)4;
+                /// <summary>
+                /// Turns on the 'MessageJson' event when ILogger.Log() is called.   It gives  JSON representation of the Arguments.
+                /// </summary>
+                public const EventKeywords JsonMessage = (EventKeywords)8;
+            }
+        }
+    }
+}
diff --git a/src/Tools/dotnet-monitor/ConsoleMetricsLogger.cs b/src/Tools/dotnet-monitor/ConsoleMetricsLogger.cs
new file mode 100644 (file)
index 0000000..aa7f8a0
--- /dev/null
@@ -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 (file)
index 0000000..35cd99d
--- /dev/null
@@ -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<DiagnosticsMonitor>
+        {
+            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>(TState state)
+            {
+                return EmptyScope.Instance;
+            }
+
+            public bool IsEnabled(LogLevel logLevel) => true;
+
+            public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
+            {
+                _console.Out.WriteLine((formatter != null) ? formatter.Invoke(state, exception) : state?.ToString());
+            }
+        }
+
+        public async Task<int> Start(CancellationToken token, IConsole console, int processId, int refreshInterval, SinkType sink, IEnumerable<FileInfo> jsonConfigs, IEnumerable<FileInfo> 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<IConfiguration>(config);
+
+            //Specialized logger for diagnostic output from the service itself rather than as a sink for the data
+            services.AddSingleton<ILogger<DiagnosticsMonitor>>((sp) => new ConsoleLoggerAdapter(console));
+
+            if (sink.HasFlag(SinkType.Console))
+            {
+                services.AddSingleton<IMetricsLogger, ConsoleMetricsLogger>();
+            }
+            if (sink.HasFlag(SinkType.LogAnalytics))
+            {
+                services.AddSingleton<IMetricsLogger, MetricsLogger>();
+            }
+
+            services.AddLogging(builder =>
+                {
+                    if (sink.HasFlag(SinkType.Console))
+                    {
+                        builder.AddConsole();
+                    }
+                    if (sink.HasFlag(SinkType.LogAnalytics))
+                    {
+                        builder.AddProvider(new LogAnalyticsLoggerProvider());
+                    }
+                });
+            services.Configure<ContextConfiguration>(config);
+            if (sink.HasFlag(SinkType.LogAnalytics))
+            {
+                services.Configure<MetricsConfiguration>(config);
+                services.Configure<ResourceConfiguration>(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<string, string> { { 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 (file)
index 0000000..7977cd7
--- /dev/null
@@ -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<CancellationToken, IConsole, int, int, SinkType, IEnumerable<FileInfo>, IEnumerable<FileInfo>>(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<int>(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<int>(name: "refreshInterval", defaultValue: 10)
+            };
+
+        private static Option SinkOption() =>
+            new Option(
+                alias: "--sink",
+                description: "Where to send the data")
+            {
+                Argument = new Argument<SinkType>(name: "sink", defaultValue: SinkType.Console)
+            };
+
+        private static Option JsonConfigOption() =>
+        new Option(
+            alias: "--json-configs",
+            description: "Additonal configuration")
+        {
+            Argument = new Argument<IEnumerable<FileInfo>>(name: "jsonConfigs"),
+            Required = false,
+        };
+
+        private static Option FileConfigOption() =>
+        new Option(
+            alias: "--keyfile-configs",
+            description: "Additonal configuration")
+        {
+            Argument = new Argument<IEnumerable<FileInfo>>(name: "keyFileConfigs"),
+            Required = false
+        };
+
+
+        public static Task<int> 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 (file)
index 0000000..99d9b65
--- /dev/null
@@ -0,0 +1,29 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <RootNamespace>Microsoft.Diagnostics.Tools.Monitor</RootNamespace>
+    <ToolCommandName>dotnet-monitor</ToolCommandName>
+    <Description>.NET Core Diagnostic Monitoring Tool</Description>
+    <PackageTags>Diagnostic</PackageTags>
+    <PackageReleaseNotes>$(Description)</PackageReleaseNotes>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.2" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.KeyPerFile" Version="3.1.2" />
+    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.2" />
+    <PackageReference Include="System.CommandLine.Experimental" Version="$(SystemCommandLineExperimentalVersion)" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Compile Include="..\Common\CommandExtensions.cs" Link="CommandExtensions.cs" />
+    <Compile Include="..\Common\Commands\ProcessStatus.cs" Link="ProcessStatus.cs" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\Microsoft.Diagnostics.Monitoring.LogAnalytics\Microsoft.Diagnostics.Monitoring.LogAnalytics.csproj" />
+    <ProjectReference Include="..\..\Microsoft.Diagnostics.Monitoring\Microsoft.Diagnostics.Monitoring.csproj" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/Tools/dotnet-monitor/runtimeconfig.template.json b/src/Tools/dotnet-monitor/runtimeconfig.template.json
new file mode 100644 (file)
index 0000000..f022b7f
--- /dev/null
@@ -0,0 +1,3 @@
+{
+  "rollForwardOnNoCandidateFx": 2
+}
\ No newline at end of file