Log common actions and exceptions with artifact type and process information.
Refactor host builder to route lifetime events through logging and include event log by default.
Consolidate process acquisition for DiagController with logging scopes.
Only write experiment message through logging and always enable its category.
Add correlation and process data as blob metadata.
// 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.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Diagnostics.NETCore.Client;
+using Microsoft.Extensions.Logging;
using System;
using System.ComponentModel.DataAnnotations;
using System.Net;
{
internal static class ActionContextExtensions
{
+ private const string ExceptionLogMessage = "Request failed.";
+
public static Task ProblemAsync(this ActionContext context, Exception ex)
{
- ActionResult result = new BadRequestObjectResult(ex.ToProblemDetails((int)HttpStatusCode.BadRequest));
+ if (context.HttpContext.Features.Get<IHttpResponseFeature>().HasStarted)
+ {
+ // If already started writing response, do not rewrite
+ // as this will throw an InvalidOperationException.
+ return Task.CompletedTask;
+ }
+ else
+ {
+ ActionResult result = new BadRequestObjectResult(ex.ToProblemDetails((int)HttpStatusCode.BadRequest));
- return result.ExecuteResultAsync(context);
+ return result.ExecuteResultAsync(context);
+ }
}
- public static async Task InvokeAsync(this ActionContext context, Func<CancellationToken, Task> action)
+ public static async Task InvokeAsync(this ActionContext context, Func<CancellationToken, Task> action, ILogger logger)
{
+ CancellationToken token = context.HttpContext.RequestAborted;
+ // Exceptions are logged in the "when" clause in order to preview the exception
+ // from the point of where it was thrown. This allows capturing of the log scopes
+ // that were active when the exception was thrown. Waiting to log during the exception
+ // handler will miss any scopes that were added during invocation of action.
try
{
- await action(context.HttpContext.RequestAborted);
+ await action(token);
+ }
+ catch (ArgumentException ex) when (LogError(logger, ex))
+ {
+ await context.ProblemAsync(ex);
}
- catch (ArgumentException ex)
+ catch (DiagnosticsClientException ex) when (LogError(logger, ex))
{
await context.ProblemAsync(ex);
}
- catch (DiagnosticsClientException ex)
+ catch (InvalidOperationException ex) when (LogError(logger, ex))
{
await context.ProblemAsync(ex);
}
- catch (InvalidOperationException ex)
+ catch (OperationCanceledException ex) when (token.IsCancellationRequested && LogInformation(logger, ex))
{
await context.ProblemAsync(ex);
}
- catch (OperationCanceledException ex)
+ catch (OperationCanceledException ex) when (LogError(logger, ex))
{
await context.ProblemAsync(ex);
}
- catch (MonitoringException ex)
+ catch (MonitoringException ex) when (LogError(logger, ex))
{
await context.ProblemAsync(ex);
}
- catch (ValidationException ex)
+ catch (ValidationException ex) when (LogError(logger, ex))
{
await context.ProblemAsync(ex);
}
- catch (UnauthorizedAccessException ex)
+ catch (UnauthorizedAccessException ex) when (LogError(logger, ex))
{
await context.ProblemAsync(ex);
}
}
+
+ private static bool LogError(ILogger logger, Exception ex)
+ {
+ logger.LogError(ex, ExceptionLogMessage);
+ return true;
+ }
+
+ private static bool LogInformation(ILogger logger, Exception ex)
+ {
+ logger.LogInformation(ex.Message);
+ return true;
+ }
}
}
--- /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.
+
+namespace Microsoft.Diagnostics.Monitoring.RestServer
+{
+ /// <summary>
+ /// Metadata keys that represent artfiact information.
+ /// </summary>
+ internal static class ArtifactMetadataNames
+ {
+ /// <summary>
+ /// Represents the type of artifact created from the source.
+ /// </summary>
+ public const string ArtifactType = nameof(ArtifactType);
+
+ /// <summary>
+ /// Metadata keus that represent the source of an artifact.
+ /// </summary>
+ public static class ArtifactSource
+ {
+ /// <summary>
+ /// The ID of the process from which the artifact was collected.
+ /// </summary>
+ public const string ProcessId = nameof(ArtifactSource) + "_" + nameof(ProcessId);
+
+ /// <summary>
+ /// The runtime instance cookie of the process from which the artifact was collected.
+ /// </summary>
+ public const string RuntimeInstanceCookie = nameof(ArtifactSource) + "_" + nameof(RuntimeInstanceCookie);
+ }
+ }
+}
[HostRestriction]
public class DiagController : ControllerBase
{
+ private const string ArtifactType_Dump = "dump";
+ private const string ArtifactType_GCDump = "gcdump";
+ private const string ArtifactType_Logs = "logs";
+ private const string ArtifactType_Trace = "trace";
+
private const TraceProfile DefaultTraceProfiles = TraceProfile.Cpu | TraceProfile.Http | TraceProfile.Metrics;
private static readonly MediaTypeHeaderValue NdJsonHeader = new MediaTypeHeaderValue(ContentTypes.ApplicationNdJson);
private static readonly MediaTypeHeaderValue EventStreamHeader = new MediaTypeHeaderValue(ContentTypes.TextEventStream);
processesIdentifiers.Add(ProcessIdentifierModel.FromProcessInfo(p));
}
return new ActionResult<IEnumerable<ProcessIdentifierModel>>(processesIdentifiers);
- });
+ }, _logger);
}
[HttpGet("processes/{processFilter}")]
public Task<ActionResult<ProcessModel>> GetProcess(
ProcessFilter processFilter)
{
- return this.InvokeService<ProcessModel>(async () =>
- {
- IProcessInfo processInfo = await _diagnosticServices.GetProcessAsync(
- processFilter,
- HttpContext.RequestAborted);
-
- return ProcessModel.FromProcessInfo(processInfo);
- });
+ return InvokeForProcess<ProcessModel>(
+ processInfo => ProcessModel.FromProcessInfo(processInfo),
+ processFilter);
}
[HttpGet("processes/{processFilter}/env")]
public Task<ActionResult<Dictionary<string, string>>> GetProcessEnvironment(
ProcessFilter processFilter)
{
- return this.InvokeService<Dictionary<string, string>>(async () =>
+ return InvokeForProcess<Dictionary<string, string>>(processInfo =>
{
- IProcessInfo processInfo = await _diagnosticServices.GetProcessAsync(
- processFilter,
- HttpContext.RequestAborted);
-
var client = new DiagnosticsClient(processInfo.EndpointInfo.Endpoint);
try
{
throw new InvalidOperationException("Unable to get process environment.");
}
- });
+ },
+ processFilter);
}
[HttpGet("dump/{processFilter?}")]
[FromQuery] DumpType type = DumpType.WithHeap,
[FromQuery] string egressProvider = null)
{
- return this.InvokeService(async () =>
+ return InvokeForProcess(async processInfo =>
{
- IProcessInfo processInfo = await _diagnosticServices.GetProcessAsync(processFilter, HttpContext.RequestAborted);
-
string dumpFileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
FormattableString.Invariant($"dump_{GetFileNameTimeStampUtcNow()}.dmp") :
FormattableString.Invariant($"core_{GetFileNameTimeStampUtcNow()}");
}
else
{
+ KeyValueLogScope scope = new KeyValueLogScope();
+ scope.AddArtifactType(ArtifactType_Dump);
+ scope.AddEndpointInfo(processInfo.EndpointInfo);
+
return new EgressStreamResult(
token => _diagnosticServices.GetDump(processInfo, type, token),
egressProvider,
dumpFileName,
processInfo.EndpointInfo,
- ContentTypes.ApplicationOctectStream);
+ ContentTypes.ApplicationOctectStream,
+ scope);
}
- });
+ }, processFilter, ArtifactType_Dump);
}
[HttpGet("gcdump/{processFilter?}")]
ProcessFilter? processFilter,
[FromQuery] string egressProvider = null)
{
- return this.InvokeService(async () =>
+ return InvokeForProcess(processInfo =>
{
- IProcessInfo processInfo = await _diagnosticServices.GetProcessAsync(processFilter, HttpContext.RequestAborted);
-
string fileName = FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{processInfo.EndpointInfo.ProcessId}.gcdump");
Func<CancellationToken, Task<IFastSerializable>> action = async (token) => {
};
return Result(
+ ArtifactType_GCDump,
egressProvider,
ConvertFastSerializeAction(action),
fileName,
ContentTypes.ApplicationOctectStream,
processInfo.EndpointInfo);
- });
+ }, processFilter, ArtifactType_GCDump);
}
[HttpGet("trace/{processFilter?}")]
[FromQuery][Range(1, int.MaxValue)] int metricsIntervalSeconds = 1,
[FromQuery] string egressProvider = null)
{
- TimeSpan duration = ConvertSecondsToTimeSpan(durationSeconds);
-
- return this.InvokeService(async () =>
+ return InvokeForProcess(processInfo =>
{
+ TimeSpan duration = ConvertSecondsToTimeSpan(durationSeconds);
+
var configurations = new List<MonitoringSourceConfiguration>();
if (profile.HasFlag(TraceProfile.Cpu))
{
var aggregateConfiguration = new AggregateSourceConfiguration(configurations.ToArray());
- return await StartTrace(processFilter, aggregateConfiguration, duration, egressProvider);
- });
+ return StartTrace(processInfo, aggregateConfiguration, duration, egressProvider);
+ }, processFilter, ArtifactType_Trace);
}
[HttpPost("trace/{processFilter?}")]
[FromQuery][Range(-1, int.MaxValue)] int durationSeconds = 30,
[FromQuery] string egressProvider = null)
{
- TimeSpan duration = ConvertSecondsToTimeSpan(durationSeconds);
-
- return this.InvokeService(async () =>
+ return InvokeForProcess(processInfo =>
{
+ TimeSpan duration = ConvertSecondsToTimeSpan(durationSeconds);
+
var providers = new List<EventPipeProvider>();
foreach (EventPipeProviderModel providerModel in configuration.Providers)
requestRundown: configuration.RequestRundown,
bufferSizeInMB: configuration.BufferSizeInMB);
- return await StartTrace(processFilter, traceConfiguration, duration, egressProvider);
- });
+ return StartTrace(processInfo, traceConfiguration, duration, egressProvider);
+ }, processFilter, ArtifactType_Trace);
}
[HttpGet("logs/{processFilter?}")]
[FromQuery] LogLevel level = LogLevel.Debug,
[FromQuery] string egressProvider = null)
{
- TimeSpan duration = ConvertSecondsToTimeSpan(durationSeconds);
- return this.InvokeService(async () =>
+ return InvokeForProcess(processInfo =>
{
- IProcessInfo processInfo = await _diagnosticServices.GetProcessAsync(processFilter, HttpContext.RequestAborted);
+ TimeSpan duration = ConvertSecondsToTimeSpan(durationSeconds);
LogFormat format = ComputeLogFormat(Request.GetTypedHeaders().Accept);
if (format == LogFormat.None)
};
return Result(
+ ArtifactType_Logs,
egressProvider,
action,
fileName,
contentType,
processInfo.EndpointInfo,
format != LogFormat.EventStream);
- });
+ }, processFilter, ArtifactType_Logs);
}
- private async Task<ActionResult> StartTrace(
- ProcessFilter? processFilter,
+ private ActionResult StartTrace(
+ IProcessInfo processInfo,
MonitoringSourceConfiguration configuration,
TimeSpan duration,
string egressProvider)
{
- IProcessInfo processInfo = await _diagnosticServices.GetProcessAsync(processFilter, HttpContext.RequestAborted);
-
string fileName = FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{processInfo.EndpointInfo.ProcessId}.nettrace");
Func<Stream, CancellationToken, Task> action = async (outputStream, token) =>
};
return Result(
+ ArtifactType_Trace,
egressProvider,
action,
fileName,
return LogFormat.None;
}
- private static ActionResult Result(
+ private ActionResult Result(
+ string artifactType,
string providerName,
Func<Stream, CancellationToken, Task> action,
string fileName,
IEndpointInfo endpointInfo,
bool asAttachment = true)
{
+ KeyValueLogScope scope = new KeyValueLogScope();
+ scope.AddArtifactType(artifactType);
+ scope.AddEndpointInfo(endpointInfo);
+
if (string.IsNullOrEmpty(providerName))
{
return new OutputStreamResult(
action,
contentType,
- asAttachment ? fileName : null);
+ asAttachment ? fileName : null,
+ scope);
}
else
{
providerName,
fileName,
endpointInfo,
- contentType);
+ contentType,
+ scope);
}
}
}
};
}
+
+ private Task<ActionResult> InvokeForProcess(Func<IProcessInfo, ActionResult> func, ProcessFilter? filter, string artifactType = null)
+ {
+ Func<IProcessInfo, Task<ActionResult>> asyncFunc =
+ processInfo => Task.FromResult(func(processInfo));
+
+ return InvokeForProcess(asyncFunc, filter, artifactType);
+ }
+
+ private async Task<ActionResult> InvokeForProcess(Func<IProcessInfo, Task<ActionResult>> func, ProcessFilter? filter, string artifactType)
+ {
+ ActionResult<object> result = await InvokeForProcess<object>(async processInfo => await func(processInfo), filter, artifactType);
+
+ return result.Result;
+ }
+
+ private Task<ActionResult<T>> InvokeForProcess<T>(Func<IProcessInfo, ActionResult<T>> func, ProcessFilter? filter, string artifactType = null)
+ {
+ return InvokeForProcess(processInfo => Task.FromResult(func(processInfo)), filter, artifactType);
+ }
+
+ private async Task<ActionResult<T>> InvokeForProcess<T>(Func<IProcessInfo, Task<ActionResult<T>>> func, ProcessFilter? filter, string artifactType = null)
+ {
+ IDisposable artifactTypeRegistration = null;
+ if (!string.IsNullOrEmpty(artifactType))
+ {
+ KeyValueLogScope artifactTypeScope = new KeyValueLogScope();
+ artifactTypeScope.AddArtifactType(artifactType);
+ artifactTypeRegistration = _logger.BeginScope(artifactTypeScope);
+ }
+
+ try
+ {
+ return await this.InvokeService(async () =>
+ {
+ IProcessInfo processInfo = await _diagnosticServices.GetProcessAsync(filter, HttpContext.RequestAborted);
+
+ KeyValueLogScope processInfoScope = new KeyValueLogScope();
+ processInfoScope.AddEndpointInfo(processInfo.EndpointInfo);
+ using var _ = _logger.BeginScope(processInfoScope);
+
+ _logger.LogDebug("Resolved target process.");
+
+ return await func(processInfo);
+ }, _logger);
+ }
+ finally
+ {
+ artifactTypeRegistration?.Dispose();
+ }
+ }
}
}
using Microsoft.AspNetCore.Mvc;
using Microsoft.Diagnostics.NETCore.Client;
+using Microsoft.Extensions.Logging;
using System;
using System.ComponentModel.DataAnnotations;
using System.Net;
+using System.Threading;
using System.Threading.Tasks;
// For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
{
internal static class DiagControllerExtensions
{
+ private const string ExceptionLogMessage = "Request failed.";
+
public static ActionResult NotAcceptable(this ControllerBase controller)
{
return new StatusCodeResult((int)HttpStatusCode.NotAcceptable);
}
- public static ActionResult InvokeService(this ControllerBase controller, Func<ActionResult> serviceCall)
+ public static ActionResult InvokeService(this ControllerBase controller, Func<ActionResult> serviceCall, ILogger logger)
{
//We can convert ActionResult to ActionResult<T>
//and then safely convert back.
- return controller.InvokeService<object>(() => serviceCall()).Result;
+ return controller.InvokeService<object>(() => serviceCall(), logger).Result;
}
- public static ActionResult<T> InvokeService<T>(this ControllerBase controller, Func<ActionResult<T>> serviceCall)
+ public static ActionResult<T> InvokeService<T>(this ControllerBase controller, Func<ActionResult<T>> serviceCall, ILogger logger)
{
//Convert from ActionResult<T> to Task<ActionResult<T>>
//and safely convert back.
- return controller.InvokeService(() => Task.FromResult(serviceCall())).Result;
+ return controller.InvokeService(() => Task.FromResult(serviceCall()), logger).Result;
}
- public static async Task<ActionResult> InvokeService(this ControllerBase controller, Func<Task<ActionResult>> serviceCall)
+ public static async Task<ActionResult> InvokeService(this ControllerBase controller, Func<Task<ActionResult>> serviceCall, ILogger logger)
{
//Task<ActionResult> -> Task<ActionResult<T>>
//Then unwrap the result back to ActionResult
- ActionResult<object> result = await controller.InvokeService<object>(async () => await serviceCall());
+ ActionResult<object> result = await controller.InvokeService<object>(async () => await serviceCall(), logger);
return result.Result;
}
- public static async Task<ActionResult<T>> InvokeService<T>(this ControllerBase controller, Func<Task<ActionResult<T>>> serviceCall)
+ public static async Task<ActionResult<T>> InvokeService<T>(this ControllerBase controller, Func<Task<ActionResult<T>>> serviceCall, ILogger logger)
{
+ CancellationToken token = controller.HttpContext.RequestAborted;
+ // Exceptions are logged in the "when" clause in order to preview the exception
+ // from the point of where it was thrown. This allows capturing of the log scopes
+ // that were active when the exception was thrown. Waiting to log during the exception
+ // handler will miss any scopes that were added during invocation of serviceCall.
try
{
return await serviceCall();
}
- catch (ArgumentException e)
+ catch (ArgumentException e) when (LogError(logger, e))
{
return controller.Problem(e);
}
- catch (DiagnosticsClientException e)
+ catch (DiagnosticsClientException e) when (LogError(logger, e))
{
return controller.Problem(e);
}
- catch (InvalidOperationException e)
+ catch (InvalidOperationException e) when (LogError(logger, e))
{
return controller.Problem(e);
}
- catch (OperationCanceledException e)
+ catch (OperationCanceledException e) when (token.IsCancellationRequested && LogInformation(logger, e))
{
return controller.Problem(e);
}
- catch (MonitoringException e)
+ catch (OperationCanceledException e) when (LogError(logger, e))
{
return controller.Problem(e);
}
- catch (ValidationException e)
+ catch (MonitoringException e) when (LogError(logger, e))
+ {
+ return controller.Problem(e);
+ }
+ catch (ValidationException e) when (LogError(logger, e))
{
return controller.Problem(e);
}
{
return controller.BadRequest(ex.ToProblemDetails((int)HttpStatusCode.BadRequest));
}
+
+ private static bool LogError(ILogger logger, Exception ex)
+ {
+ logger.LogError(ex, ExceptionLogMessage);
+ return true;
+ }
+
+ private static bool LogInformation(ILogger logger, Exception ex)
+ {
+ logger.LogInformation(ex.Message);
+ return true;
+ }
}
}
[ApiController]
public class MetricsController : ControllerBase
{
+ private const string ArtifactType_Metrics = "metrics";
+
private readonly ILogger<MetricsController> _logger;
private readonly MetricsStoreService _metricsStore;
private readonly MetricsOptions _metricsOptions;
throw new InvalidOperationException("Metrics was not enabled");
}
+ KeyValueLogScope scope = new KeyValueLogScope();
+ scope.AddArtifactType(ArtifactType_Metrics);
+
return new OutputStreamResult(async (outputStream, token) =>
- {
- await _metricsStore.MetricsStore.SnapshotMetrics(outputStream, token);
- }, "text/plain; version=0.0.4");
- });
+ {
+ await _metricsStore.MetricsStore.SnapshotMetrics(outputStream, token);
+ },
+ "text/plain; version=0.0.4",
+ null,
+ scope);
+ }, _logger);
}
}
}
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
internal class EgressStreamResult : ActionResult
{
private readonly Func<IEgressService, CancellationToken, Task<EgressResult>> _egress;
+ private readonly KeyValueLogScope _scope;
- public EgressStreamResult(Func<CancellationToken, Task<Stream>> action, string endpointName, string artifactName, IEndpointInfo source, string contentType)
+ public EgressStreamResult(Func<CancellationToken, Task<Stream>> action, string endpointName, string artifactName, IEndpointInfo source, string contentType, KeyValueLogScope scope)
{
_egress = (service, token) => service.EgressAsync(endpointName, action, artifactName, contentType, source, token);
+ _scope = scope;
}
- public EgressStreamResult(Func<Stream, CancellationToken, Task> action, string endpointName, string artifactName, IEndpointInfo source, string contentType)
+ public EgressStreamResult(Func<Stream, CancellationToken, Task> action, string endpointName, string artifactName, IEndpointInfo source, string contentType, KeyValueLogScope scope)
{
_egress = (service, token) => service.EgressAsync(endpointName, action, artifactName, contentType, source, token);
+ _scope = scope;
}
- public override Task ExecuteResultAsync(ActionContext context)
+ public override async Task ExecuteResultAsync(ActionContext context)
{
- return context.InvokeAsync(async (token) =>
+ ILogger<EgressStreamResult> logger = context.HttpContext.RequestServices
+ .GetRequiredService<ILoggerFactory>()
+ .CreateLogger<EgressStreamResult>();
+
+ using var _ = logger.BeginScope(_scope);
+
+ await context.InvokeAsync(async (token) =>
{
IEgressService egressService = context.HttpContext.RequestServices
.GetRequiredService<IEgressService>();
EgressResult egressResult = await _egress(egressService, token);
+ logger.LogInformation("Egressed to {0}", egressResult.Value);
+
// The remaining code is creating a JSON object with a single property and scalar value
// that indiates where the stream data was egressed. Because the name of the artifact is
// automatically generated by the REST API and the caller of the endpoint might not know
ActionResult jsonResult = new JsonResult(data);
await jsonResult.ExecuteResultAsync(context);
- });
+ }, logger);
}
}
}
--- /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.Collections;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Microsoft.Diagnostics.Monitoring.RestServer
+{
+ // Logger implementations have different ways of serializing log scopes. This class helps those loggers
+ // serialize the scope information in the best way possible for each of the implementations. For example,
+ // the console logger will only call ToString on the scope data, thus the data needs to be formatted appropriately
+ // in the ToString method. Another example, the event log logger will check if the scope data impelements
+ // IEnumerable<KeyValuePair<string, object>> and then formats each value from the enumeration; it will fallback
+ // calling the ToString method otherwise.
+ internal class KeyValueLogScope : IEnumerable<KeyValuePair<string, object>>
+ {
+ public IDictionary<string, object> Values =
+ new Dictionary<string, object>();
+
+ IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
+ {
+ return Values.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable)Values).GetEnumerator();
+ }
+
+ public override string ToString()
+ {
+ StringBuilder builder = new StringBuilder();
+ foreach (var kvp in Values)
+ {
+ if (builder.Length > 0)
+ {
+ builder.Append(" ");
+ }
+ builder.Append(kvp.Key);
+ builder.Append(":");
+ builder.Append(kvp.Value);
+ }
+ return builder.ToString();
+ }
+ }
+}
--- /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.Globalization;
+
+namespace Microsoft.Diagnostics.Monitoring.RestServer
+{
+ internal static class KeyValueLogScopeExtensions
+ {
+ public static void AddArtifactType(this KeyValueLogScope scope, string artifactType)
+ {
+ scope.Values.Add("ArtifactType", artifactType);
+ }
+
+ public static void AddEndpointInfo(this KeyValueLogScope scope, IEndpointInfo endpointInfo)
+ {
+ scope.Values.Add(
+ ArtifactMetadataNames.ArtifactSource.ProcessId,
+ endpointInfo.ProcessId.ToString(CultureInfo.InvariantCulture));
+ scope.Values.Add(
+ ArtifactMetadataNames.ArtifactSource.RuntimeInstanceCookie,
+ endpointInfo.RuntimeInstanceCookie.ToString("N"));
+ }
+ }
+}
// See the LICENSE file in the project root for more information.
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
using System;
+using System.Collections.Generic;
using System.IO;
using System.Net.Http.Headers;
using System.Threading;
private readonly Func<Stream, CancellationToken, Task> _action;
private readonly string _contentType;
private readonly string _fileDownloadName;
+ private readonly KeyValueLogScope _scope;
- public OutputStreamResult(Func<Stream, CancellationToken, Task> action, string contentType, string fileDownloadName = null)
+ public OutputStreamResult(Func<Stream, CancellationToken, Task> action, string contentType, string fileDownloadName, KeyValueLogScope scope)
{
_contentType = contentType;
_fileDownloadName = fileDownloadName;
_action = action;
+ _scope = scope;
}
- public override Task ExecuteResultAsync(ActionContext context)
+ public override async Task ExecuteResultAsync(ActionContext context)
{
- return context.InvokeAsync(async (token) =>
+ ILogger<OutputStreamResult> logger = context.HttpContext.RequestServices
+ .GetRequiredService<ILoggerFactory>()
+ .CreateLogger<OutputStreamResult>();
+
+ using var _ = logger.BeginScope(_scope);
+
+ await context.InvokeAsync(async (token) =>
{
if (_fileDownloadName != null)
{
#endif
await _action(context.HttpContext.Response.Body, token);
- });
+
+ logger.LogInformation("Written to HTTP stream.");
+ }, logger);
}
}
}
{
"Logging": {
"LogLevel": {
- "Default": "Warning"
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ },
+ "Console": {
+ "IncludeScopes": true,
+ "TimestampFormat": "HH:mm:ss "
+ },
+ "EventLog": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
}
},
"AllowedHosts": "*"
--- /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.Diagnostics;
+
+namespace Microsoft.Diagnostics.Tools.Monitor
+{
+ internal static class ActivityExtensions
+ {
+ public static string GetSpanId(this Activity activity)
+ {
+ switch (activity.IdFormat)
+ {
+ case ActivityIdFormat.Hierarchical:
+ return activity.Id;
+ case ActivityIdFormat.W3C:
+ return activity.SpanId.ToHexString();
+ }
+ return string.Empty;
+ }
+
+ public static string GetParentId(this Activity activity)
+ {
+ switch (activity.IdFormat)
+ {
+ case ActivityIdFormat.Hierarchical:
+ return activity.ParentId;
+ case ActivityIdFormat.W3C:
+ return activity.ParentSpanId.ToHexString();
+ }
+ return string.Empty;
+ }
+
+ public static string GetTraceId(this Activity activity)
+ {
+ switch (activity.IdFormat)
+ {
+ case ActivityIdFormat.Hierarchical:
+ return activity.RootId;
+ case ActivityIdFormat.W3C:
+ return activity.TraceId.ToHexString();
+ }
+ return 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.
+
+namespace Microsoft.Diagnostics.Tools.Monitor
+{
+ /// <summary>
+ /// Metadata keys that correspond to <see cref="System.Diagnostics.Activity"/> properties.
+ /// </summary>
+ internal static class ActivityMetadataNames
+ {
+ /// <summary>
+ /// Represents the ID of the parent activity.
+ /// </summary>
+ /// <remarks>
+ /// This name is the same as logged by the ActivityLogScope.
+ /// </remarks>
+ public const string ParentId = nameof(ParentId);
+
+ /// <summary>
+ /// Represents the ID of the current activity.
+ /// </summary>
+ /// <remarks>
+ /// This name is the same as logged by the ActivityLogScope.
+ /// </remarks>
+ public const string SpanId = nameof(SpanId);
+
+ /// <summary>
+ /// Represents the trace ID of the activity.
+ /// </summary>
+ /// <remarks>
+ /// This name is the same as logged by the ActivityLogScope.
+ /// </remarks>
+ public const string TraceId = nameof(TraceId);
+ }
+}
using Microsoft.Diagnostics.Monitoring.RestServer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.CommandLine;
public async Task<int> Start(CancellationToken token, IConsole console, string[] urls, string[] metricUrls, bool metrics, string diagnosticPort)
{
//CONSIDER The console logger uses the standard AddConsole, and therefore disregards IConsole.
- using IWebHost host = CreateWebHostBuilder(console, urls, metricUrls, metrics, diagnosticPort).Build();
+ using IHost host = CreateHostBuilder(console, urls, metricUrls, metrics, diagnosticPort).Build();
await host.RunAsync(token);
return 0;
}
- public IWebHostBuilder CreateWebHostBuilder(IConsole console, string[] urls, string[] metricUrls, bool metrics, string diagnosticPort)
+ public IHostBuilder CreateHostBuilder(IConsole console, string[] urls, string[] metricUrls, bool metrics, string diagnosticPort)
{
if (metrics)
{
urls = urls.Concat(metricUrls).ToArray();
}
- IWebHostBuilder builder = WebHost.CreateDefaultBuilder()
+ return Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((IConfigurationBuilder builder) =>
{
//Note these are in precedence order.
builder.AddKeyPerFile(SharedConfigDirectoryPath, optional: true);
builder.AddEnvironmentVariables(ConfigPrefix);
})
- .ConfigureServices((WebHostBuilderContext context, IServiceCollection services) =>
+ .ConfigureServices((HostBuilderContext context, IServiceCollection services) =>
{
//TODO Many of these service additions should be done through extension methods
services.Configure<DiagnosticPortOptions>(context.Configuration.GetSection(DiagnosticPortOptions.ConfigurationKey));
{
services.ConfigureMetrics(context.Configuration);
}
+ services.AddSingleton<ExperimentalToolLogger>();
})
- .UseUrls(urls)
- .UseStartup<Startup>();
-
- return builder;
+ .ConfigureLogging(builder =>
+ {
+ // Always allow the experimental tool message to be logged
+ ExperimentalToolLogger.AddLogFilter(builder);
+ })
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseUrls(urls);
+ webBuilder.UseStartup<Startup>();
+ });
}
private static void ConfigureMetricsEndpoint(IConfigurationBuilder builder, string[] metricEndpoints)
Logger?.LogDebug("End uploading to storage with headers and metadata.");
string blobUriString = GetBlobUri(blobClient);
- Logger?.LogInformation("Uploaded stream to {0}", blobUriString);
+ Logger?.LogDebug("Uploaded stream to {0}", blobUriString);
return blobUriString;
}
catch (AggregateException ex) when (ex.InnerException is RequestFailedException innerException)
Logger?.LogDebug("End writing metadata.");
string blobUriString = GetBlobUri(blobClient);
- Logger?.LogInformation("Uploaded stream to {0}", blobUriString);
+ Logger?.LogDebug("Uploaded stream to {0}", blobUriString);
return blobUriString;
}
catch (AggregateException ex) when (ex.InnerException is RequestFailedException innerException)
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
IEndpointInfo source,
CancellationToken token)
{
- // TODO: Add metadata based on source
var streamOptions = new AzureBlobEgressStreamOptions();
streamOptions.ContentType = contentType;
+ FillBlobMetadata(streamOptions.Metadata, source);
string blobUri = await _provider.EgressAsync(action, fileName, streamOptions, token);
IEndpointInfo source,
CancellationToken token)
{
- // TODO: Add metadata based on source
var streamOptions = new AzureBlobEgressStreamOptions();
streamOptions.ContentType = contentType;
+ FillBlobMetadata(streamOptions.Metadata, source);
string blobUri = await _provider.EgressAsync(action, fileName, streamOptions, token);
return new EgressResult("uri", blobUri);
}
+
+ private static void FillBlobMetadata(IDictionary<string, string> metadata, IEndpointInfo source)
+ {
+ // Activity metadata
+ Activity activity = Activity.Current;
+ if (null != activity)
+ {
+ metadata.Add(
+ ActivityMetadataNames.ParentId,
+ activity.GetParentId());
+ metadata.Add(
+ ActivityMetadataNames.SpanId,
+ activity.GetSpanId());
+ metadata.Add(
+ ActivityMetadataNames.TraceId,
+ activity.GetTraceId());
+ }
+
+ // Artifact metadata
+ metadata.Add(
+ ArtifactMetadataNames.ArtifactSource.ProcessId,
+ source.ProcessId.ToString(CultureInfo.InvariantCulture));
+ metadata.Add(
+ ArtifactMetadataNames.ArtifactSource.RuntimeInstanceCookie,
+ source.RuntimeInstanceCookie.ToString("N"));
+ }
}
/// <summary>
// 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.RestServer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
{
string providerName = providerSection.Key;
- using var providerNameScope = _logger.BeginScope(new Dictionary<string, string>() { { "ProviderName", providerName } });
+ KeyValueLogScope providerNameScope = new KeyValueLogScope();
+ providerNameScope.Values.Add("EgressProviderName", providerName);
+ using var providerNameRegistration = _logger.BeginScope(providerNameScope);
CommonEgressProviderOptions commonOptions = new CommonEgressProviderOptions();
providerSection.Bind(commonOptions);
}
string providerType = commonOptions.Type;
- using var providerTypeScope = _logger.BeginScope(new Dictionary<string, string>() { { "ProviderType", providerType } });
+ KeyValueLogScope providerTypeScope = new KeyValueLogScope();
+ providerTypeScope.Values.Add("EgressProviderType", providerType);
+ using var providerTypeRegistration = _logger.BeginScope(providerTypeScope);
if (!_factories.TryGetValue(providerType, out EgressFactory factory))
{
options.Providers.Add(providerName, provider);
- _logger.LogInformation("Added egress provider '{0}'.", providerName);
+ _logger.LogDebug("Added egress provider '{0}'.", providerName);
}
_logger.LogDebug("End loading egress providers.");
}
await WriteFileAsync(action, targetPath, token);
}
- Logger?.LogInformation("Saved stream to '{0}.", targetPath);
+ Logger?.LogDebug("Saved stream to '{0}.", targetPath);
return targetPath;
}
--- /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;
+
+namespace Microsoft.Diagnostics.Tools.Monitor
+{
+ // FUTURE: This log message should be removed when dotnet-monitor is no longer an experimental tool
+ internal class ExperimentalToolLogger
+ {
+ private const string ExperimentMessage = "WARNING: dotnet-monitor is experimental and is not intended for production environments yet.";
+
+ private readonly ILogger<ExperimentalToolLogger> _logger;
+
+ public ExperimentalToolLogger(ILogger<ExperimentalToolLogger> logger)
+ {
+ _logger = logger;
+ }
+
+ public void LogExperimentMessage()
+ {
+ _logger.LogWarning(ExperimentMessage);
+ }
+
+ public static void AddLogFilter(ILoggingBuilder builder)
+ {
+ builder.AddFilter(typeof(ExperimentalToolLogger).FullName, LogLevel.Warning);
+ }
+ }
+}
// See the LICENSE file in the project root for more information.
using Microsoft.Diagnostics.Monitoring;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Tools.Common;
using System;
using System.CommandLine;
public static Task<int> Main(string[] args)
{
- // FUTURE: This log message should be removed when dotnet-monitor is no longer an experimental tool
- Console.WriteLine("WARNING: dotnet-monitor is experimental and is not intended for production environments yet.");
var parser = new CommandLineBuilder()
- .AddCommand(CollectCommand())
- .UseDefaults()
- .Build();
+ .AddCommand(CollectCommand())
+ .UseDefaults()
+ .Build();
return parser.InvokeAsync(args);
}
}
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.IO.Compression;
-namespace Microsoft.Diagnostics.Monitoring
+namespace Microsoft.Diagnostics.Tools.Monitor
{
internal class Startup
{
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
- public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ public void Configure(
+ IApplicationBuilder app,
+ IWebHostEnvironment env,
+ ExperimentalToolLogger logger)
{
+ logger.LogExperimentMessage();
+
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();