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
{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
{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}
--- /dev/null
+// 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; }
+ }
+}
--- /dev/null
+// 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);
+ }
+ }
+}
--- /dev/null
+// 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;
+ }
+ }
+}
--- /dev/null
+// 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()
+ {
+ }
+ }
+}
--- /dev/null
+// 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
+ {
+ }
+}
--- /dev/null
+// 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; }
+ }
+}
--- /dev/null
+// 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();
+ }
+ }
+}
--- /dev/null
+// 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();
+ }
+}
--- /dev/null
+// 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();
+ }
+ }
+}
--- /dev/null
+<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>
--- /dev/null
+// 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; }
+
+ }
+}
--- /dev/null
+// 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
--- /dev/null
+// 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);
+ }
+}
--- /dev/null
+// 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;
+ }
+ }
+}
--- /dev/null
+// 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
--- /dev/null
+// 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();
+ }
+ }
+}
--- /dev/null
+// 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; }
+ }
+}
--- /dev/null
+<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>
--- /dev/null
+// 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;
+ }
+ }
+ }
+}
--- /dev/null
+// 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()
+ {
+ }
+ }
+}
--- /dev/null
+// 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 } });
+
+ }
+ }
+}
--- /dev/null
+// 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);
+ }
+ }
+}
--- /dev/null
+<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>
--- /dev/null
+{
+ "rollForwardOnNoCandidateFx": 2
+}
\ No newline at end of file