});
}
- [HttpGet("dump/{pid?}")]
- public Task<ActionResult> GetDump(int? pid, [FromQuery] DumpType type = DumpType.WithHeap)
+ [HttpGet("dump/{processFilter?}")]
+ public Task<ActionResult> GetDump(
+ ProcessFilter? processFilter,
+ [FromQuery] DumpType type = DumpType.WithHeap)
{
return this.InvokeService(async () =>
{
- int pidValue = await _diagnosticServices.ResolveProcessAsync(pid, HttpContext.RequestAborted);
- Stream result = await _diagnosticServices.GetDump(pidValue, type, HttpContext.RequestAborted);
+ IProcessInfo processInfo = await _diagnosticServices.GetProcessAsync(processFilter, HttpContext.RequestAborted);
+ Stream result = await _diagnosticServices.GetDump(processInfo, type, HttpContext.RequestAborted);
string dumpFileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
FormattableString.Invariant($"dump_{GetFileNameTimeStampUtcNow()}.dmp") :
});
}
- [HttpGet("gcdump/{pid?}")]
- public Task<ActionResult> GetGcDump(int? pid)
+ [HttpGet("gcdump/{processFilter?}")]
+ public Task<ActionResult> GetGcDump(
+ ProcessFilter? processFilter)
{
return this.InvokeService(async () =>
{
- int pidValue = await _diagnosticServices.ResolveProcessAsync(pid, HttpContext.RequestAborted);
- Stream result = await _diagnosticServices.GetGcDump(pidValue, this.HttpContext.RequestAborted);
- return File(result, "application/octet-stream", FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{pidValue}.gcdump"));
+ IProcessInfo processInfo = await _diagnosticServices.GetProcessAsync(processFilter, HttpContext.RequestAborted);
+ Stream result = await _diagnosticServices.GetGcDump(processInfo, this.HttpContext.RequestAborted);
+ return File(result, "application/octet-stream", FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{processInfo.Pid}.gcdump"));
});
}
- [HttpGet("trace/{pid?}")]
+ [HttpGet("trace/{processFilter?}")]
public Task<ActionResult> Trace(
- int? pid,
+ ProcessFilter? processFilter,
[FromQuery]TraceProfile profile = DefaultTraceProfiles,
[FromQuery][Range(-1, int.MaxValue)] int durationSeconds = 30,
[FromQuery][Range(1, int.MaxValue)] int metricsIntervalSeconds = 1)
var aggregateConfiguration = new AggregateSourceConfiguration(configurations.ToArray());
- return await StartTrace(pid, aggregateConfiguration, duration);
+ return await StartTrace(processFilter, aggregateConfiguration, duration);
});
}
- [HttpPost("trace/{pid?}")]
+ [HttpPost("trace/{processFilter?}")]
public Task<ActionResult> TraceCustomConfiguration(
- int? pid,
+ ProcessFilter? processFilter,
[FromBody][Required] EventPipeConfigurationModel configuration,
[FromQuery][Range(-1, int.MaxValue)] int durationSeconds = 30)
{
requestRundown: configuration.RequestRundown,
bufferSizeInMB: configuration.BufferSizeInMB);
- return await StartTrace(pid, traceConfiguration, duration);
+ return await StartTrace(processFilter, traceConfiguration, duration);
});
}
- [HttpGet("logs/{pid?}")]
+ [HttpGet("logs/{processFilter?}")]
[Produces(ContentTypeEventStream, ContentTypeNdJson, ContentTypeJson)]
- public Task<ActionResult> Logs(int? pid, [FromQuery][Range(-1, int.MaxValue)] int durationSeconds = 30, [FromQuery] LogLevel level = LogLevel.Debug)
+ public Task<ActionResult> Logs(
+ ProcessFilter? processFilter,
+ [FromQuery][Range(-1, int.MaxValue)] int durationSeconds = 30,
+ [FromQuery] LogLevel level = LogLevel.Debug)
{
TimeSpan duration = ConvertSecondsToTimeSpan(durationSeconds);
return this.InvokeService(async () =>
{
- int pidValue = await _diagnosticServices.ResolveProcessAsync(pid, HttpContext.RequestAborted);
+ IProcessInfo processInfo = await _diagnosticServices.GetProcessAsync(processFilter, HttpContext.RequestAborted);
LogFormat format = ComputeLogFormat(Request.GetTypedHeaders().Accept);
if (format == LogFormat.None)
}
string contentType = (format == LogFormat.EventStream) ? ContentTypeEventStream : ContentTypeNdJson;
- string downloadName = (format == LogFormat.EventStream) ? null : FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{pidValue}.txt");
+ string downloadName = (format == LogFormat.EventStream) ? null : FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{processInfo.Pid}.txt");
return new OutputStreamResult(async (outputStream, token) =>
{
- await _diagnosticServices.StartLogs(outputStream, pidValue, duration, format, level, token);
+ await _diagnosticServices.StartLogs(outputStream, processInfo, duration, format, level, token);
}, contentType, downloadName);
});
}
- private async Task<StreamWithCleanupResult> StartTrace(int? pid, MonitoringSourceConfiguration configuration, TimeSpan duration)
+ private async Task<StreamWithCleanupResult> StartTrace(
+ ProcessFilter? processFilter,
+ MonitoringSourceConfiguration configuration,
+ TimeSpan duration)
{
- int pidValue = await _diagnosticServices.ResolveProcessAsync(pid, HttpContext.RequestAborted);
- IStreamWithCleanup result = await _diagnosticServices.StartTrace(pidValue, configuration, duration, this.HttpContext.RequestAborted);
- return new StreamWithCleanupResult(result, "application/octet-stream", FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{pidValue}.nettrace"));
+ IProcessInfo processInfo = await _diagnosticServices.GetProcessAsync(processFilter, HttpContext.RequestAborted);
+ IStreamWithCleanup result = await _diagnosticServices.StartTrace(processInfo, configuration, duration, this.HttpContext.RequestAborted);
+ return new StreamWithCleanupResult(result, "application/octet-stream", FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{processInfo.Pid}.nettrace"));
}
private static TimeSpan ConvertSecondsToTimeSpan(int durationSeconds)
{
//TODO In multi-process scenarios, how do we decide which process to choose?
//One possibility is to enable metrics after a request to begin polling for metrics
- int pid = await _services.ResolveProcessAsync(pid: null, stoppingToken);
- var client = new DiagnosticsClient(pid);
- await _pipeProcessor.Process(client, pid, Timeout.InfiniteTimeSpan, stoppingToken);
+ IProcessInfo pi = await _services.GetProcessAsync(filter: null, stoppingToken);
+ await _pipeProcessor.Process(pi.Client, pi.Pid, Timeout.InfiniteTimeSpan, stoppingToken);
}
catch(Exception e) when (!(e is OperationCanceledException))
{
// 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.IO;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Diagnostics.NETCore.Client;
+using Microsoft.Extensions.Logging;
namespace Microsoft.Diagnostics.Monitoring
{
{
Task<IEnumerable<IProcessInfo>> GetProcessesAsync(CancellationToken token);
- Task<int> ResolveProcessAsync(int? pid, CancellationToken token);
+ Task<IProcessInfo> GetProcessAsync(ProcessFilter? filter, CancellationToken token);
- Task<Stream> GetDump(int pid, DumpType mode, CancellationToken token);
+ Task<Stream> GetDump(IProcessInfo pi, DumpType mode, CancellationToken token);
- Task<Stream> GetGcDump(int pid, CancellationToken token);
+ Task<Stream> GetGcDump(IProcessInfo pi, CancellationToken token);
- Task<IStreamWithCleanup> StartTrace(int pid, MonitoringSourceConfiguration configuration, TimeSpan duration, CancellationToken token);
+ Task<IStreamWithCleanup> StartTrace(IProcessInfo pi, MonitoringSourceConfiguration configuration, TimeSpan duration, CancellationToken token);
- Task StartLogs(Stream outputStream, int pid, TimeSpan duration, LogFormat logFormat, LogLevel logLevel, CancellationToken token);
+ Task StartLogs(Stream outputStream, IProcessInfo pi, TimeSpan duration, LogFormat logFormat, LogLevel logLevel, CancellationToken token);
}
public interface IStreamWithCleanup : IAsyncDisposable
public interface IProcessInfo
{
+ DiagnosticsClient Client { get; }
+
int Pid { get; }
Guid Uid { get; }
--- /dev/null
+using System;
+using System.ComponentModel;
+using System.Globalization;
+
+namespace Microsoft.Diagnostics.Monitoring
+{
+ [TypeConverter(typeof(ProcessFilterTypeConverter))]
+ public struct ProcessFilter
+ {
+ public ProcessFilter(int processId)
+ {
+ ProcessId = processId;
+ RuntimeInstanceCookie = null;
+ }
+
+ public ProcessFilter(Guid runtimeInstanceCookie)
+ {
+ ProcessId = null;
+ RuntimeInstanceCookie = runtimeInstanceCookie;
+ }
+
+ public int? ProcessId { get; }
+
+ public Guid? RuntimeInstanceCookie { get; }
+ }
+
+ internal class ProcessFilterTypeConverter : TypeConverter
+ {
+ public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+ {
+ if (null == sourceType)
+ {
+ throw new ArgumentNullException(nameof(sourceType));
+ }
+ return sourceType == typeof(string) || sourceType == typeof(ProcessFilter);
+ }
+
+ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+ {
+ if (value is string valueString)
+ {
+ if (string.IsNullOrEmpty(valueString))
+ {
+ return null;
+ }
+ else if (Guid.TryParse(valueString, out Guid cookie))
+ {
+ return new ProcessFilter(cookie);
+ }
+ else if (int.TryParse(valueString, out int processId))
+ {
+ return new ProcessFilter(processId);
+ }
+ }
+ else if (value is ProcessFilter identifier)
+ {
+ return identifier;
+ }
+ throw new FormatException();
+ }
+ }
+}
{
public sealed class DiagnosticServices : IDiagnosticServices
{
- private const int DockerEntrypointProcessId = 1;
+ // A Docker container's entrypoint process ID is 1
+ private static readonly ProcessFilter DockerEntrypointProcessFilter = new ProcessFilter(1);
// The amount of time to wait when checking if the docker entrypoint process is a .NET process
// with a diagnostics transport connection.
{
var endpointInfos = await _endpointInfoSource.GetEndpointInfoAsync(token);
- return endpointInfos.Select(c => new ProcessInfo(c.RuntimeInstanceCookie, c.ProcessId));
+ return endpointInfos.Select(ProcessInfo.FromEndpointInfo);
}
catch (UnauthorizedAccessException)
{
}
}
- public async Task<Stream> GetDump(int pid, DumpType mode, CancellationToken token)
+ public async Task<Stream> GetDump(IProcessInfo pi, DumpType mode, CancellationToken token)
{
- string dumpFilePath = Path.Combine(Path.GetTempPath(), FormattableString.Invariant($"{Guid.NewGuid()}_{pid}"));
+ string dumpFilePath = Path.Combine(Path.GetTempPath(), FormattableString.Invariant($"{Guid.NewGuid()}_{pi.Pid}"));
NETCore.Client.DumpType dumpType = MapDumpType(mode);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Get the process
- Process process = Process.GetProcessById(pid);
+ Process process = Process.GetProcessById(pi.Pid);
await Dumper.CollectDumpAsync(process, dumpFilePath, dumpType);
}
else
{
- var client = await GetClientAsync(pid, CancellationToken.None);
await Task.Run(() =>
{
- client.WriteDump(dumpType, dumpFilePath);
+ pi.Client.WriteDump(dumpType, dumpFilePath);
});
}
return new AutoDeleteFileStream(dumpFilePath);
}
- public async Task<Stream> GetGcDump(int pid, CancellationToken token)
+ public async Task<Stream> GetGcDump(IProcessInfo pi, CancellationToken token)
{
var graph = new MemoryGraph(50_000);
await using var processor = new DiagnosticsEventPipeProcessor(
PipeMode.GCDump,
gcGraph: graph);
- var client = await GetClientAsync(pid, token);
- await processor.Process(client, pid, Timeout.InfiniteTimeSpan, token);
+ await processor.Process(pi.Client, pi.Pid, Timeout.InfiniteTimeSpan, token);
var dumper = new GCHeapDump(graph);
dumper.CreationTool = "dotnet-monitor";
return stream;
}
- public async Task<IStreamWithCleanup> StartTrace(int pid, MonitoringSourceConfiguration configuration, TimeSpan duration, CancellationToken token)
+ public async Task<IStreamWithCleanup> StartTrace(IProcessInfo pi, MonitoringSourceConfiguration configuration, TimeSpan duration, CancellationToken token)
{
DiagnosticsMonitor monitor = new DiagnosticsMonitor(configuration);
- var client = await GetClientAsync(pid, token);
- Stream stream = await monitor.ProcessEvents(client, duration, token);
+ Stream stream = await monitor.ProcessEvents(pi.Client, duration, token);
return new StreamWithCleanup(monitor, stream);
}
- public async Task StartLogs(Stream outputStream, int pid, TimeSpan duration, LogFormat format, LogLevel level, CancellationToken token)
+ public async Task StartLogs(Stream outputStream, IProcessInfo pi, TimeSpan duration, LogFormat format, LogLevel level, CancellationToken token)
{
using var loggerFactory = new LoggerFactory();
loggerFactory: loggerFactory,
logsLevel: level);
- var client = await GetClientAsync(pid, token);
- await processor.Process(client, pid, duration, token);
+ await processor.Process(pi.Client, pi.Pid, duration, token);
}
private static NETCore.Client.DumpType MapDumpType(DumpType dumpType)
{
- switch(dumpType)
+ switch (dumpType)
{
case DumpType.Full:
return NETCore.Client.DumpType.Full;
}
}
- public async Task<int> ResolveProcessAsync(int? pid, CancellationToken token)
+ public async Task<IProcessInfo> GetProcessAsync(ProcessFilter? filter, CancellationToken token)
{
- if (pid.HasValue)
+ var endpointInfos = await _endpointInfoSource.GetEndpointInfoAsync(token);
+
+ if (filter.HasValue)
{
- return pid.Value;
+ return GetSingleProcessInfo(
+ endpointInfos,
+ filter);
}
// Short-circuit for when running in a Docker container.
{
try
{
- var client = await GetClientAsync(DockerEntrypointProcessId, token);
+ IProcessInfo processInfo = GetSingleProcessInfo(
+ endpointInfos,
+ DockerEntrypointProcessFilter);
+
using var timeoutSource = new CancellationTokenSource(DockerEntrypointWaitTimeout);
-
- await client.WaitForConnectionAsync(timeoutSource.Token);
- return DockerEntrypointProcessId;
+ await processInfo.Client.WaitForConnectionAsync(timeoutSource.Token);
+
+ return processInfo;
}
catch
{
- // Process ID 1 doesn't exist or didn't advertise in the reverse pipe configuration.
+ // Process ID 1 doesn't exist, didn't advertise in connect mode, or is not a .NET process.
+ }
+ }
+
+ return GetSingleProcessInfo(
+ endpointInfos,
+ filter: null);
+ }
+
+ private IProcessInfo GetSingleProcessInfo(IEnumerable<IEndpointInfo> endpointInfos, ProcessFilter? filter)
+ {
+ if (filter.HasValue)
+ {
+ if (filter.Value.RuntimeInstanceCookie.HasValue)
+ {
+ Guid cookie = filter.Value.RuntimeInstanceCookie.Value;
+ endpointInfos = endpointInfos.Where(info => info.RuntimeInstanceCookie == cookie);
+ }
+
+ if (filter.Value.ProcessId.HasValue)
+ {
+ int pid = filter.Value.ProcessId.Value;
+ endpointInfos = endpointInfos.Where(info => info.ProcessId == pid);
}
}
- // Only return a process ID if there is exactly one discoverable process.
- IProcessInfo[] processes = (await GetProcessesAsync(token)).ToArray();
- switch (processes.Length)
+ IEndpointInfo[] endpointInfoArray = endpointInfos.ToArray();
+ switch (endpointInfoArray.Length)
{
case 0:
throw new ArgumentException("Unable to discover a target process.");
case 1:
- return processes[0].Pid;
+ return ProcessInfo.FromEndpointInfo(endpointInfoArray[0]);
default:
#if DEBUG
- Process process = processes.Select(p => Process.GetProcessById(p.Pid)).FirstOrDefault(p => string.Equals(p.ProcessName, "iisexpress", StringComparison.OrdinalIgnoreCase));
- if (process != null)
+ IEndpointInfo endpointInfo = endpointInfoArray.FirstOrDefault(info => string.Equals(Process.GetProcessById(info.ProcessId).ProcessName, "iisexpress", StringComparison.OrdinalIgnoreCase));
+ if (endpointInfo != null)
{
- return process.Id;
+ return ProcessInfo.FromEndpointInfo(endpointInfo);
}
#endif
throw new ArgumentException("Unable to select a single target process because multiple target processes have been discovered.");
}
}
- private async Task<DiagnosticsClient> GetClientAsync(int processId, CancellationToken token)
- {
- var endpointInfos = await _endpointInfoSource.GetEndpointInfoAsync(token);
- var endpointInfo = endpointInfos.FirstOrDefault(c => c.ProcessId == processId);
-
- if (null == endpointInfo)
- {
- throw new InvalidOperationException($"Diagnostics client for process ID {processId} does not exist.");
- }
-
- return new DiagnosticsClient(endpointInfo.Endpoint);
- }
-
public void Dispose()
{
_tokenSource.Cancel();
private sealed class ProcessInfo : IProcessInfo
{
- public ProcessInfo(Guid uid, int pid)
+ public ProcessInfo(DiagnosticsClient client, Guid uid, int pid)
{
+ Client = client;
Pid = pid;
Uid = uid;
}
+ public static ProcessInfo FromEndpointInfo(IEndpointInfo endpointInfo)
+ {
+ if (null == endpointInfo)
+ {
+ throw new ArgumentNullException(nameof(endpointInfo));
+ }
+
+ return new ProcessInfo(
+ new DiagnosticsClient(endpointInfo.Endpoint),
+ endpointInfo.RuntimeInstanceCookie,
+ endpointInfo.ProcessId);
+ }
+
+ public DiagnosticsClient Client { get; }
+
public int Pid { get; }
public Guid Uid { get; }
// HACK We need to disable EndpointRouting in order to run properly in 3.1
System.Reflection.PropertyInfo prop = options.GetType().GetProperty("EnableEndpointRouting");
prop?.SetValue(options, false);
-
}).SetCompatibilityVersion(CompatibilityVersion.Latest);
+ services.Configure<ApiBehaviorOptions>(options =>
+ {
+ options.InvalidModelStateResponseFactory = context =>
+ {
+ var details = new ValidationProblemDetails(context.ModelState);
+ var result = new BadRequestObjectResult(details);
+ result.ContentTypes.Add("application/problem+json");
+ return result;
+ };
+ });
+
services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Optimal;