src\Tools\Common\Commands\ProcessStatus.cs = src\Tools\Common\Commands\ProcessStatus.cs
EndProjectSection
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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.RestServer", "src\Microsoft.Diagnostics.Monitoring.RestServer\Microsoft.Diagnostics.Monitoring.RestServer.csproj", "{B54DE8DD-6591-45C2-B9F7-22C4A23A384C}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "inc", "inc", "{41BDFD6D-D165-4D67-BEF6-4E539040D30A}"
ProjectSection(SolutionItems) = preProject
src\inc\arrayholder.h = src\inc\arrayholder.h
{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}.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
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Checked|Any CPU.ActiveCfg = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Checked|Any CPU.Build.0 = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Checked|ARM.ActiveCfg = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Checked|ARM.Build.0 = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Checked|ARM64.ActiveCfg = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Checked|ARM64.Build.0 = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Checked|x64.ActiveCfg = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Checked|x64.Build.0 = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Checked|x86.ActiveCfg = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Checked|x86.Build.0 = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Debug|ARM.ActiveCfg = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Debug|ARM.Build.0 = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Debug|ARM64.ActiveCfg = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Debug|ARM64.Build.0 = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Debug|x64.ActiveCfg = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Debug|x64.Build.0 = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Debug|x86.ActiveCfg = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Debug|x86.Build.0 = Debug|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Release|Any CPU.Build.0 = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Release|ARM.ActiveCfg = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Release|ARM.Build.0 = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Release|ARM64.ActiveCfg = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Release|ARM64.Build.0 = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Release|x64.ActiveCfg = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Release|x64.Build.0 = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Release|x86.ActiveCfg = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.Release|x86.Build.0 = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.RelWithDebInfo|ARM.ActiveCfg = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.RelWithDebInfo|ARM.Build.0 = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.RelWithDebInfo|ARM64.ActiveCfg = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.RelWithDebInfo|ARM64.Build.0 = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.RelWithDebInfo|x64.Build.0 = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C}.RelWithDebInfo|x86.Build.0 = Release|Any CPU
{6419BA04-6F1A-4D2F-8DE4-5C359E0364A3}.Checked|Any CPU.ActiveCfg = Debug|Any CPU
{6419BA04-6F1A-4D2F-8DE4-5C359E0364A3}.Checked|Any CPU.Build.0 = Debug|Any CPU
{6419BA04-6F1A-4D2F-8DE4-5C359E0364A3}.Checked|ARM.ActiveCfg = Debug|Any CPU
{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}
- {B54DE8DD-6591-45C2-B9F7-22C4A23A384C} = {19FAB78C-3351-4911-8F0C-8C6056401740}
{41BDFD6D-D165-4D67-BEF6-4E539040D30A} = {19FAB78C-3351-4911-8F0C-8C6056401740}
{33239640-6F4B-4DA4-A780-2F5B26D57EAD} = {41BDFD6D-D165-4D67-BEF6-4E539040D30A}
{06730767-421B-465F-BB63-A3A07D72D7A2} = {41BDFD6D-D165-4D67-BEF6-4E539040D30A}
<Project>
<PropertyGroup>
- <!-- Since Arcade SDK's AfterSigning.proj file does not import the Arcade SDK, this
- setting is necessary because we are manually importing the Arcade SDK targets
- below in order to get the GenerateChecksums target. -->
- <_SuppressSdkImports>false</_SuppressSdkImports>
<PublishingVersion>3</PublishingVersion>
</PropertyGroup>
<PropertyGroup>
<BundleOutputDir>$(ArtifactsDir)bundledtools/</BundleOutputDir>
- <PublishDependsOnTargets>$(PublishDependsOnTargets);CollectBundledToolsArchives;CollectPackageArtifactFiles</PublishDependsOnTargets>
+ <PublishDependsOnTargets>$(PublishDependsOnTargets);CollectBundledToolsArchives</PublishDependsOnTargets>
</PropertyGroup>
<ItemGroup>
- <PackageFile Include="$(ArtifactsShippingPackagesDir)**/*.nupkg" IsShipping="true" />
- <PackageFile Include="$(ArtifactsNonShippingPackagesDir)**/*.nupkg" IsShipping="false" />
<FilesToPublishToSymbolServer Include="$(BundleOutputDir)**/*.pdb" />
<BundledToolsArchives Include="$(BundleOutputDir)**/*.zip" />
</ItemGroup>
</ItemGroup>
</Target>
-
- <Target Name="GenerateChecksumsForBlobgroups">
- <ItemGroup>
- <GenerateChecksumItems Include="%(PackageFile.Identity)"
- Condition="$([System.IO.File]::Exists('%(PackageFile.Identity).blobgroup'))" >
- <DestinationPath>%(FullPath).sha512</DestinationPath>
- </GenerateChecksumItems>
- </ItemGroup>
-
- <GenerateChecksums Items="@(GenerateChecksumItems)" />
- </Target>
-
- <!-- Run the CollectPackageArtifactFiles target on each PackageFile by target batching on a non-existing file.
- This allows using the ReadLinesFromFile task to read the blob group file, which was written with WriteLinesToFile,
- thus avoiding erroneously reading in the newline at the end of the blob group file. -->
- <Target Name="CollectPackageArtifactFiles"
- DependsOnTargets="GenerateChecksumsForBlobgroups"
- Inputs="@(PackageFile)"
- Outputs="%(PackageFile.Identity).notexist">
-
- <!-- Find the artifact files next to the package file. -->
- <PropertyGroup>
- <_BlobGroupFilePath>%(PackageFile.FullPath).blobgroup</_BlobGroupFilePath>
- <_ChecksumFilePath>%(PackageFile.FullPath).sha512</_ChecksumFilePath>
- <_VersionFilePath>%(PackageFile.FullPath).version</_VersionFilePath>
- </PropertyGroup>
-
- <Error Condition="Exists('$(_BlobGroupFilePath)') and !Exists('$(_ChecksumFilePath)')"
- Text="Expected SHA512 hash for %(PackageFile.FullPath) not found at $(_ChecksumFilePath)"/>
-
- <!-- Read in blob group name, if it exists -->
- <ReadLinesFromFile File="$(_BlobGroupFilePath)" Condition="Exists('$(_BlobGroupFilePath)')">
- <Output TaskParameter="Lines" PropertyName="_BlobGroupName"/>
- </ReadLinesFromFile>
-
- <!-- Calculate manifest artifact data for each file type. -->
- <ItemGroup>
- <_CommonArtifactData Include="NonShipping=true" Condition="'%(PackageFile.IsShipping)' != 'true'" />
- </ItemGroup>
- <ItemGroup>
- <_PackageArtifactData Include="@(_CommonArtifactData)" />
- <!-- Setting Category to Other will upload to installers blob feed. -->
- <!-- Feed configuration categories are uppercase and case sensitive in PublishArtifactsInManifest task. -->
- <_PackageArtifactData Include="Category=OTHER" />
- </ItemGroup>
-
- <!-- Capture each blob item to upload to blob feed -->
- <ItemGroup>
- <_BlobItem Include="%(PackageFile.FullPath)">
- <ManifestArtifactData>@(_PackageArtifactData)</ManifestArtifactData>
- </_BlobItem>
- <_BlobItem Include="$(_ChecksumFilePath)" Condition="Exists('$(_ChecksumFilePath)')">
- <ManifestArtifactData Condition="'@(_CommonArtifactData)' != ''">@(_CommonArtifactData)</ManifestArtifactData>
- </_BlobItem>
- <_BlobItem Include="$(_VersionFilePath)" Condition="Exists('$(_VersionFilePath)')">
- <ManifestArtifactData Condition="'@(_CommonArtifactData)' != ''">@(_CommonArtifactData)</ManifestArtifactData>
- </_BlobItem>
- </ItemGroup>
-
- <!-- Add artifact items to be pushed to blob feed -->
- <ItemGroup>
- <ItemsToPushToBlobFeed Include="@(_BlobItem)" Condition="'$(_BlobGroupName)' != ''">
- <RelativeBlobPath>diagnostics/$(_BlobGroupName)/%(_BlobItem.Filename)%(_BlobItem.Extension)</RelativeBlobPath>
- <PublishFlatContainer>true</PublishFlatContainer>
- </ItemsToPushToBlobFeed>
- </ItemGroup>
- </Target>
-
- <Import Project="Sdk.targets" Sdk="Microsoft.DotNet.Arcade.Sdk" />
</Project>
Condition="$(NeedsPublishing) == 'true'"
DependsOnTargets="$(_BeforePublishNoBuildTargets);$(_CorePublishTargets)" />
- <!-- Creates artifact files related to the package that will be uploaded to blob storage during publish. -->
- <Target Name="GeneratePackageArtifactFiles"
- AfterTargets="Pack"
- Condition="'$(IsPackable)' == 'true' and '$(BlobGroupPrefix)' != ''">
- <PropertyGroup>
- <_BlobGroupVersionMajor>$(PackageVersion.Split('.')[0])</_BlobGroupVersionMajor>
- <_BlobGroupVersionMinor>$(PackageVersion.Split('.')[1])</_BlobGroupVersionMinor>
- <_BlobGroupName>$(BlobGroupPrefix)$(_BlobGroupVersionMajor).$(_BlobGroupVersionMinor)</_BlobGroupName>
- </PropertyGroup>
- <!-- A file that contains the blob group so that publishing can use it in the blob path calculation. -->
- <WriteLinesToFile File="$(PackageOutputPath)\$(PackageId).$(PackageVersion).nupkg.blobgroup"
- Lines="$(_BlobGroupName)"
- Overwrite="true" />
- <WriteLinesToFile File="$(PackageOutputPath)\$(PackageId).$(PackageVersion).nupkg.version"
- Lines="$(PackageVersion)"
- Overwrite="true" />
- </Target>
-
</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.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;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- internal static class ActionContextExtensions
- {
- public static Task ProblemAsync(this ActionContext context, Exception ex)
- {
- 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);
- }
- }
-
- 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(token);
- }
- catch (ArgumentException ex) when (LogError(logger, ex))
- {
- await context.ProblemAsync(ex);
- }
- catch (DiagnosticsClientException ex) when (LogError(logger, ex))
- {
- await context.ProblemAsync(ex);
- }
- catch (InvalidOperationException ex) when (LogError(logger, ex))
- {
- await context.ProblemAsync(ex);
- }
- catch (OperationCanceledException ex) when (token.IsCancellationRequested && LogInformation(logger, ex))
- {
- await context.ProblemAsync(ex);
- }
- catch (OperationCanceledException ex) when (LogError(logger, ex))
- {
- await context.ProblemAsync(ex);
- }
- catch (MonitoringException ex) when (LogError(logger, ex))
- {
- await context.ProblemAsync(ex);
- }
- catch (ValidationException ex) when (LogError(logger, ex))
- {
- await context.ProblemAsync(ex);
- }
- catch (UnauthorizedAccessException ex) when (LogError(logger, ex))
- {
- await context.ProblemAsync(ex);
- }
- }
-
- private static bool LogError(ILogger logger, Exception ex)
- {
- logger.RequestFailed(ex);
- return true;
- }
-
- private static bool LogInformation(ILogger logger, Exception ex)
- {
- logger.RequestCanceled();
- 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);
- }
- }
-}
+++ /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.RestServer
-{
- internal sealed class ApiAuthenticationOptions
- {
- public const string ConfigurationKey = "ApiAuthentication";
-
- public string ApiKeyHash { get; set; }
- public string ApiKeyHashType { 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.Linq;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- internal static class AuthConstants
- {
- public const string PolicyName = "AuthorizedUserPolicy";
- public const string NegotiateSchema = "Negotiate";
- public const string NtlmSchema = "NTLM";
- public const string KerberosSchema = "Kerberos";
- public const string ApiKeySchema = "MonitorApiKey";
- }
-}
+++ /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.Linq;
-using System.Runtime.InteropServices;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- internal sealed class AuthOptions : IAuthOptions
- {
- public bool EnableNegotiate => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
-
- public KeyAuthenticationMode KeyAuthenticationMode { get; }
-
- public bool EnableKeyAuth => (KeyAuthenticationMode == KeyAuthenticationMode.StoredKey) ||
- (KeyAuthenticationMode == KeyAuthenticationMode.TemporaryKey);
-
- public AuthOptions(KeyAuthenticationMode mode)
- {
- KeyAuthenticationMode = mode;
- }
- }
-}
+++ /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.Linq;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- internal enum KeyAuthenticationMode
- {
- StoredKey,
- TemporaryKey,
- NoAuth,
- }
-
- internal interface IAuthOptions
- {
- bool EnableNegotiate { get; }
- KeyAuthenticationMode KeyAuthenticationMode { get; }
- }
-}
+++ /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.Linq;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- internal static class ConfigurationHelper
- {
- private const char ValueSeparator = ';';
- private static readonly char[] ValueSeparatorArray = new char[] { ValueSeparator };
- private static readonly string ValueSeparatorString = ValueSeparator.ToString();
-
- public static string MakeKey(string parent, string child)
- {
- return FormattableString.Invariant($"{parent}:{child}");
- }
-
- public static string[] SplitValue(string value)
- {
- return value.Split(ValueSeparatorArray, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public static string JoinValue(string[] values)
- {
- return string.Join(ValueSeparatorString, values);
- }
- }
-}
+++ /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
-{
- internal static class ContentTypes
- {
- public const string ApplicationJson = "application/json";
- public const string ApplicationNdJson = "application/x-ndjson";
- public const string ApplicationOctectStream = "application/octet-stream";
- public const string TextEventStream = "text/event-stream";
- }
-}
+++ /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 FastSerialization;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Diagnostics.Monitoring.EventPipe;
-using Microsoft.Diagnostics.Monitoring.RestServer.Models;
-using Microsoft.Diagnostics.Monitoring.RestServer.Validation;
-using Microsoft.Diagnostics.NETCore.Client;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using System.IO;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer.Controllers
-{
- [Route("")] // Root
- [ApiController]
- [HostRestriction]
- [Authorize(Policy = AuthConstants.PolicyName)]
- 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);
-
- private readonly ILogger<DiagController> _logger;
- private readonly IDiagnosticServices _diagnosticServices;
-
- public DiagController(ILogger<DiagController> logger, IServiceProvider serviceProvider)
- {
- _logger = logger;
- _diagnosticServices = serviceProvider.GetRequiredService<IDiagnosticServices>();
- }
-
- [HttpGet("processes")]
- public Task<ActionResult<IEnumerable<ProcessIdentifierModel>>> GetProcesses()
- {
- return this.InvokeService(async () =>
- {
- IList<ProcessIdentifierModel> processesIdentifiers = new List<ProcessIdentifierModel>();
- foreach (IProcessInfo p in await _diagnosticServices.GetProcessesAsync(HttpContext.RequestAborted))
- {
- processesIdentifiers.Add(ProcessIdentifierModel.FromProcessInfo(p));
- }
- _logger.WrittenToHttpStream();
- return new ActionResult<IEnumerable<ProcessIdentifierModel>>(processesIdentifiers);
- }, _logger);
- }
-
- [HttpGet("processes/{processFilter}")]
- public Task<ActionResult<ProcessModel>> GetProcess(
- ProcessFilter processFilter)
- {
- return InvokeForProcess<ProcessModel>(processInfo =>
- {
- ProcessModel processModel = ProcessModel.FromProcessInfo(processInfo);
-
- _logger.WrittenToHttpStream();
-
- return processModel;
- },
- processFilter);
- }
-
- [HttpGet("processes/{processFilter}/env")]
- public Task<ActionResult<Dictionary<string, string>>> GetProcessEnvironment(
- ProcessFilter processFilter)
- {
- return InvokeForProcess<Dictionary<string, string>>(processInfo =>
- {
- var client = new DiagnosticsClient(processInfo.EndpointInfo.Endpoint);
-
- try
- {
- Dictionary<string, string> environment = client.GetProcessEnvironment();
-
- _logger.WrittenToHttpStream();
-
- return environment;
- }
- catch (ServerErrorException)
- {
- throw new InvalidOperationException("Unable to get process environment.");
- }
- },
- processFilter);
- }
-
- [HttpGet("dump/{processFilter?}")]
- public Task<ActionResult> GetDump(
- ProcessFilter? processFilter,
- [FromQuery] DumpType type = DumpType.WithHeap,
- [FromQuery] string egressProvider = null)
- {
- return InvokeForProcess(async processInfo =>
- {
- string dumpFileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
- FormattableString.Invariant($"dump_{GetFileNameTimeStampUtcNow()}.dmp") :
- FormattableString.Invariant($"core_{GetFileNameTimeStampUtcNow()}");
-
- if (string.IsNullOrEmpty(egressProvider))
- {
- Stream dumpStream = await _diagnosticServices.GetDump(processInfo, type, HttpContext.RequestAborted);
-
- _logger.WrittenToHttpStream();
- //Compression is done automatically by the response
- //Chunking is done because the result has no content-length
- return File(dumpStream, ContentTypes.ApplicationOctectStream, dumpFileName);
- }
- 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,
- scope);
- }
- }, processFilter, ArtifactType_Dump);
- }
-
- [HttpGet("gcdump/{processFilter?}")]
- public Task<ActionResult> GetGcDump(
- ProcessFilter? processFilter,
- [FromQuery] string egressProvider = null)
- {
- return InvokeForProcess(processInfo =>
- {
- string fileName = FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{processInfo.EndpointInfo.ProcessId}.gcdump");
-
- Func<CancellationToken, Task<IFastSerializable>> action = async (token) => {
- var graph = new Graphs.MemoryGraph(50_000);
-
- EventGCPipelineSettings settings = new EventGCPipelineSettings
- {
- Duration = Timeout.InfiniteTimeSpan,
- };
-
- var client = new DiagnosticsClient(processInfo.EndpointInfo.Endpoint);
-
- await using var pipeline = new EventGCDumpPipeline(client, settings, graph);
- await pipeline.RunAsync(token);
-
- return new GCHeapDump(graph)
- {
- CreationTool = "dotnet-monitor"
- };
- };
-
- return Result(
- ArtifactType_GCDump,
- egressProvider,
- ConvertFastSerializeAction(action),
- fileName,
- ContentTypes.ApplicationOctectStream,
- processInfo.EndpointInfo);
- }, processFilter, ArtifactType_GCDump);
- }
-
- [HttpGet("trace/{processFilter?}")]
- public Task<ActionResult> Trace(
- ProcessFilter? processFilter,
- [FromQuery]TraceProfile profile = DefaultTraceProfiles,
- [FromQuery][Range(-1, int.MaxValue)] int durationSeconds = 30,
- [FromQuery][Range(1, int.MaxValue)] int metricsIntervalSeconds = 1,
- [FromQuery] string egressProvider = null)
- {
- return InvokeForProcess(processInfo =>
- {
- TimeSpan duration = ConvertSecondsToTimeSpan(durationSeconds);
-
- var configurations = new List<MonitoringSourceConfiguration>();
- if (profile.HasFlag(TraceProfile.Cpu))
- {
- configurations.Add(new CpuProfileConfiguration());
- }
- if (profile.HasFlag(TraceProfile.Http))
- {
- configurations.Add(new HttpRequestSourceConfiguration());
- }
- if (profile.HasFlag(TraceProfile.Logs))
- {
- configurations.Add(new LoggingSourceConfiguration());
- }
- if (profile.HasFlag(TraceProfile.Metrics))
- {
- configurations.Add(new MetricSourceConfiguration(metricsIntervalSeconds, Enumerable.Empty<string>()));
- }
-
- var aggregateConfiguration = new AggregateSourceConfiguration(configurations.ToArray());
-
- return StartTrace(processInfo, aggregateConfiguration, duration, egressProvider);
- }, processFilter, ArtifactType_Trace);
- }
-
- [HttpPost("trace/{processFilter?}")]
- public Task<ActionResult> TraceCustomConfiguration(
- ProcessFilter? processFilter,
- [FromBody][Required] EventPipeConfigurationModel configuration,
- [FromQuery][Range(-1, int.MaxValue)] int durationSeconds = 30,
- [FromQuery] string egressProvider = null)
- {
- return InvokeForProcess(processInfo =>
- {
- TimeSpan duration = ConvertSecondsToTimeSpan(durationSeconds);
-
- var providers = new List<EventPipeProvider>();
-
- foreach (EventPipeProviderModel providerModel in configuration.Providers)
- {
- if (!IntegerOrHexStringAttribute.TryParse(providerModel.Keywords, out long keywords, out string parseError))
- {
- throw new InvalidOperationException(parseError);
- }
-
- providers.Add(new EventPipeProvider(
- providerModel.Name,
- providerModel.EventLevel,
- keywords,
- providerModel.Arguments
- ));
- }
-
- var traceConfiguration = new EventPipeProviderSourceConfiguration(
- providers: providers.ToArray(),
- requestRundown: configuration.RequestRundown,
- bufferSizeInMB: configuration.BufferSizeInMB);
-
- return StartTrace(processInfo, traceConfiguration, duration, egressProvider);
- }, processFilter, ArtifactType_Trace);
- }
-
- [HttpGet("logs/{processFilter?}")]
- [Produces(ContentTypes.TextEventStream, ContentTypes.ApplicationNdJson, ContentTypes.ApplicationJson)]
- public Task<ActionResult> Logs(
- ProcessFilter? processFilter,
- [FromQuery][Range(-1, int.MaxValue)] int durationSeconds = 30,
- [FromQuery] LogLevel level = LogLevel.Debug,
- [FromQuery] string egressProvider = null)
- {
- return InvokeForProcess(processInfo =>
- {
- TimeSpan duration = ConvertSecondsToTimeSpan(durationSeconds);
-
- LogFormat format = ComputeLogFormat(Request.GetTypedHeaders().Accept);
- if (format == LogFormat.None)
- {
- return this.NotAcceptable();
- }
-
- string fileName = FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{processInfo.EndpointInfo.ProcessId}.txt");
- string contentType = format == LogFormat.EventStream ? ContentTypes.TextEventStream : ContentTypes.ApplicationNdJson;
-
- Func<Stream, CancellationToken, Task> action = async (outputStream, token) =>
- {
- using var loggerFactory = new LoggerFactory();
-
- loggerFactory.AddProvider(new StreamingLoggerProvider(outputStream, format, level));
-
- var settings = new EventLogsPipelineSettings
- {
- Duration = duration,
- LogLevel = level,
- };
-
- var client = new DiagnosticsClient(processInfo.EndpointInfo.Endpoint);
-
- await using EventLogsPipeline pipeline = new EventLogsPipeline(client, settings, loggerFactory);
- await pipeline.RunAsync(token);
- };
-
- return Result(
- ArtifactType_Logs,
- egressProvider,
- action,
- fileName,
- contentType,
- processInfo.EndpointInfo,
- format != LogFormat.EventStream);
- }, processFilter, ArtifactType_Logs);
- }
-
- private ActionResult StartTrace(
- IProcessInfo processInfo,
- MonitoringSourceConfiguration configuration,
- TimeSpan duration,
- string egressProvider)
- {
- string fileName = FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{processInfo.EndpointInfo.ProcessId}.nettrace");
-
- Func<Stream, CancellationToken, Task> action = async (outputStream, token) =>
- {
- Func<Stream, CancellationToken, Task> streamAvailable = async (Stream eventStream, CancellationToken token) =>
- {
- //Buffer size matches FileStreamResult
- //CONSIDER Should we allow client to change the buffer size?
- await eventStream.CopyToAsync(outputStream, 0x10000, token);
- };
-
- var client = new DiagnosticsClient(processInfo.EndpointInfo.Endpoint);
-
- await using EventTracePipeline pipeProcessor = new EventTracePipeline(client, new EventTracePipelineSettings
- {
- Configuration = configuration,
- Duration = duration,
- }, streamAvailable);
-
- await pipeProcessor.RunAsync(token);
- };
-
- return Result(
- ArtifactType_Trace,
- egressProvider,
- action,
- fileName,
- ContentTypes.ApplicationOctectStream,
- processInfo.EndpointInfo);
- }
-
- private static TimeSpan ConvertSecondsToTimeSpan(int durationSeconds)
- {
- return durationSeconds < 0 ?
- Timeout.InfiniteTimeSpan :
- TimeSpan.FromSeconds(durationSeconds);
- }
-
- private static string GetFileNameTimeStampUtcNow()
- {
- return DateTime.UtcNow.ToString("yyyyMMdd_HHmmss");
- }
-
- private static LogFormat ComputeLogFormat(IList<MediaTypeHeaderValue> acceptedHeaders)
- {
- if (acceptedHeaders == null)
- {
- return LogFormat.None;
- }
-
- if (acceptedHeaders.Contains(EventStreamHeader))
- {
- return LogFormat.EventStream;
- }
- if (acceptedHeaders.Contains(NdJsonHeader))
- {
- return LogFormat.Json;
- }
- if (acceptedHeaders.Any(h => EventStreamHeader.IsSubsetOf(h)))
- {
- return LogFormat.EventStream;
- }
- if (acceptedHeaders.Any(h => NdJsonHeader.IsSubsetOf(h)))
- {
- return LogFormat.Json;
- }
- return LogFormat.None;
- }
-
- private ActionResult Result(
- string artifactType,
- string providerName,
- Func<Stream, CancellationToken, Task> action,
- string fileName,
- string contentType,
- 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,
- scope);
- }
- else
- {
- return new EgressStreamResult(
- action,
- providerName,
- fileName,
- endpointInfo,
- contentType,
- scope);
- }
- }
-
- private static Func<Stream, CancellationToken, Task> ConvertFastSerializeAction(Func<CancellationToken, Task<IFastSerializable>> action)
- {
- return async (stream, token) =>
- {
- IFastSerializable fastSerializable = await action(token);
-
- // FastSerialization requests the length of the stream before serializing to the stream.
- // If the stream is a response stream, requesting the length or setting the position is
- // not supported. Create an intermediate buffer if testing the stream fails.
- // This can use a huge amount of memory if the IFastSerializable is very large.
- // CONSIDER: Update FastSerialization to not get the length or attempt to reset the position.
- bool useIntermediateStream = false;
- try
- {
- _ = stream.Length;
- }
- catch (NotSupportedException)
- {
- useIntermediateStream = true;
- }
-
- if (useIntermediateStream)
- {
- using var intermediateStream = new MemoryStream();
-
- var serializer = new Serializer(intermediateStream, fastSerializable, leaveOpen: true);
- serializer.Close();
-
- intermediateStream.Position = 0;
-
- await intermediateStream.CopyToAsync(stream, 0x10000, token);
- }
- else
- {
- var serializer = new Serializer(stream, fastSerializable, leaveOpen: true);
- serializer.Close();
- }
- };
- }
-
- 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.ResolvedTargetProcess();
-
- return await func(processInfo);
- }, _logger);
- }
- finally
- {
- artifactTypeRegistration?.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.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
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer.Controllers
-{
- internal static class DiagControllerExtensions
- {
- public static ActionResult NotAcceptable(this ControllerBase controller)
- {
- return new StatusCodeResult((int)HttpStatusCode.NotAcceptable);
- }
-
- 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(), logger).Result;
- }
-
- 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()), logger).Result;
- }
-
- 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(), logger);
- return result.Result;
- }
-
- 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) when (LogError(logger, e))
- {
- return controller.Problem(e);
- }
- catch (DiagnosticsClientException e) when (LogError(logger, e))
- {
- return controller.Problem(e);
- }
- catch (InvalidOperationException e) when (LogError(logger, e))
- {
- return controller.Problem(e);
- }
- catch (OperationCanceledException e) when (token.IsCancellationRequested && LogInformation(logger, e))
- {
- return controller.Problem(e);
- }
- catch (OperationCanceledException e) when (LogError(logger, e))
- {
- return controller.Problem(e);
- }
- catch (MonitoringException e) when (LogError(logger, e))
- {
- return controller.Problem(e);
- }
- catch (ValidationException e) when (LogError(logger, e))
- {
- return controller.Problem(e);
- }
- }
-
- public static ObjectResult Problem(this ControllerBase controller, Exception ex)
- {
- return controller.BadRequest(ex.ToProblemDetails((int)HttpStatusCode.BadRequest));
- }
-
- private static bool LogError(ILogger logger, Exception ex)
- {
- logger.RequestFailed(ex);
- return true;
- }
-
- private static bool LogInformation(ILogger logger, Exception ex)
- {
- logger.RequestCanceled();
- 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.
-
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-using System;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer.Controllers
-{
- [Route("")]
- [ApiController]
- public class MetricsController : ControllerBase
- {
- private const string ArtifactType_Metrics = "metrics";
-
- private readonly ILogger<MetricsController> _logger;
- private readonly MetricsStoreService _metricsStore;
- private readonly MetricsOptions _metricsOptions;
-
- public MetricsController(ILogger<MetricsController> logger,
- IServiceProvider serviceProvider,
- IOptions<MetricsOptions> metricsOptions)
- {
- _logger = logger;
- _metricsStore = serviceProvider.GetService<MetricsStoreService>();
- _metricsOptions = metricsOptions.Value;
- }
-
- [HttpGet("metrics")]
- public ActionResult Metrics()
- {
- return this.InvokeService(() =>
- {
- if (!_metricsOptions.Enabled)
- {
- 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",
- null,
- scope);
- }, _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;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- public class CorsConfiguration
- {
- public string AllowedOrigins { get; set; }
-
- public string[] GetOrigins() => AllowedOrigins?.Split(';');
- }
-}
+++ /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.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.Diagnostics.Monitoring.EventPipe;
-using Microsoft.Diagnostics.NETCore.Client;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- internal sealed class DiagnosticServices : IDiagnosticServices
- {
- // The value of the operating system field of the ProcessInfo result when the target process is running
- // on a Windows operating system.
- private const string ProcessOperatingSystemWindowsValue = "windows";
-
- // 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.
- private static readonly TimeSpan DockerEntrypointWaitTimeout = TimeSpan.FromMilliseconds(250);
- // The amount of time to wait before cancelling get additional process information (e.g. getting
- // the process command line if the IEndpointInfo doesn't provide it).
- private static readonly TimeSpan ExtendedProcessInfoTimeout = TimeSpan.FromMilliseconds(500);
-
- private readonly IEndpointInfoSourceInternal _endpointInfoSource;
- private readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
-
- public DiagnosticServices(IEndpointInfoSource endpointInfoSource)
- {
- _endpointInfoSource = (IEndpointInfoSourceInternal)endpointInfoSource;
- }
-
- public async Task<IEnumerable<IProcessInfo>> GetProcessesAsync(CancellationToken token)
- {
- try
- {
- using CancellationTokenSource extendedInfoCancellation = CancellationTokenSource.CreateLinkedTokenSource(token);
- IList<Task<ProcessInfo>> processInfoTasks = new List<Task<ProcessInfo>>();
- foreach (IEndpointInfo endpointInfo in await _endpointInfoSource.GetEndpointInfoAsync(token))
- {
- processInfoTasks.Add(ProcessInfo.FromEndpointInfoAsync(endpointInfo, extendedInfoCancellation.Token));
- }
-
- // FromEndpointInfoAsync can fill in the command line for .NET Core 3.1 processes by invoking the
- // event pipe and capturing the ProcessInfo event. Timebox this operation with the cancellation token
- // so that getting the process list does not take a long time or wait indefinitely.
- extendedInfoCancellation.CancelAfter(ExtendedProcessInfoTimeout);
-
- await Task.WhenAll(processInfoTasks);
-
- return processInfoTasks.Select(t => t.Result);
- }
- catch (UnauthorizedAccessException)
- {
- throw new InvalidOperationException("Unable to enumerate processes.");
- }
- }
-
- public async Task<Stream> GetDump(IProcessInfo pi, DumpType mode, CancellationToken token)
- {
- string dumpFilePath = Path.Combine(Path.GetTempPath(), FormattableString.Invariant($"{Guid.NewGuid()}_{pi.EndpointInfo.ProcessId}"));
- NETCore.Client.DumpType dumpType = MapDumpType(mode);
-
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- // Get the process
- Process process = Process.GetProcessById(pi.EndpointInfo.ProcessId);
- await Dumper.CollectDumpAsync(process, dumpFilePath, dumpType);
- }
- else
- {
- await Task.Run(() =>
- {
- var client = new DiagnosticsClient(pi.EndpointInfo.Endpoint);
- client.WriteDump(dumpType, dumpFilePath);
- });
- }
-
- return new AutoDeleteFileStream(dumpFilePath);
- }
-
- private static NETCore.Client.DumpType MapDumpType(DumpType dumpType)
- {
- switch (dumpType)
- {
- case DumpType.Full:
- return NETCore.Client.DumpType.Full;
- case DumpType.WithHeap:
- return NETCore.Client.DumpType.WithHeap;
- case DumpType.Triage:
- return NETCore.Client.DumpType.Triage;
- case DumpType.Mini:
- return NETCore.Client.DumpType.Normal;
- default:
- throw new ArgumentException("Unexpected dumpType", nameof(dumpType));
- }
- }
-
- public async Task<IProcessInfo> GetProcessAsync(ProcessFilter? filter, CancellationToken token)
- {
- var endpointInfos = await _endpointInfoSource.GetEndpointInfoAsync(token);
-
- if (filter.HasValue)
- {
- return await GetSingleProcessInfoAsync(
- endpointInfos,
- filter);
- }
-
- // Short-circuit for when running in a Docker container.
- if (RuntimeInfo.IsInDockerContainer)
- {
- try
- {
- IProcessInfo processInfo = await GetSingleProcessInfoAsync(
- endpointInfos,
- DockerEntrypointProcessFilter);
-
- using var timeoutSource = new CancellationTokenSource(DockerEntrypointWaitTimeout);
-
- var client = new DiagnosticsClient(processInfo.EndpointInfo.Endpoint);
- await client.WaitForConnectionAsync(timeoutSource.Token);
-
- return processInfo;
- }
- catch
- {
- // Process ID 1 doesn't exist, didn't advertise in connect mode, or is not a .NET process.
- }
- }
-
- return await GetSingleProcessInfoAsync(
- endpointInfos,
- filter: null);
- }
-
- private async Task<IProcessInfo> GetSingleProcessInfoAsync(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);
- }
- }
-
- IEndpointInfo[] endpointInfoArray = endpointInfos.ToArray();
- switch (endpointInfoArray.Length)
- {
- case 0:
- throw new ArgumentException("Unable to discover a target process.");
- case 1:
- return await ProcessInfo.FromEndpointInfoAsync(endpointInfoArray[0]);
- default:
-#if DEBUG
- IEndpointInfo endpointInfo = endpointInfoArray.FirstOrDefault(info => string.Equals(Process.GetProcessById(info.ProcessId).ProcessName, "iisexpress", StringComparison.OrdinalIgnoreCase));
- if (endpointInfo != null)
- {
- return await ProcessInfo.FromEndpointInfoAsync(endpointInfo);
- }
-#endif
- throw new ArgumentException("Unable to select a single target process because multiple target processes have been discovered.");
- }
- }
-
- public void Dispose()
- {
- _tokenSource.Cancel();
- }
-
- /// <summary>
- /// We want to make sure we destroy files we finish streaming.
- /// We want to make sure that we stream out files since we compress on the fly; the size cannot be known upfront.
- /// CONSIDER The above implies knowledge of how the file is used by the rest api.
- /// </summary>
- private sealed class AutoDeleteFileStream : FileStream
- {
- public AutoDeleteFileStream(string path) : base(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete,
- bufferSize: 4096, FileOptions.DeleteOnClose)
- {
- }
-
- public override bool CanSeek => false;
- }
-
-
- private sealed class ProcessInfo : IProcessInfo
- {
- // String returned for a process field when its value could not be retrieved. This is the same
- // value that is returned by the runtime when it could not determine the value for each of those fields.
- private const string ProcessFieldUnknownValue = "unknown";
-
- public ProcessInfo(
- IEndpointInfo endpointInfo,
- string commandLine,
- string processName)
- {
- EndpointInfo = endpointInfo;
-
- // The GetProcessInfo command will return "unknown" for values for which it does
- // not know the value, such as operating system and process architecture if the
- // process is running on one that is not predefined. Mimic the same behavior here
- // when the extra process information was not provided.
- CommandLine = commandLine ?? ProcessFieldUnknownValue;
- ProcessName = processName ?? ProcessFieldUnknownValue;
- }
-
- public static async Task<ProcessInfo> FromEndpointInfoAsync(IEndpointInfo endpointInfo)
- {
- using CancellationTokenSource extendedInfoCancellation = new CancellationTokenSource(ExtendedProcessInfoTimeout);
- return await FromEndpointInfoAsync(endpointInfo, extendedInfoCancellation.Token);
- }
-
- // Creates a ProcessInfo object from the IEndpointInfo. Attempts to get the command line using event pipe
- // if the endpoint information doesn't provide it. The cancelation token can be used to timebox this fallback
- // mechansim.
- public static async Task<ProcessInfo> FromEndpointInfoAsync(IEndpointInfo endpointInfo, CancellationToken extendedInfoCancellationToken)
- {
- if (null == endpointInfo)
- {
- throw new ArgumentNullException(nameof(endpointInfo));
- }
-
- var client = new DiagnosticsClient(endpointInfo.Endpoint);
-
- string commandLine = endpointInfo.CommandLine;
- if (string.IsNullOrEmpty(commandLine))
- {
- try
- {
- var infoSettings = new EventProcessInfoPipelineSettings
- {
- Duration = Timeout.InfiniteTimeSpan,
- };
-
- await using var pipeline = new EventProcessInfoPipeline(client, infoSettings,
- (cmdLine, token) => { commandLine = cmdLine; return Task.CompletedTask; });
-
- await pipeline.RunAsync(extendedInfoCancellationToken);
- }
- catch
- {
- }
- }
-
- string processName = null;
- if (!string.IsNullOrEmpty(commandLine))
- {
- // Get the process name from the command line
- bool isWindowsProcess = false;
- if (string.IsNullOrEmpty(endpointInfo.OperatingSystem))
- {
- // If operating system is null, the process is likely .NET Core 3.1 (which doesn't have the GetProcessInfo command).
- // Since the underlying diagnostic communication channel used by the .NET runtime requires that the diagnostic process
- // must be running on the same type of operating system as the target process (e.g. dotnet-monitor must be running on Windows
- // if the target process is running on Windows), then checking the local operating system should be a sufficient heuristic
- // to determine the operating system of the target process.
- isWindowsProcess = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
- }
- else
- {
- isWindowsProcess = ProcessOperatingSystemWindowsValue.Equals(endpointInfo.OperatingSystem, StringComparison.OrdinalIgnoreCase);
- }
-
- string processPath = CommandLineHelper.ExtractExecutablePath(commandLine, isWindowsProcess);
- if (!string.IsNullOrEmpty(processPath))
- {
- processName = Path.GetFileName(processPath);
- if (isWindowsProcess)
- {
- // Remove the extension on Windows to match the behavior of Process.ProcessName
- processName = Path.GetFileNameWithoutExtension(processName);
- }
- }
- }
-
- return new ProcessInfo(
- endpointInfo,
- commandLine,
- processName);
- }
-
- public IEndpointInfo EndpointInfo { get; }
-
- public string CommandLine { get; }
-
- public string OperatingSystem => EndpointInfo.OperatingSystem ?? ProcessFieldUnknownValue;
-
- public string ProcessArchitecture => EndpointInfo.ProcessArchitecture ?? ProcessFieldUnknownValue;
-
- public string ProcessName { get; }
- }
- }
-}
+++ /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
-{
- internal struct EgressResult
- {
- public EgressResult(string name, string value)
- {
- Name = name;
- Value = value;
- }
-
- public string Name { get; }
-
- public string Value { get; }
- }
-}
+++ /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.AspNetCore.Mvc;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- 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, 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, KeyValueLogScope scope)
- {
- _egress = (service, token) => service.EgressAsync(endpointName, action, artifactName, contentType, source, token);
- _scope = scope;
- }
-
- public override async Task ExecuteResultAsync(ActionContext context)
- {
- 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.EgressedArtifact(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
- // the specific configuration information for the egress provider, this value allows the
- // caller to more easily find the artifact after egress has completed.
- IDictionary<string, string> data = new Dictionary<string, string>(StringComparer.Ordinal);
- data.Add(egressResult.Name, egressResult.Value);
-
- 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 Microsoft.AspNetCore.Mvc;
-using System;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- internal static class ExceptionExtensions
- {
- public static ProblemDetails ToProblemDetails(this Exception ex, int statusCode)
- {
- return new ProblemDetails
- {
- Detail = ex.Message,
- Status = statusCode
- };
- }
- }
-}
+++ /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.AspNetCore.Mvc.ActionConstraints;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Options;
-using System;
-using System.Linq;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- /// <summary>
- /// We want to restrict the Prometheus scraping endpoint to only the /metrics call.
- /// To do this, we determine what port the request is on, and disallow other actions on the prometheus port.
- /// </summary>
- [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
- internal sealed class HostRestrictionAttribute : Attribute, IActionConstraintFactory
- {
- private sealed class HostConstraint : IActionConstraint
- {
- private readonly int?[] _restrictedPorts;
-
- public HostConstraint(int?[] restrictedPorts)
- {
- _restrictedPorts = restrictedPorts;
- }
-
- public int Order => 0;
-
- public bool Accept(ActionConstraintContext context)
- {
- return !_restrictedPorts.Any(port => context.RouteContext.HttpContext.Request.Host.Port == port);
- }
- }
-
- public bool IsReusable => true;
-
- public IActionConstraint CreateInstance(IServiceProvider services)
- {
- var metricsOptions = services.GetRequiredService<IOptions<MetricsOptions>>();
- return new HostConstraint(metricsOptions.Value.Enabled ? metricsOptions.Value.Ports : Array.Empty<int?>());
- }
- }
-}
+++ /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.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- /// <summary>
- /// Set of services provided by the monitoring tool. These are consumed by
- /// the REST Api.
- /// </summary>
- internal interface IDiagnosticServices : IDisposable
- {
- Task<IEnumerable<IProcessInfo>> GetProcessesAsync(CancellationToken token);
-
- Task<IProcessInfo> GetProcessAsync(ProcessFilter? filter, CancellationToken token);
-
- Task<Stream> GetDump(IProcessInfo pi, DumpType mode, CancellationToken token);
- }
-
-
- internal interface IProcessInfo
- {
- IEndpointInfo EndpointInfo { get; }
-
- string CommandLine { get; }
-
- public string OperatingSystem { get; }
-
- public string ProcessArchitecture { get; }
-
- string ProcessName { get; }
- }
-
- public enum DumpType
- {
- Full = 1,
- Mini,
- WithHeap,
- Triage
- }
-}
+++ /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.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- internal interface IEgressService
- {
- Task<EgressResult> EgressAsync(
- string endpointName,
- Func<CancellationToken, Task<Stream>> action,
- string fileName,
- string contentType,
- IEndpointInfo source,
- CancellationToken token);
-
- Task<EgressResult> EgressAsync(
- string endpointName,
- Func<Stream, CancellationToken, Task> action,
- string fileName,
- string contentType,
- IEndpointInfo source,
- CancellationToken token);
- }
-}
+++ /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.EventPipe;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Monitoring
-{
- /// <summary>
- /// Used to store metrics. A snapshot will be requested periodically.
- /// </summary>
- internal interface IMetricsStore : IDisposable
- {
- void AddMetric(ICounterPayload metric);
-
- Task SnapshotMetrics(Stream stream, CancellationToken token);
-
- void Clear();
- }
-}
+++ /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.
- //
- // Handled examples:
- // - Simple Console Logger: only calls ToString, thus the data needs to be formatted in the ToString method.
- // - JSON Console Logger: checks for IReadOnlyCollection<KeyValuePair<string, object>> and formats each value
- // in the enumeration; otherwise falls back to ToString.
- // - Event Log Logger: checks for IEnumerable<KeyValuePair<string, object>> and formats each value
- // in the enumeration; otherwise falls back to ToString.
- internal class KeyValueLogScope : IReadOnlyCollection<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();
- }
-
- int IReadOnlyCollection<KeyValuePair<string, object>>.Count => Values.Count;
-
- 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"));
- }
- }
-}
+++ /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.Linq;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- public enum LogFormat
- {
- None = 0,
- Json = 1,
- EventStream = 2
- }
-}
+++ /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;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- internal static class LoggingExtensions
- {
- private static readonly Action<ILogger, Exception> _requestFailed =
- LoggerMessage.Define(
- eventId: new EventId(1, "RequestFailed"),
- logLevel: LogLevel.Error,
- formatString: "Request failed.");
-
- private static readonly Action<ILogger, Exception> _requestCanceled =
- LoggerMessage.Define(
- eventId: new EventId(2, "RequestCanceled"),
- logLevel: LogLevel.Information,
- formatString: "Request canceled.");
-
- private static readonly Action<ILogger, Exception> _resolvedTargetProcess =
- LoggerMessage.Define(
- eventId: new EventId(3, "ResolvedTargetProcess"),
- logLevel: LogLevel.Debug,
- formatString: "Resolved target process.");
-
- private static readonly Action<ILogger, string, Exception> _egressedArtifact =
- LoggerMessage.Define<string>(
- eventId: new EventId(4, "EgressedArtifact"),
- logLevel: LogLevel.Information,
- formatString: "Egressed artifact to {location}");
-
- private static readonly Action<ILogger, Exception> _writtenToHttpStream =
- LoggerMessage.Define(
- eventId: new EventId(5, "WrittenToHttpStream"),
- logLevel: LogLevel.Information,
- formatString: "Written to HTTP stream.");
-
- public static void RequestFailed(this ILogger logger, Exception ex)
- {
- _requestFailed(logger, ex);
- }
-
- public static void RequestCanceled(this ILogger logger)
- {
- _requestCanceled(logger, null);
- }
-
- public static void ResolvedTargetProcess(this ILogger logger)
- {
- _resolvedTargetProcess(logger, null);
- }
-
- public static void EgressedArtifact(this ILogger logger, string location)
- {
- _egressedArtifact(logger, location, null);
- }
-
- public static void WrittenToHttpStream(this ILogger logger)
- {
- _writtenToHttpStream(logger, null);
- }
- }
-}
+++ /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.EventPipe;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-
-namespace Microsoft.Diagnostics.Monitoring
-{
- internal sealed class MetricsLogger : ICountersLogger
- {
- private readonly IMetricsStore _store;
-
- public MetricsLogger(IMetricsStore metricsStore)
- {
- _store = metricsStore;
- }
-
- public void Log(ICounterPayload metric)
- {
- _store.AddMetric(metric);
- }
-
- public void PipelineStarted()
- {
- }
-
- public void PipelineStopped()
- {
- }
- }
-}
+++ /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.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
-using System;
-using System.Collections.Generic;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- /// <summary>
- /// Configuration for prometheus metric collection and retrieval.
- /// TODO We may want to expose https endpoints here as well, and make port changes
- /// TODO How do we determine which process to scrape in multi-proc situations? How do we configure this
- /// for situations where the pid is not known or ambiguous?
- /// </summary>
- public class MetricsOptions
- {
- public const string ConfigurationKey = "Metrics";
-
- private readonly Lazy<int?[]> _ports;
-
- public MetricsOptions()
- {
- _ports = new Lazy<int?[]>(() =>
- {
- string[] endpoints = ConfigurationHelper.SplitValue(Endpoints);
- int?[] ports = new int?[endpoints.Length];
- for(int i = 0; i < endpoints.Length; i++)
- {
- //We cannot use Uri[Builder], since we are sometimes parsing invalid hostnames
- try
- {
- UriHelper.FromAbsolute(endpoints[i], out _, out HostString host, out _, out _, out _);
- ports[i] = host.Port;
- }
- catch (FormatException)
- {
- }
- }
- return ports;
- });
- }
-
- public bool Enabled { get; set; }
-
- public string Endpoints { get; set; }
-
- public int?[] Ports => _ports.Value;
-
- public int UpdateIntervalSeconds { get; set; }
-
- public int MetricCount { get; set; }
-
- public bool IncludeDefaultProviders { get; set; } = true;
-
- public bool AllowInsecureChannelForCustomMetrics { get; set; } = false;
-
- public List<MetricProvider> Providers { get; set; } = new List<MetricProvider>(0);
- }
-
- public class MetricProvider
- {
- public string ProviderName { get; set; }
- public List<string> CounterNames { get; set; } = new List<string>(0);
- }
-}
+++ /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.EventPipe;
-using Microsoft.Diagnostics.NETCore.Client;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Options;
-using Microsoft.Extensions.Primitives;
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- /// <summary>
- /// Periodically gets metrics from the app, and persists these to a metrics store.
- /// </summary>
- internal sealed class MetricsService : BackgroundService
- {
- private EventCounterPipeline _counterPipeline;
- private readonly IDiagnosticServices _services;
- private readonly MetricsStoreService _store;
- private IOptionsMonitor<MetricsOptions> _optionsMonitor;
-
- public MetricsService(IDiagnosticServices services,
- IOptionsMonitor<MetricsOptions> optionsMonitor,
- MetricsStoreService metricsStore)
- {
- _store = metricsStore;
- _services = services;
- _optionsMonitor = optionsMonitor;
- }
-
- protected override async Task ExecuteAsync(CancellationToken stoppingToken)
- {
- while (!stoppingToken.IsCancellationRequested)
- {
- stoppingToken.ThrowIfCancellationRequested();
-
- try
- {
- //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
- IProcessInfo pi = await _services.GetProcessAsync(filter: null, stoppingToken);
-
- var client = new DiagnosticsClient(pi.EndpointInfo.Endpoint);
-
- MetricsOptions options = _optionsMonitor.CurrentValue;
- using var optionsTokenSource = new CancellationTokenSource();
-
- //If metric options change, we need to cancel the existing metrics pipeline and restart with the new settings.
- using IDisposable monitorListener = _optionsMonitor.OnChange((_, _) => optionsTokenSource.Cancel());
-
- EventPipeCounterGroup[] counterGroups = Array.Empty<EventPipeCounterGroup>();
- if (options.Providers.Count > 0)
- {
- //In the dotnet-monitor case, custom metrics are additive to the default counters.
- var eventPipeCounterGroups = new List<EventPipeCounterGroup>();
- if (options.IncludeDefaultProviders)
- {
- eventPipeCounterGroups.Add(new EventPipeCounterGroup { ProviderName = MonitoringSourceConfiguration.SystemRuntimeEventSourceName });
- eventPipeCounterGroups.Add(new EventPipeCounterGroup { ProviderName = MonitoringSourceConfiguration.MicrosoftAspNetCoreHostingEventSourceName });
- eventPipeCounterGroups.Add(new EventPipeCounterGroup { ProviderName = MonitoringSourceConfiguration.GrpcAspNetCoreServer });
- }
-
- foreach (MetricProvider customProvider in options.Providers)
- {
- var customCounterGroup = new EventPipeCounterGroup { ProviderName = customProvider.ProviderName };
- if (customProvider.CounterNames.Count > 0)
- {
- customCounterGroup.CounterNames = customProvider.CounterNames.ToArray();
- }
- eventPipeCounterGroups.Add(customCounterGroup);
- }
- counterGroups = eventPipeCounterGroups.ToArray();
- }
-
- _counterPipeline = new EventCounterPipeline(client, new EventPipeCounterPipelineSettings
- {
- CounterGroups = counterGroups,
- Duration = Timeout.InfiniteTimeSpan,
- RefreshInterval = TimeSpan.FromSeconds(options.UpdateIntervalSeconds)
- }, loggers: new[] { new MetricsLogger(_store.MetricsStore) });
-
- using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken, optionsTokenSource.Token);
- await _counterPipeline.RunAsync(linkedTokenSource.Token);
- }
- catch (Exception e) when (e is not OperationCanceledException || !stoppingToken.IsCancellationRequested)
- {
- //Most likely we failed to resolve the pid or metric configuration change. Attempt to do this again.
- if (_counterPipeline != null)
- {
- await _counterPipeline.DisposeAsync();
- }
- await Task.Delay(5000);
- }
- }
- }
-
- public override async void Dispose()
- {
- base.Dispose();
- if (_counterPipeline != null)
- {
- await _counterPipeline.DisposeAsync();
- }
- }
- }
-}
+++ /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.EventPipe;
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Monitoring
-{
- /// <summary>
- /// Stores metrics, and produces a snapshot in Prometheus exposition format.
- /// </summary>
- internal sealed class MetricsStore : IMetricsStore
- {
- private static readonly Dictionary<string, string> KnownUnits = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
- {
- {string.Empty, string.Empty},
- {"count", string.Empty},
- {"B", "_bytes" },
- {"MB", "_bytes" },
- {"%", "_ratio" },
- };
-
- private sealed class MetricKey
- {
- private ICounterPayload _metric;
-
- public MetricKey(ICounterPayload metric)
- {
- _metric = metric;
- }
-
- public override int GetHashCode()
- {
- HashCode code = new HashCode();
- code.Add(_metric.Provider);
- code.Add(_metric.Name);
- return code.ToHashCode();
- }
-
- public override bool Equals(object obj)
- {
- if (obj is MetricKey metricKey)
- {
- return CompareMetrics(_metric, metricKey._metric);
- }
- return false;
- }
- }
-
- private Dictionary<MetricKey, Queue<ICounterPayload>> _allMetrics = new Dictionary<MetricKey, Queue<ICounterPayload>>();
- private readonly int _maxMetricCount;
-
- public MetricsStore(int maxMetricCount)
- {
- if (maxMetricCount < 1)
- {
- throw new ArgumentException("Invalid metric count");
- }
- _maxMetricCount = maxMetricCount;
- }
-
- public void AddMetric(ICounterPayload metric)
- {
- lock (_allMetrics)
- {
- var metricKey = new MetricKey(metric);
- if (!_allMetrics.TryGetValue(metricKey, out Queue<ICounterPayload> metrics))
- {
- metrics = new Queue<ICounterPayload>();
- _allMetrics.Add(metricKey, metrics);
- }
- metrics.Enqueue(metric);
- if (metrics.Count > _maxMetricCount)
- {
- metrics.Dequeue();
- }
- }
- }
-
- public async Task SnapshotMetrics(Stream outputStream, CancellationToken token)
- {
- Dictionary<MetricKey, Queue<ICounterPayload>> copy = null;
- lock (_allMetrics)
- {
- copy = new Dictionary<MetricKey, Queue<ICounterPayload>>();
- foreach (var metricGroup in _allMetrics)
- {
- copy.Add(metricGroup.Key, new Queue<ICounterPayload>(metricGroup.Value));
- }
- }
-
- using var writer = new StreamWriter(outputStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), bufferSize: 1024, leaveOpen: true);
- writer.NewLine = "\n";
-
- foreach (var metricGroup in copy)
- {
- ICounterPayload metricInfo = metricGroup.Value.First();
- string metricName = GetPrometheusMetric(metricInfo, out string metricValue);
- string metricType = "gauge";
-
- //TODO Some clr metrics claim to be incrementing, but are really gauges.
-
- await writer.WriteLineAsync(FormattableString.Invariant($"# HELP {metricName} {metricInfo.DisplayName}"));
- await writer.WriteLineAsync(FormattableString.Invariant($"# TYPE {metricName} {metricType}"));
-
- foreach (var metric in metricGroup.Value)
- {
- await WriteMetricDetails(writer, metric, metricName, metricValue);
- }
- }
- }
-
- private static async Task WriteMetricDetails(
- StreamWriter writer,
- ICounterPayload metric,
- string metricName,
- string metricValue)
- {
- await writer.WriteAsync(metricName);
- await writer.WriteLineAsync(FormattableString.Invariant($" {metricValue} {new DateTimeOffset(metric.Timestamp).ToUnixTimeMilliseconds()}"));
- }
-
- private static string GetPrometheusMetric(ICounterPayload metric, out string metricValue)
- {
- string unitSuffix = string.Empty;
-
- if ((metric.Unit != null) && (!KnownUnits.TryGetValue(metric.Unit, out unitSuffix)))
- {
- //TODO The prometheus data model does not allow certain characters. Units we are not expecting could cause a scrape failure.
- unitSuffix = "_" + metric.Unit;
- }
-
- double value = metric.Value;
- if (string.Equals(metric.Unit, "MB", StringComparison.OrdinalIgnoreCase))
- {
- value *= 1_000_000; //Note that the metric uses MB not MiB
- }
-
- metricValue = value.ToString(CultureInfo.InvariantCulture);
- return FormattableString.Invariant($"{metric.Provider.Replace(".", string.Empty).ToLowerInvariant()}_{metric.Name.Replace('-', '_')}{unitSuffix}");
- }
-
- private static bool CompareMetrics(ICounterPayload first, ICounterPayload second)
- {
- return string.Equals(first.Name, second.Name);
- }
-
- public void Clear()
- {
- lock (_allMetrics)
- {
- _allMetrics.Clear();
- }
- }
-
- 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.Extensions.Options;
-using System;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- internal sealed class MetricsStoreService : IDisposable
- {
- public MetricsStore MetricsStore { get; }
-
- public MetricsStoreService(
- IOptions<MetricsOptions> options)
- {
- MetricsStore = new MetricsStore(options.Value.MetricCount);
- }
-
- public void Dispose()
- {
- MetricsStore.Dispose();
- }
- }
-}
+++ /dev/null
-<Project Sdk="Microsoft.NET.Sdk.Web">
-
- <PropertyGroup>
- <TargetFrameworks>netstandard2.0;netcoreapp3.1</TargetFrameworks>
- <NoWarn>;1591;1701</NoWarn>
- <Description>REST Api surface for dotnet-monitor</Description>
- <!-- Tentatively create package so other teams can tentatively consume. -->
- <IsPackable>true</IsPackable>
- <PackageTags>Diagnostic</PackageTags>
- <PackageReleaseNotes>$(Description)</PackageReleaseNotes>
- <GenerateDocumentationFile>false</GenerateDocumentationFile>
- <IncludeSymbols>true</IncludeSymbols>
- <!-- Do not ship this package until ready to be consumed. -->
- <IsShipping>false</IsShipping>
- <OutputType>Library</OutputType>
- <!-- Version information -->
- <VersionPrefix>5.0.0</VersionPrefix>
- <LangVersion>preview</LangVersion>
- </PropertyGroup>
-
- <ItemGroup>
- <PackageReference Include="Microsoft.Diagnostics.Monitoring.EventPipe" Version="$(MicrosoftDiagnosticsMonitoringEventPipeVersion)" />
- </ItemGroup>
-
- <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
- <PackageReference Include="Microsoft.Bcl.HashCode" Version="$(MicrosoftBclHashCodeVersion)" />
- <PackageReference Include="Microsoft.AspNetCore" Version="$(MicrosoftAspNetCoreVersion)" />
- <PackageReference Include="Microsoft.AspNetCore.HttpsPolicy" Version="$(MicrosoftAspNetCoreHttpsPolicyVersion)" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(MicrosoftAspNetCoreMvcVersion)" />
- <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="$(MicrosoftAspNetCoreResponseCompressionVersion)" />
- </ItemGroup>
-
- <ItemGroup>
- <InternalsVisibleTo Include="dotnet-monitor" />
- </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.ComponentModel.DataAnnotations;
-using System.Runtime.Serialization;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer.Models
-{
- [DataContract(Name = "EventPipeConfiguration")]
- public class EventPipeConfigurationModel
- {
- [DataMember(Name = "providers", IsRequired = true)]
- public EventPipeProviderModel[] Providers { get; set; }
-
- [DataMember(Name = "requestRundown")]
- public bool RequestRundown { get; set; } = true;
-
- [DataMember(Name = "bufferSizeInMB")]
- [Range(1, 1024)]
- public int BufferSizeInMB { get; set; } = 256;
- }
-}
+++ /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.Generic;
-using System.Diagnostics.Tracing;
-using System.Runtime.Serialization;
-using Microsoft.Diagnostics.Monitoring.RestServer.Validation;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer.Models
-{
- [DataContract(Name = "EventPipeProvider")]
- public class EventPipeProviderModel
- {
- [DataMember(Name = "name", IsRequired = true)]
- public string Name { get; set; }
-
- [DataMember(Name = "keywords")]
- [IntegerOrHexString]
- public string Keywords { get; set; } = "0x" + EventKeywords.All.ToString("X");
-
- [DataMember(Name = "eventLevel")]
- public EventLevel EventLevel { get; set; } = EventLevel.Verbose;
-
- [DataMember(Name = "arguments")]
- public IDictionary<string, string> Arguments { 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.Runtime.Serialization;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer.Models
-{
- [DataContract(Name = "Process")]
- public class ProcessIdentifierModel
- {
- [DataMember(Name = "pid")]
- public int Pid { get; set; }
-
- [DataMember(Name = "uid")]
- public Guid Uid { get; set; }
-
- internal static ProcessIdentifierModel FromProcessInfo(IProcessInfo processInfo)
- {
- return new ProcessIdentifierModel()
- {
- Pid = processInfo.EndpointInfo.ProcessId,
- Uid = processInfo.EndpointInfo.RuntimeInstanceCookie
- };
- }
- }
-}
+++ /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.Runtime.Serialization;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer.Models
-{
- [DataContract(Name = "Process")]
- public class ProcessModel
- {
- [DataMember(Name = "pid")]
- public int Pid { get; set; }
-
- [DataMember(Name = "uid")]
- public Guid Uid { get; set; }
-
- [DataMember(Name = "name")]
- public string Name { get; private set; }
-
- [DataMember(Name = "commandLine")]
- public string CommandLine { get; private set; }
-
- [DataMember(Name = "os")]
- public string OperatingSystem { get; private set; }
-
- [DataMember(Name = "architecture")]
- public string ProcessArchitecture { get; private set; }
-
- internal static ProcessModel FromProcessInfo(IProcessInfo processInfo)
- {
- return new ProcessModel()
- {
- CommandLine = processInfo.CommandLine,
- Name = processInfo.ProcessName,
- OperatingSystem = processInfo.OperatingSystem,
- ProcessArchitecture = processInfo.ProcessArchitecture,
- Pid = processInfo.EndpointInfo.ProcessId,
- Uid = processInfo.EndpointInfo.RuntimeInstanceCookie
- };
- }
- }
-}
\ 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 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;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- internal sealed class OutputStreamResult : ActionResult
- {
- 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, KeyValueLogScope scope)
- {
- _contentType = contentType;
- _fileDownloadName = fileDownloadName;
- _action = action;
- _scope = scope;
- }
-
- public override async Task ExecuteResultAsync(ActionContext context)
- {
- ILogger<OutputStreamResult> logger = context.HttpContext.RequestServices
- .GetRequiredService<ILoggerFactory>()
- .CreateLogger<OutputStreamResult>();
-
- using var _ = logger.BeginScope(_scope);
-
- await context.InvokeAsync(async (token) =>
- {
- if (_fileDownloadName != null)
- {
- ContentDispositionHeaderValue contentDispositionHeaderValue = new ContentDispositionHeaderValue("attachment");
- contentDispositionHeaderValue.FileName = _fileDownloadName;
- context.HttpContext.Response.Headers["Content-Disposition"] = contentDispositionHeaderValue.ToString();
- }
- context.HttpContext.Response.Headers["Content-Type"] = _contentType;
-
-#if !NETSTANDARD2_0
- context.HttpContext.Features.Get<AspNetCore.Http.Features.IHttpResponseBodyFeature>()?.DisableBuffering();
-#else
- context.HttpContext.Features.Get<AspNetCore.Http.Features.IHttpBufferingFeature>()?.DisableResponseBuffering();
-#endif
-
- await _action(context.HttpContext.Response.Body, token);
-
- logger.WrittenToHttpStream();
- }, 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;
-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();
- }
- }
-}
+++ /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;
-
-namespace Microsoft.Diagnostics.Monitoring
-{
- internal sealed class ScopeState : IEnumerable<IReadOnlyList<KeyValuePair<string, object>>>
- {
- private readonly Stack<IReadOnlyList<KeyValuePair<string, object>>> _scopes = new Stack<IReadOnlyList<KeyValuePair<string, object>>>();
-
- private sealed class ScopeEntry : IDisposable
- {
- private readonly Stack<IReadOnlyList<KeyValuePair<string, object>>> _scopes;
-
- public ScopeEntry(Stack<IReadOnlyList<KeyValuePair<string, object>>> scopes, IReadOnlyList<KeyValuePair<string, object>> scope)
- {
- _scopes = scopes;
- _scopes.Push(scope);
- }
-
- public void Dispose()
- {
- _scopes.Pop();
- }
- }
-
- public IDisposable Push(IReadOnlyList<KeyValuePair<string, object>> scope)
- {
- return new ScopeEntry(_scopes, scope);
- }
-
- public IEnumerator<IReadOnlyList<KeyValuePair<string, object>>> GetEnumerator()
- {
- return _scopes.GetEnumerator();
- }
-
- IEnumerator IEnumerable.GetEnumerator()
- {
- return GetEnumerator();
- }
- }
-}
+++ /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.EventPipe;
-using Microsoft.Extensions.Logging;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-using System.Text.Json;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- /// <summary>
- /// This class is used to write structured event data in json format to an output stream.
- /// </summary>
- public sealed class StreamingLoggerProvider : ILoggerProvider
- {
- private readonly Stream _outputStream;
- private readonly LogFormat _format;
- private readonly LogLevel _logLevel;
-
- public StreamingLoggerProvider(Stream outputStream, LogFormat logFormat, LogLevel logLevel = LogLevel.Debug)
- {
- _outputStream = outputStream;
- _format = logFormat;
- _logLevel = logLevel;
- }
-
- public ILogger CreateLogger(string categoryName)
- {
- return new StreamingLogger(categoryName, _outputStream, _format, _logLevel);
- }
-
- public void Dispose()
- {
- }
- }
-
- public sealed class StreamingLogger : ILogger
- {
- private readonly ScopeState _scopes = new ScopeState();
- private readonly Stream _outputStream;
- private readonly string _categoryName;
- private readonly LogFormat _logFormat;
- private readonly LogLevel _logLevel;
-
- public StreamingLogger(string category, Stream outputStream, LogFormat format, LogLevel logLevel)
- {
- _outputStream = outputStream;
- _categoryName = category;
- _logFormat = format;
- _logLevel = logLevel;
- }
-
- public IDisposable BeginScope<TState>(TState state)
- {
- if (state is LogObject logObject)
- {
- return _scopes.Push(logObject);
- }
- return null;
- }
-
- public bool IsEnabled(LogLevel logLevel) => logLevel <= _logLevel;
-
- public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
- {
- if (_logFormat == LogFormat.Json)
- {
- LogJson(logLevel, eventId, state, exception, formatter);
- }
- else
- {
- LogEventStream(logLevel, eventId, state, exception, formatter);
- }
- }
-
- private void LogJson<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
- {
- Stream outputStream = _outputStream;
-
- //CONSIDER Should we cache up the loggers and writers?
- using (var jsonWriter = new Utf8JsonWriter(outputStream, new JsonWriterOptions { Indented = false }))
- {
- jsonWriter.WriteStartObject();
- jsonWriter.WriteString("LogLevel", logLevel.ToString());
- jsonWriter.WriteString("EventId", eventId.ToString());
- jsonWriter.WriteString("Category", _categoryName);
- if (exception != null)
- {
- jsonWriter.WriteString("Exception", formatter(state, exception));
- }
- else
- {
- jsonWriter.WriteString("Message", formatter(state, exception));
- }
-
- //Write out scope data
- jsonWriter.WriteStartObject("Scopes");
- foreach (IReadOnlyList<KeyValuePair<string, object>> scope in _scopes)
- {
- foreach (KeyValuePair<string, object> scopeValue in scope)
- {
- WriteKeyValuePair(jsonWriter, scopeValue);
- }
- }
- jsonWriter.WriteEndObject();
-
- //Write out structured data
- jsonWriter.WriteStartObject("Arguments");
- if (state is IEnumerable<KeyValuePair<string, object>> values)
- {
- foreach (KeyValuePair<string, object> arg in values)
- {
- WriteKeyValuePair(jsonWriter, arg);
- }
- }
- jsonWriter.WriteEndObject();
-
- jsonWriter.WriteEndObject();
- jsonWriter.Flush();
- }
-
- outputStream.WriteByte((byte)'\n');
- outputStream.Flush();
- }
-
- private void LogEventStream<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
- {
- Stream outputStream = _outputStream;
-
- using var writer = new StreamWriter(outputStream, Encoding.UTF8, 1024, leaveOpen: true) { NewLine = "\n" };
-
- //event: eventName (if exists)
- //data: level category[eventId]
- //data: message
- //data: => scope1, scope2 => scope3, scope4
- //\n
-
- if (!string.IsNullOrEmpty(eventId.Name))
- {
- writer.Write("event: ");
- writer.WriteLine(eventId.Name);
- }
- writer.Write("data: ");
- writer.Write(logLevel);
- writer.Write(" ");
- writer.Write(_categoryName);
- writer.Write('[');
- writer.Write(eventId.Id);
- writer.WriteLine(']');
- writer.Write("data: ");
- writer.WriteLine(formatter(state, exception));
-
- bool firstScope = true;
- foreach (IReadOnlyList<KeyValuePair<string, object>> scope in _scopes)
- {
- bool firstScopeEntry = true;
- foreach (KeyValuePair<string, object> scopeValue in scope)
- {
- if (firstScope)
- {
- writer.Write("data:");
- firstScope = false;
- }
-
- if (firstScopeEntry)
- {
- writer.Write(" => ");
- firstScopeEntry = false;
- }
- else
- {
- writer.Write(", ");
- }
- writer.Write(scopeValue.Key);
- writer.Write(':');
- writer.Write(scopeValue.Value);
- }
- }
- if (!firstScope)
- {
- writer.WriteLine();
- }
- writer.WriteLine();
- }
-
- private static void WriteKeyValuePair(Utf8JsonWriter jsonWriter, KeyValuePair<string, object> kvp)
- {
- jsonWriter.WritePropertyName(kvp.Key);
- switch (kvp.Value)
- {
- case string s:
- jsonWriter.WriteStringValue(s);
- break;
- case int i:
- jsonWriter.WriteNumberValue(i);
- break;
- case bool b:
- jsonWriter.WriteBooleanValue(b);
- break;
- case null:
- jsonWriter.WriteNullValue();
- break;
- default:
- jsonWriter.WriteStringValue(kvp.Value.ToString());
- break;
- }
- }
- }
-}
+++ /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.Linq;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer
-{
- [Flags]
- public enum TraceProfile
- {
- Cpu = 0x1,
- Http = 0x2,
- Logs = 0x4,
- Metrics = 0x8
- }
-}
+++ /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.ComponentModel.DataAnnotations;
-using System.Globalization;
-
-namespace Microsoft.Diagnostics.Monitoring.RestServer.Validation
-{
- public class IntegerOrHexStringAttribute : ValidationAttribute
- {
- protected override ValidationResult IsValid(object value, ValidationContext validationContext)
- {
- if (!(value is string stringValue))
- {
- return new ValidationResult("Value must be of type string.");
- }
- else if (!TryParse(stringValue, out _, out string error))
- {
- return new ValidationResult(error);
- }
- return ValidationResult.Success;
- }
-
- public static bool TryParse(string value, out long result, out string error)
- {
- result = 0;
- error = null;
-
- if (string.IsNullOrWhiteSpace(value))
- {
- error = "Value cannot be null, empty, or whitespace.";
- return false;
- }
- else if (value.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
- {
- // AllowHexSpecifier requires that the "0x" is removed before attempting to parse.
- // It parses the actual value, not the "0x" syntax prefix.
- if (!long.TryParse(value.Substring(2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out result))
- {
- error = FormattableString.Invariant($"The value '{value}' is not a valid hexadecimal number.");
- return false;
- }
- }
- else
- {
- if (!long.TryParse(value, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out result))
- {
- error = FormattableString.Invariant($"The value '{value}' is not a valid integer.");
- return false;
- }
- }
-
- 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.
-
-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);
- }
-}
+++ /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.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Diagnostics.Monitoring.RestServer;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-using Microsoft.Extensions.Primitives;
-using Microsoft.Net.Http.Headers;
-using System;
-using System.Buffers.Text;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Net.Http.Headers;
-using System.Security.Claims;
-using System.Security.Cryptography;
-using System.Text.Encodings.Web;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Tools.Monitor
-{
- /// <summary>
- /// Authenticates against the ApiKey stored on the server.
- /// </summary>
- internal sealed class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationHandlerOptions>
- {
- private static readonly string[] DisallowedHashAlgorithms = new string[]
- {
- "SHA", "SHA1", "System.Security.Cryptography.SHA1", "System.Security.Cryptography.HashAlgorithm", "MD5", "System.Security.Cryptography.MD5"
- };
-
- public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationHandlerOptions> options, ILoggerFactory loggerFactory, UrlEncoder encoder, ISystemClock clock)
- : base(options, loggerFactory, encoder, clock)
- {
- }
-
- protected override Task<AuthenticateResult> HandleAuthenticateAsync()
- {
- //We are expecting a header such as Authorization: <Schema> <key>
- //If this is not present, we will return NoResult and move on to the next authentication handler.
- if (!Request.Headers.TryGetValue(HeaderNames.Authorization, out StringValues values) ||
- !values.Any())
- {
- return Task.FromResult(AuthenticateResult.NoResult());
- }
-
- if (!AuthenticationHeaderValue.TryParse(values.First(), out AuthenticationHeaderValue authHeader))
- {
- return Task.FromResult(AuthenticateResult.Fail("Invalid authentication header"));
- }
-
- if (!string.Equals(authHeader.Scheme, Scheme.Name, StringComparison.OrdinalIgnoreCase))
- {
- return Task.FromResult(AuthenticateResult.NoResult());
- }
-
- //The user is passing a base 64-encoded version of the secret
- //We will be hash this and compare it to the secret in our configuration.
- byte[] secret = new byte[32];
- Span<byte> span = new Span<byte>(secret);
- if (!Convert.TryFromBase64String(authHeader.Parameter, span, out int bytesWritten) || bytesWritten < 32)
- {
- return Task.FromResult(AuthenticateResult.Fail("Invalid Api Key format"));
- }
-
- //HACK IOptionsMonitor and similiar do not properly update here even though the underlying
- //configuration is updated. We get the value directly from IConfiguration.
-
- var authenticationOptions = new ApiAuthenticationOptions();
- IConfiguration configService = Context.RequestServices.GetRequiredService<IConfiguration>();
- configService.Bind(ApiAuthenticationOptions.ConfigurationKey, authenticationOptions);
- string apiKeyHash = authenticationOptions.ApiKeyHash;
- if (apiKeyHash == null)
- {
- return Task.FromResult(AuthenticateResult.Fail("Server does not contain Api Key"));
- }
- if (string.IsNullOrEmpty(authenticationOptions.ApiKeyHashType))
- {
- return Task.FromResult(AuthenticateResult.Fail("Missing hash algorithm"));
- }
- if (DisallowedHashAlgorithms.Contains(authenticationOptions.ApiKeyHashType, StringComparer.OrdinalIgnoreCase))
- {
- return Task.FromResult(AuthenticateResult.Fail($"Disallowed hash algorithm {authenticationOptions.ApiKeyHashType}"));
- }
-
- using HashAlgorithm algorithm = HashAlgorithm.Create(authenticationOptions.ApiKeyHashType);
- if (algorithm == null)
- {
- return Task.FromResult(AuthenticateResult.Fail($"Invalid hash algorithm {authenticationOptions.ApiKeyHashType}"));
- }
-
- byte[] hashedSecret = algorithm.ComputeHash(secret);
-
- //ApiKeyHash is represented as a hex string. e.g. AABBCCDDEEFF
- byte[] apiKeyHashBytes = new byte[apiKeyHash.Length / 2];
- for (int i = 0; i < apiKeyHash.Length; i += 2)
- {
- if (!byte.TryParse(apiKeyHash.AsSpan(i, 2), NumberStyles.HexNumber, provider: NumberFormatInfo.InvariantInfo, result: out byte resultByte))
- {
- return Task.FromResult(AuthenticateResult.Fail("Invalid Api Key hash"));
- }
- apiKeyHashBytes[i / 2] = resultByte;
- }
-
- if (hashedSecret.SequenceEqual(apiKeyHashBytes))
- {
- return Task.FromResult(AuthenticateResult.Success(
- new AuthenticationTicket(
- new ClaimsPrincipal(new[] { new ClaimsIdentity(AuthConstants.ApiKeySchema) }),
- AuthConstants.ApiKeySchema)));
-
- }
- else
- {
- return Task.FromResult(AuthenticateResult.Fail("Invalid Api Key"));
- }
- }
- }
-}
+++ /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.AspNetCore.Authentication;
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Microsoft.Diagnostics.Tools.Monitor
-{
- internal sealed class ApiKeyAuthenticationHandlerOptions : AuthenticationSchemeOptions
- {
- }
-}
+++ /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.AspNetCore.Authorization;
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Microsoft.Diagnostics.Tools.Monitor
-{
- internal sealed class AuthorizedUserRequirement : IAuthorizationRequirement
- {
- }
-}
+++ /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.AspNetCore.Authentication.Negotiate;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.Diagnostics.Monitoring.RestServer;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Security.Claims;
-using System.Security.Principal;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Tools.Monitor
-{
- /// <summary>
- /// Handles authorization for both Negotiate and ApiKey authentication.
- /// </summary>
- internal sealed class UserAuthorizationHandler : AuthorizationHandler<AuthorizedUserRequirement>
- {
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthorizedUserRequirement requirement)
- {
- // If the schema type is ApiKey, we do not need further authorization.
- if (context.User.Identity.AuthenticationType == AuthConstants.ApiKeySchema)
- {
- context.Succeed(requirement);
- }
- else if ((context.User.Identity.AuthenticationType == AuthConstants.NtlmSchema) ||
- (context.User.Identity.AuthenticationType == AuthConstants.KerberosSchema) ||
- (context.User.Identity.AuthenticationType == AuthConstants.NegotiateSchema))
- {
- //Negotiate, Kerberos, or NTLM.
- //CONSIDER In the future, we may want to have configuration around a dotnet-monitor group sid instead.
- //We cannot check the user against BUILTIN\Administrators group membership, since the browser user
- //has a deny claim on Administrator group.
- //Validate that the user that logged in matches the user that is running dotnet-monitor
- //Do not allow at all if running as Administrator.
- WindowsIdentity currentUser = WindowsIdentity.GetCurrent();
- WindowsPrincipal principal = new WindowsPrincipal(currentUser);
- if (principal.IsInRole(WindowsBuiltInRole.Administrator))
- {
- return Task.CompletedTask;
- }
-
- Claim currentUserClaim = currentUser.Claims.FirstOrDefault(claim => string.Equals(claim.Type, ClaimTypes.PrimarySid));
- if ((currentUserClaim != null) && context.User.HasClaim(currentUserClaim.Type, currentUserClaim.Value))
- {
- context.Succeed(requirement);
- }
- }
-
- return Task.CompletedTask;
- }
- }
-}
+++ /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
-{
- public class DiagnosticPortOptions
- {
- public const string ConfigurationKey = "DiagnosticPort";
-
- public DiagnosticPortConnectionMode ConnectionMode { get; set; }
-
- public string EndpointName { get; set; }
-
- public int? MaxConnections { get; set; }
- }
-
- public enum DiagnosticPortConnectionMode
- {
- Connect,
- Listen
- }
-}
+++ /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.AspNetCore;
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Authentication.Negotiate;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Server.Kestrel.Core;
-using Microsoft.Diagnostics.Monitoring;
-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;
-using System.CommandLine.IO;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Runtime.InteropServices;
-using System.Security.Principal;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Tools.Monitor
-{
- internal sealed class DiagnosticsMonitorCommandHandler
- {
- private const string ConfigPrefix = "DotnetMonitor_";
- private const string SettingsFileName = "settings.json";
- private const string ProductFolderName = "dotnet-monitor";
-
- // Location where shared dotnet-monitor configuration is stored.
- // Windows: "%ProgramData%\dotnet-monitor
- // Other: /etc/dotnet-monitor
- private static readonly string SharedConfigDirectoryPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), ProductFolderName) :
- Path.Combine("/etc", ProductFolderName);
-
- private static readonly string SharedSettingsPath = Path.Combine(SharedConfigDirectoryPath, SettingsFileName);
-
- // Location where user's dotnet-monitor configuration is stored.
- // Windows: "%USERPROFILE%\.dotnet-monitor"
- // Other: "%XDG_CONFIG_HOME%/dotnet-monitor" OR "%HOME%/.config/dotnet-monitor"
- private static readonly string UserConfigDirectoryPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "." + ProductFolderName) :
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ProductFolderName);
-
- private static readonly string UserSettingsPath = Path.Combine(UserConfigDirectoryPath, SettingsFileName);
-
- public async Task<int> Start(CancellationToken token, IConsole console, string[] urls, string[] metricUrls, bool metrics, string diagnosticPort, bool noAuth)
- {
- //CONSIDER The console logger uses the standard AddConsole, and therefore disregards IConsole.
- using IHost host = CreateHostBuilder(console, urls, metricUrls, metrics, diagnosticPort, noAuth).Build();
- await host.RunAsync(token);
- return 0;
- }
-
- public IHostBuilder CreateHostBuilder(IConsole console, string[] urls, string[] metricUrls, bool metrics, string diagnosticPort, bool noAuth)
- {
- return Host.CreateDefaultBuilder()
- .UseContentRoot(AppContext.BaseDirectory) // Use the application root instead of the current directory
- .ConfigureAppConfiguration((IConfigurationBuilder builder) =>
- {
- //Note these are in precedence order.
- ConfigureEndpointInfoSource(builder, diagnosticPort);
- ConfigureMetricsEndpoint(builder, metrics, metricUrls);
- builder.AddCommandLine(new[] { "--urls", ConfigurationHelper.JoinValue(urls) });
-
- builder.AddJsonFile(UserSettingsPath, optional: true, reloadOnChange: true);
- builder.AddJsonFile(SharedSettingsPath, optional: true, reloadOnChange: true);
-
- //HACK Workaround for https://github.com/dotnet/runtime/issues/36091
- //KeyPerFile provider uses a file system watcher to trigger changes.
- //The watcher does not follow symlinks inside the watched directory, such as mounted files
- //in Kubernetes.
- //We get around this by watching the target folder of the symlink instead.
- //See https://github.com/kubernetes/kubernetes/master/pkg/volume/util/atomic_writer.go
- string path = SharedConfigDirectoryPath;
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && RuntimeInfo.IsInKubernetes)
- {
- string symlinkTarget = Path.Combine(SharedConfigDirectoryPath, "..data");
- if (Directory.Exists(symlinkTarget))
- {
- path = symlinkTarget;
- }
- }
-
- builder.AddKeyPerFile(path, optional: true, reloadOnChange: true);
- builder.AddEnvironmentVariables(ConfigPrefix);
- })
- .ConfigureServices((HostBuilderContext context, IServiceCollection services) =>
- {
- //TODO Many of these service additions should be done through extension methods
-
- AuthOptions authenticationOptions = new AuthOptions(noAuth ? KeyAuthenticationMode.NoAuth : KeyAuthenticationMode.StoredKey);
- services.AddSingleton<IAuthOptions>(authenticationOptions);
-
- List<string> authSchemas = null;
- if (authenticationOptions.EnableKeyAuth)
- {
- //Add support for Authentication and Authorization.
- AuthenticationBuilder authBuilder = services.AddAuthentication(options =>
- {
- options.DefaultAuthenticateScheme = AuthConstants.ApiKeySchema;
- options.DefaultChallengeScheme = AuthConstants.ApiKeySchema;
- })
- .AddScheme<ApiKeyAuthenticationHandlerOptions, ApiKeyAuthenticationHandler>(AuthConstants.ApiKeySchema, _ => { });
-
- authSchemas = new List<string> { AuthConstants.ApiKeySchema };
-
- if (authenticationOptions.EnableNegotiate)
- {
- //On Windows add Negotiate package. This will use NTLM to perform Windows Authentication.
- authBuilder.AddNegotiate();
- authSchemas.Add(AuthConstants.NegotiateSchema);
- }
- }
-
- //Apply Authorization Policy for NTLM. Without Authorization, any user with a valid login/password will be authorized. We only
- //want to authorize the same user that is running dotnet-monitor, at least for now.
- //Note this policy applies to both Authorization schemas.
- services.AddAuthorization(authOptions =>
- {
- if (authenticationOptions.EnableKeyAuth)
- {
- authOptions.AddPolicy(AuthConstants.PolicyName, (builder) =>
- {
- builder.AddRequirements(new AuthorizedUserRequirement());
- builder.RequireAuthenticatedUser();
- builder.AddAuthenticationSchemes(authSchemas.ToArray());
-
- });
- }
- else
- {
- authOptions.AddPolicy(AuthConstants.PolicyName, (builder) =>
- {
- builder.RequireAssertion((_) => true);
- });
- }
- });
-
- if (authenticationOptions.EnableKeyAuth)
- {
- services.AddSingleton<IAuthorizationHandler, UserAuthorizationHandler>();
- }
-
- services.Configure<DiagnosticPortOptions>(context.Configuration.GetSection(DiagnosticPortOptions.ConfigurationKey));
- services.AddSingleton<IEndpointInfoSource, FilteredEndpointInfoSource>();
- services.AddHostedService<FilteredEndpointInfoSourceHostedService>();
- services.AddSingleton<IDiagnosticServices, DiagnosticServices>();
- services.ConfigureEgress(context.Configuration);
- services.ConfigureMetrics(context.Configuration);
- services.AddSingleton<ExperimentalToolLogger>();
- })
- .ConfigureLogging(builder =>
- {
- // Always allow the experimental tool message to be logged
- ExperimentalToolLogger.AddLogFilter(builder);
- })
- .ConfigureWebHostDefaults(webBuilder =>
- {
- webBuilder.ConfigureKestrel((context, options) =>
- {
- //Note our priorities for hosting urls don't match the default behavior.
- //Default Kestrel behavior priority
- //1) ConfigureKestrel settings
- //2) Command line arguments (--urls)
- //3) Environment variables (ASPNETCORE_URLS, then DOTNETCORE_URLS)
-
- //Our precedence
- //1) Environment variables (ASPNETCORE_URLS, DotnetMonitor_Metrics__Endpoints)
- //2) Command line arguments (these have defaults) --urls, --metricUrls
- //3) ConfigureKestrel is used for fine control of the server, but honors the first two configurations.
-
- string hostingUrl = context.Configuration.GetValue<string>(WebHostDefaults.ServerUrlsKey);
- urls = ConfigurationHelper.SplitValue(hostingUrl);
-
- var metricsOptions = new MetricsOptions();
- context.Configuration.Bind(MetricsOptions.ConfigurationKey, metricsOptions);
-
- if (metricsOptions.Enabled)
- {
- metricUrls = ProcessMetricUrls(metricUrls, metricsOptions);
- urls = urls.Concat(metricUrls).ToArray();
- }
-
- bool boundListeningPort = false;
-
- //Workaround for lack of default certificate. See https://github.com/dotnet/aspnetcore/issues/28120
- options.Configure(context.Configuration.GetSection("Kestrel")).Load();
-
- //By default, we bind to https for sensitive data (such as dumps and traces) and bind http for
- //non-sensitive data such as metrics. We may be missing a certificate for https binding. We want to continue with the
- //http binding in that scenario.
- foreach (BindingAddress url in urls.Select(BindingAddress.Parse))
- {
- Action<ListenOptions> configureListenOptions = (listenOptions) =>
- {
- if (url.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
- {
- listenOptions.UseHttps();
- }
- };
-
- try
- {
- if (url.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase))
- {
- options.ListenLocalhost(url.Port, configureListenOptions);
- }
- else if (IPAddress.TryParse(url.Host, out IPAddress ipAddress))
- {
- options.Listen(ipAddress, url.Port, configureListenOptions);
- }
- else
- {
- options.ListenAnyIP(url.Port, configureListenOptions);
- }
- boundListeningPort = true;
- }
- catch (InvalidOperationException e)
- {
- //This binding failure is typically due to missing default certificate
- console.Error.WriteLine($"Unable to bind to {url}. Dotnet-monitor functionality will be limited.");
- console.Error.WriteLine(e.Message);
- }
- }
-
- //If we end up not binding any ports, Kestrel defaults to port 5000. Make sure we don't attempt this.
- if (!boundListeningPort)
- {
- throw new InvalidOperationException("Unable to bind any urls.");
- }
- })
- .UseStartup<Startup>();
- });
- }
-
- private static string[] ProcessMetricUrls(string[] metricUrls, MetricsOptions metricsOptions)
- {
- string metricUrlFromConfig = metricsOptions.Endpoints;
- if (!string.IsNullOrEmpty(metricUrlFromConfig))
- {
- metricUrls = ConfigurationHelper.SplitValue(metricUrlFromConfig);
- }
-
- //If we have custom metrics we want to upgrade the metrics transport channel to https, but
- //also respect the user's configuration to leave it insecure.
- if ((metricsOptions.Providers.Count > 0) &&
- (!metricsOptions.AllowInsecureChannelForCustomMetrics) &&
- (metricsOptions.Providers.Any(provider =>
- !Monitoring.EventPipe.MonitoringSourceConfiguration.DefaultMetricProviders.Contains(provider.ProviderName, StringComparer.OrdinalIgnoreCase))))
- {
- for (int i = 0; i < metricUrls.Length; i++)
- {
- BindingAddress metricUrl = BindingAddress.Parse(metricUrls[i]);
-
- //Upgrade http to https by default.
- if (metricUrl.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase))
- {
- //Based on BindAddress.ToString
- metricUrls[i] = string.Concat(Uri.UriSchemeHttps.ToLowerInvariant(),
- Uri.SchemeDelimiter,
- metricUrl.Host.ToLowerInvariant(),
- ":",
- metricUrl.Port.ToString(System.Globalization.CultureInfo.InvariantCulture),
- metricUrl.PathBase);
- }
- }
- }
-
- return metricUrls;
- }
-
- private static void ConfigureMetricsEndpoint(IConfigurationBuilder builder, bool enableMetrics, string[] metricEndpoints)
- {
- builder.AddInMemoryCollection(new Dictionary<string, string>
- {
- {ConfigurationHelper.MakeKey(MetricsOptions.ConfigurationKey, nameof(MetricsOptions.Endpoints)), string.Join(';', metricEndpoints)},
- {ConfigurationHelper.MakeKey(MetricsOptions.ConfigurationKey, nameof(MetricsOptions.Enabled)), enableMetrics.ToString()},
- {ConfigurationHelper.MakeKey(MetricsOptions.ConfigurationKey, nameof(MetricsOptions.UpdateIntervalSeconds)), 10.ToString()},
- {ConfigurationHelper.MakeKey(MetricsOptions.ConfigurationKey, nameof(MetricsOptions.MetricCount)), 3.ToString()}
- });
- }
-
- private static void ConfigureEndpointInfoSource(IConfigurationBuilder builder, string diagnosticPort)
- {
- DiagnosticPortConnectionMode connectionMode = string.IsNullOrEmpty(diagnosticPort) ? DiagnosticPortConnectionMode.Connect : DiagnosticPortConnectionMode.Listen;
- builder.AddInMemoryCollection(new Dictionary<string, string>
- {
- {ConfigurationHelper.MakeKey(DiagnosticPortOptions.ConfigurationKey, nameof(DiagnosticPortOptions.ConnectionMode)), connectionMode.ToString()},
- {ConfigurationHelper.MakeKey(DiagnosticPortOptions.ConfigurationKey, nameof(DiagnosticPortOptions.EndpointName)), diagnosticPort}
- });
- }
- }
-}
+++ /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 Azure;
-using Azure.Storage;
-using Azure.Storage.Blobs;
-using Azure.Storage.Blobs.Models;
-using Azure.Storage.Blobs.Specialized;
-using Microsoft.Extensions.Logging;
-using System;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Tools.Monitor.Egress.AzureStorage
-{
- /// <summary>
- /// Egress provider for egressing stream data to an Azure blob storage account.
- /// </summary>
- /// <remarks>
- /// Blobs created through this provider will overwrite existing blobs if they have the same blob name.
- /// </remarks>
- internal class AzureBlobEgressProvider :
- EgressProvider<AzureBlobEgressProviderOptions, AzureBlobEgressStreamOptions>
- {
- public AzureBlobEgressProvider(AzureBlobEgressProviderOptions options, ILogger logger = null)
- : base(options, logger)
- {
- }
-
- public override async Task<string> EgressAsync(
- Func<CancellationToken, Task<Stream>> action,
- string name,
- AzureBlobEgressStreamOptions streamOptions,
- CancellationToken token)
- {
- LogAndValidateOptions(streamOptions, name);
-
- try
- {
- var containerClient = await GetBlobContainerClientAsync(token);
-
- BlobClient blobClient = containerClient.GetBlobClient(GetBlobName(name));
-
- Logger?.EgressProviderInvokeStreamAction(EgressProviderTypes.AzureBlobStorage);
- using var stream = await action(token);
-
- // Write blob content, headers, and metadata
- await blobClient.UploadAsync(stream, CreateHttpHeaders(streamOptions), streamOptions.Metadata, cancellationToken: token);
-
- string blobUriString = GetBlobUri(blobClient);
- Logger?.EgressProviderSavedStream(EgressProviderTypes.AzureBlobStorage, blobUriString);
- return blobUriString;
- }
- catch (AggregateException ex) when (ex.InnerException is RequestFailedException innerException)
- {
- throw CreateException(innerException);
- }
- catch (RequestFailedException ex)
- {
- throw CreateException(ex);
- }
- }
-
- public override async Task<string> EgressAsync(
- Func<Stream, CancellationToken, Task> action,
- string name,
- AzureBlobEgressStreamOptions streamOptions,
- CancellationToken token)
- {
- LogAndValidateOptions(streamOptions, name);
-
- try
- {
- var containerClient = await GetBlobContainerClientAsync(token);
-
- BlockBlobClient blobClient = containerClient.GetBlockBlobClient(GetBlobName(name));
-
- // Write blob content
- using (Stream blobStream = await blobClient.OpenWriteAsync(overwrite: true, cancellationToken: token))
- {
- Logger?.EgressProviderInvokeStreamAction(EgressProviderTypes.AzureBlobStorage);
- await action(blobStream, token);
-
- await blobStream.FlushAsync(token);
- }
-
- // Write blob headers
- await blobClient.SetHttpHeadersAsync(CreateHttpHeaders(streamOptions), cancellationToken: token);
-
- // Write blob metadata
- await blobClient.SetMetadataAsync(streamOptions.Metadata, cancellationToken: token);
-
- string blobUriString = GetBlobUri(blobClient);
- Logger?.EgressProviderSavedStream(EgressProviderTypes.AzureBlobStorage, blobUriString);
- return blobUriString;
- }
- catch (AggregateException ex) when (ex.InnerException is RequestFailedException innerException)
- {
- throw CreateException(innerException);
- }
- catch (RequestFailedException ex)
- {
- throw CreateException(ex);
- }
- }
-
- private void LogAndValidateOptions(AzureBlobEgressStreamOptions streamOptions, string fileName)
- {
- Logger?.EgressProviderOptionValue(EgressProviderTypes.AzureBlobStorage, nameof(Options.AccountKey), Options.AccountKey, redact: true);
- Logger?.EgressProviderOptionValue(EgressProviderTypes.AzureBlobStorage, nameof(Options.AccountUri), GetAccountUri(out _));
- Logger?.EgressProviderOptionValue(EgressProviderTypes.AzureBlobStorage, nameof(Options.BlobPrefix), Options.BlobPrefix);
- Logger?.EgressProviderOptionValue(EgressProviderTypes.AzureBlobStorage, nameof(Options.ContainerName), Options.ContainerName);
- Logger?.EgressProviderOptionValue(EgressProviderTypes.AzureBlobStorage, nameof(Options.SharedAccessSignature), Options.SharedAccessSignature, redact: true);
- Logger?.EgressStreamOptionValue(EgressProviderTypes.AzureBlobStorage, nameof(streamOptions.ContentEncoding), streamOptions.ContentEncoding);
- Logger?.EgressStreamOptionValue(EgressProviderTypes.AzureBlobStorage, nameof(streamOptions.ContentType), streamOptions.ContentType);
- Logger?.EgressStreamOptionValue(EgressProviderTypes.AzureBlobStorage, nameof(streamOptions.Metadata), "[" + string.Join(", ", streamOptions.Metadata.Keys) + "]");
- Logger?.EgressProviderFileName(EgressProviderTypes.AzureBlobStorage, fileName);
-
- ValidateOptions();
- }
-
- private Uri GetAccountUri(out string accountName)
- {
- var blobUriBuilder = new BlobUriBuilder(Options.AccountUri);
- blobUriBuilder.Query = null;
- blobUriBuilder.BlobName = null;
- blobUriBuilder.BlobContainerName = null;
-
- accountName = blobUriBuilder.AccountName;
-
- return blobUriBuilder.ToUri();
- }
-
- private async Task<BlobContainerClient> GetBlobContainerClientAsync(CancellationToken token)
- {
- BlobServiceClient serviceClient;
- if (!string.IsNullOrWhiteSpace(Options.SharedAccessSignature))
- {
- var serviceUriBuilder = new UriBuilder(Options.AccountUri)
- {
- Query = Options.SharedAccessSignature
- };
-
- serviceClient = new BlobServiceClient(serviceUriBuilder.Uri);
- }
- else if (!string.IsNullOrEmpty(Options.AccountKey))
- {
- // Remove Query in case SAS token was specified
- Uri accountUri = GetAccountUri(out string accountName);
-
- StorageSharedKeyCredential credential = new StorageSharedKeyCredential(
- accountName,
- Options.AccountKey);
-
- serviceClient = new BlobServiceClient(accountUri, credential);
- }
- else
- {
- throw CreateException("SharedAccessSignature or AccountKey must be specified.");
- }
-
- BlobContainerClient containerClient = serviceClient.GetBlobContainerClient(Options.ContainerName);
- await containerClient.CreateIfNotExistsAsync(cancellationToken: token);
-
- return containerClient;
- }
-
- private string GetBlobName(string fileName)
- {
- if (string.IsNullOrEmpty(Options.BlobPrefix))
- {
- return fileName;
- }
- else
- {
- return string.Concat(Options.BlobPrefix, "/", fileName);
- }
- }
-
- private BlobHttpHeaders CreateHttpHeaders(AzureBlobEgressStreamOptions streamOptions)
- {
- BlobHttpHeaders headers = new BlobHttpHeaders();
- headers.ContentEncoding = streamOptions.ContentEncoding;
- headers.ContentType = streamOptions.ContentType;
- return headers;
- }
-
- private static string GetBlobUri(BlobBaseClient client)
- {
- // The BlobClient URI has the SAS token as the query parameter
- // Remove the SAS token before returning the URI
- UriBuilder outputBuilder = new UriBuilder(client.Uri);
- outputBuilder.Query = null;
-
- return outputBuilder.Uri.ToString();
- }
-
- private static EgressException CreateException(string message)
- {
- return new EgressException(WrapMessage(message));
- }
-
- private static EgressException CreateException(Exception innerException)
- {
- return new EgressException(WrapMessage(innerException.Message), innerException);
- }
-
- private static string WrapMessage(string innerMessage)
- {
- if (!string.IsNullOrEmpty(innerMessage))
- {
- return $"Azure blob egress failed: {innerMessage}";
- }
- else
- {
- return "Azure blob egress failed.";
- }
- }
- }
-}
+++ /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.ComponentModel.DataAnnotations;
-
-namespace Microsoft.Diagnostics.Tools.Monitor.Egress.AzureStorage
-{
- /// <summary>
- /// Egress provider options for Azure blob storage.
- /// </summary>
- internal class AzureBlobEgressProviderOptions :
- EgressProviderOptions,
- IValidatableObject
- {
- /// <summary>
- /// The URI of the Azure blob storage account.
- /// </summary>
- [Required]
- public Uri AccountUri { get; set; }
-
- /// <summary>
- /// The acount key used to access the Azure blob storage account.
- /// </summary>
- /// <remarks>
- /// If not provided, <see cref="AzureBlobEgressProviderOptions.SharedAccessSignature"/> must be specified.
- /// </remarks>
- public string AccountKey { get; set; }
-
- /// <summary>
- /// The shared access signature (SAS) used to access the azure blob storage account.
- /// </summary>
- /// <remarks>
- /// If not provided, <see cref="AzureBlobEgressProviderOptions.AccountKey"/> must be specified.
- /// </remarks>
- public string SharedAccessSignature { get; set; }
-
- /// <summary>
- /// The name of the container to which the blob will be egressed. If egressing to the root container,
- /// use the "$root" sentinel value.
- /// </summary>
- [Required]
- public string ContainerName { get; set; }
-
- /// <summary>
- /// The prefix to prepend to the blob name.
- /// </summary>
- public string BlobPrefix { get; set; }
-
- IEnumerable<ValidationResult> IValidatableObject.Validate(ValidationContext validationContext)
- {
- IList<ValidationResult> results = new List<ValidationResult>();
-
- // One of the authentication keys/tokens is required
- if (string.IsNullOrEmpty(AccountKey) && string.IsNullOrEmpty(SharedAccessSignature))
- {
- results.Add(new ValidationResult($"The {nameof(AccountKey)} field or the {nameof(SharedAccessSignature)} field is required."));
- }
-
- return results;
- }
- }
-}
+++ /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;
-
-namespace Microsoft.Diagnostics.Tools.Monitor.Egress.AzureStorage
-{
- /// <summary>
- /// Egress stream options for Azure blob storage.
- /// </summary>
- internal class AzureBlobEgressStreamOptions
- {
- /// <summary>
- /// The content encoding of the blob to be created.
- /// </summary>
- public string ContentEncoding { get; set; }
-
- /// <summary>
- /// The content type of the blob to be created.
- /// </summary>
- public string ContentType { get; set; }
-
- /// <summary>
- /// The metadata of the blob to be created.
- /// </summary>
- public Dictionary<string, string> Metadata { get; }
- = new Dictionary<string, string>(StringComparer.Ordinal);
- }
-}
+++ /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.RestServer;
-using Microsoft.Diagnostics.Tools.Monitor.Egress.AzureStorage;
-using Microsoft.Extensions.Configuration;
-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;
-
-namespace Microsoft.Diagnostics.Tools.Monitor.Egress
-{
- /// <summary>
- /// Creates <see cref="ConfiguredEgressProvider"/> for Azure blob storage egress.
- /// </summary>
- internal class AzureBlobEgressFactory : EgressFactory
- {
- private readonly ILoggerFactory _loggerFactory;
-
- public AzureBlobEgressFactory(ILoggerFactory loggerFactory)
- : base(loggerFactory.CreateLogger<AzureBlobEgressFactory>())
- {
- _loggerFactory = loggerFactory;
- }
-
- public override bool TryCreate(
- string providerName,
- IConfigurationSection providerSection,
- Dictionary<string, string> egressProperties,
- out ConfiguredEgressProvider provider)
- {
- var options = providerSection.Get<ConfigurationOptions>();
-
- // If account key was not provided but the name was provided,
- // lookup the account key property value from EgressOptions.Properties
- if (string.IsNullOrEmpty(options.AccountKey) &&
- !string.IsNullOrEmpty(options.AccountKeyName))
- {
- if (TryGetPropertyValue(providerName, egressProperties, options.AccountKeyName, out string key))
- {
- options.AccountKey = key;
- }
- }
-
- // If shared access signature (SAS) was not provided but the name was provided,
- // lookup the SAS property value from EgressOptions.Properties
- if (string.IsNullOrEmpty(options.SharedAccessSignature) &&
- !string.IsNullOrEmpty(options.SharedAccessSignatureName))
- {
- if (TryGetPropertyValue(providerName, egressProperties, options.SharedAccessSignatureName, out string signature))
- {
- options.SharedAccessSignature = signature;
- }
- }
-
- if (!TryValidateOptions(options, providerName))
- {
- provider = null;
- return false;
- }
-
- provider = new Provider(options, _loggerFactory);
- return true;
- }
-
- private bool TryGetPropertyValue(string providerName, IDictionary<string, string> egressProperties, string propertyName, out string value)
- {
- if (!egressProperties.TryGetValue(propertyName, out value))
- {
- Logger.EgressProviderUnableToFindPropertyKey(providerName, propertyName);
- return false;
- }
- return true;
- }
-
- private class Provider : ConfiguredEgressProvider
- {
- private readonly AzureBlobEgressProvider _provider;
-
- public Provider(AzureBlobEgressProviderOptions options, ILoggerFactory loggerFactory)
- {
- _provider = new AzureBlobEgressProvider(options, loggerFactory.CreateLogger<AzureBlobEgressProvider>());
- }
-
- public override async Task<EgressResult> EgressAsync(
- Func<CancellationToken, Task<Stream>> action,
- string fileName,
- string contentType,
- IEndpointInfo source,
- CancellationToken token)
- {
- 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);
- }
-
- public override async Task<EgressResult> EgressAsync(
- Func<Stream, CancellationToken, Task> action,
- string fileName,
- string contentType,
- IEndpointInfo source,
- CancellationToken token)
- {
- 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>
- /// Egress provider options for Azure blob storage with additional options.
- /// </summary>
- private class ConfigurationOptions : AzureBlobEgressProviderOptions
- {
- /// <summary>
- /// The name of the account key used to look up the value from the <see cref="EgressOptions.Properties"/> map.
- /// </summary>
- public string AccountKeyName { get; set; }
-
- /// <summary>
- /// The name of the shared access signature (SAS) used to look up the value from the <see cref="EgressOptions.Properties"/> map.
- /// </summary>
- public string SharedAccessSignatureName { 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.Configuration;
-
-namespace Microsoft.Diagnostics.Tools.Monitor.Egress.Configuration
-{
- internal static class EgressConfigurationExtensions
- {
- /// <summary>
- /// Get the Egress configuration section from the specified configuration.
- /// </summary>
- public static IConfigurationSection GetEgressSection(this IConfiguration configuration)
- {
- return configuration.GetSection(EgressOptions.ConfigurationKey);
- }
- }
-}
+++ /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.RestServer;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-
-namespace Microsoft.Diagnostics.Tools.Monitor.Egress.Configuration
-{
- /*
- * == Egress Configuration Design ==
- * - Each type of egress is called an egress type. The following are the defined egress types:
- * - AzureBlobStorage: Allows egressing stream data to a blob in Azure blob storage.
- * - FileSystem: Allows egressing stream data to the file system.
- * - An egress type in combination with its well defined set of options is called a egress provider.
- * Each egress provider is named in the egress configuration in order to identify individual providers.
- * - All egress configuration information is found in the root Egress configuration section. This section
- * has two immediate subsections:
- * - Providers: a mapping of egress provider names to egress provider options + the egress type.
- * - Each provider must have a 'type' field, which must have a value that is one of the egress types.
- * This field informs the egress configuration the type of egress provider that should be constructed
- * with the remaining options.
- * - If a provider's options fail validation, the failure is reported and the provider will not be
- * available to be used as a means of egress.
- * - Properties: a mapping of named values, typically for storing secrets (account keys, SAS, etc) by name.
- */
-
- /// <summary>
- /// Binds egress configuration information to an <see cref="EgressOptions"/> instance.
- /// </summary>
- internal class EgressConfigureOptions : IConfigureOptions<EgressOptions>
- {
- private readonly IConfiguration _configuration;
- private readonly ILogger<EgressConfigureOptions> _logger;
- private readonly IDictionary<string, EgressFactory> _factories
- = new Dictionary<string, EgressFactory>(StringComparer.OrdinalIgnoreCase);
-
- public EgressConfigureOptions(
- ILoggerFactory loggerFactory,
- IConfiguration configuration)
- {
- _configuration = configuration;
- _logger = loggerFactory.CreateLogger<EgressConfigureOptions>();
-
- // Register egress providers
- _factories.Add(EgressProviderTypes.AzureBlobStorage, new AzureBlobEgressFactory(loggerFactory));
- _factories.Add(EgressProviderTypes.FileSystem, new FileSystemEgressFactory(loggerFactory));
- }
-
- public void Configure(EgressOptions options)
- {
- IConfigurationSection egressSection = _configuration.GetEgressSection();
-
- IConfigurationSection propertiesSection = egressSection.GetSection(nameof(EgressOptions.Properties));
- propertiesSection.Bind(options.Properties);
-
- IConfigurationSection providersSection = egressSection.GetSection(nameof(EgressOptions.Providers));
- foreach (var providerSection in providersSection.GetChildren())
- {
- string providerName = providerSection.Key;
-
- KeyValueLogScope providerNameScope = new KeyValueLogScope();
- providerNameScope.Values.Add("EgressProviderName", providerName);
- using var providerNameRegistration = _logger.BeginScope(providerNameScope);
-
- CommonEgressProviderOptions commonOptions = new CommonEgressProviderOptions();
- providerSection.Bind(commonOptions);
-
- EgressProviderValidation validation = new EgressProviderValidation(providerName, _logger);
- if (!validation.TryValidate(commonOptions))
- {
- _logger.EgressProviderInvalidOptions(providerName);
- }
-
- string providerType = commonOptions.Type;
- KeyValueLogScope providerTypeScope = new KeyValueLogScope();
- providerTypeScope.Values.Add("EgressProviderType", providerType);
- using var providerTypeRegistration = _logger.BeginScope(providerTypeScope);
-
- if (!_factories.TryGetValue(providerType, out EgressFactory factory))
- {
- _logger.EgressProviderInvalidType(providerName, providerType);
- continue;
- }
-
- if (!factory.TryCreate(providerName, providerSection, options.Properties, out ConfiguredEgressProvider provider))
- {
- _logger.EgressProviderInvalidOptions(providerName);
- continue;
- }
-
- options.Providers.Add(providerName, provider);
-
- _logger.EgressProviderAdded(providerName);
- }
- }
-
- /// <summary>
- /// Configuration options to all egress providers.
- /// </summary>
- private class CommonEgressProviderOptions
- {
- /// <summary>
- /// The type of the egress provider.
- /// </summary>
- [Required]
- public string Type { 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;
-
-namespace Microsoft.Diagnostics.Tools.Monitor.Egress.Configuration
-{
- /// <summary>
- /// Configuration options for specifying egress providers.
- /// </summary>
- internal class EgressOptions
- {
- public const string ConfigurationKey = "Egress";
-
- /// <summary>
- /// Mapping of egress provider names to egress provider implementations.
- /// </summary>
- public Dictionary<string, ConfiguredEgressProvider> Providers { get; }
- = new Dictionary<string, ConfiguredEgressProvider>(StringComparer.OrdinalIgnoreCase);
-
- /// <summary>
- /// Mapping of keyed values, typically used for naming a secret such as a storage account
- /// key or shared access signature rather than embedding values directly in the egress provider options.
- /// </summary>
- public Dictionary<string, string> Properties { get; }
- = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- }
-}
+++ /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.RestServer;
-using System;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Tools.Monitor.Egress
-{
- /// <summary>
- /// Base class for configured egress providers. A configured egress provider is an egress provider instance
- /// that does not require any additional configuration beyond information about the stream that will be egressed.
- /// </summary>
- internal abstract class ConfiguredEgressProvider
- {
- /// <summary>
- /// Egress a stream via a callback by returning the stream from the callback.
- /// </summary>
- /// <param name="action">Callback that is invoked in order to get the stream to be egressed.</param>
- /// <param name="fileName">The name of the stream data, typically used as the file name.</param>
- /// <param name="contentType">The type of content contained by the stream.</param>
- /// <param name="source">The source of the egress artifact; for for interrogation purposed to fill out additional stream option data.</param>
- /// <param name="token">The token to monitor for cancellation requests.</param>
- /// <returns>A task that completes with an <see cref="EgressResult"/> describing the completion of the egress operation.</returns>
- public abstract Task<EgressResult> EgressAsync(
- Func<CancellationToken, Task<Stream>> action,
- string fileName,
- string contentType,
- IEndpointInfo source,
- CancellationToken token);
-
- /// <summary>
- /// Egress a stream via a callback by writing to the provided stream.
- /// </summary>
- /// <param name="action">Callback that is invoked in order to write data to the provided stream.</param>
- /// <param name="fileName">The name of the stream data, typically used as the file name.</param>
- /// <param name="contentType">The type of content contained by the stream.</param>
- /// <param name="source">The source of the egress artifact; for for interrogation purposed to fill out additional stream option data.</param>
- /// <param name="token">The token to monitor for cancellation requests.</param>
- /// <returns>A task that completes with an <see cref="EgressResult"/> describing the completion of the egress operation.</returns>
- public abstract Task<EgressResult> EgressAsync(
- Func<Stream, CancellationToken, Task> action,
- string fileName,
- string contentType,
- IEndpointInfo source,
- CancellationToken token);
- }
-}
+++ /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;
-
-namespace Microsoft.Diagnostics.Tools.Monitor.Egress
-{
- /// <summary>
- /// Exception that egress providers can throw when an operational error occurs (e.g. failed to write the stream data).
- /// </summary>
- internal class EgressException : MonitoringException
- {
- public EgressException(string message) : base(message) { }
-
- public EgressException(string message, Exception innerException) : base(message, innerException) { }
- }
-}
+++ /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.Configuration;
-using Microsoft.Extensions.Logging;
-using System.Collections.Generic;
-
-namespace Microsoft.Diagnostics.Tools.Monitor.Egress
-{
- /// <summary>
- /// Base class for creating configured egress providers.
- /// </summary>
- internal abstract class EgressFactory
- {
- public EgressFactory(ILogger logger)
- {
- Logger = logger;
- }
-
- /// <summary>
- /// Attempts to create a <see cref="ConfiguredEgressProvider"/> from the provided <paramref name="providerSection"/>
- /// and <paramref name="egressProperties"/>.
- /// </summary>
- /// <param name="providerName">The name of the egress provider.</param>
- /// <param name="providerSection">The configuration section containing the provider options.</param>
- /// <param name="egressProperties">The mapping of egress properties.</param>
- /// <param name="provider">The created <see cref="ConfiguredEgressProvider"/>.</param>
- /// <returns>True if the provider was created; otherwise, false.</returns>
- public abstract bool TryCreate(
- string providerName,
- IConfigurationSection providerSection,
- Dictionary<string, string> egressProperties,
- out ConfiguredEgressProvider provider);
-
- protected bool TryValidateOptions(object value, string providerName)
- {
- Logger.EgressProviderValidatingOptions(providerName);
- EgressProviderValidation validation = new EgressProviderValidation(providerName, Logger);
- return validation.TryValidate(value);
- }
-
- protected ILogger Logger { get; }
- }
-}
+++ /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.ComponentModel.DataAnnotations;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Tools.Monitor.Egress
-{
- /*
- * == Egress Provider Design ==
- * - Each type of egress is implemented as an EgressProvider. The following are the built-in providers:
- * - AzureBlobEgressProvider: Allows egressing stream data to a blob in Azure blob storage.
- * - FileSystemEgressProvider: Allows egressing stream data to the file system.
- * - When constructing an egress provider, the options of the provider must be passed via the constructor.
- * These options are typically use for describing to where stream data is to be egressed.
- * - When invoking an egress provider, an action for acquiring the stream data, a file name, and stream options
- * are required. The acquisition action can either provide the stream or allow the provider to provision the
- * stream, which is passed into the action. The stream options represent additional data about the storage
- * of the stream.
- * - When an egress provider finishes egressing stream data, it will return a value that identifies the location
- * of where the stream data was egressed.
- */
-
- /// <summary>
- /// Base class for all egress implementations.
- /// </summary>
- /// <typeparam name="TProviderOptions">Type of provider options class.</typeparam>
- /// <typeparam name="TStreamOptions">Type of stream options class.</typeparam>
- /// <remarks>
- /// The <typeparamref name="TProviderOptions"/> type is typically used for providing information
- /// about to where a stream is egressed (e.g. directory path, blob storage account, etc).
- /// The <typeparamref name="TStreamOptions"/> type is typically used for providing information
- /// about the storage of the stream itself (e.g. file system permissions, file metadata, etc).
- /// Egress providers should throw <see cref="EgressException"/> when operational error occurs
- /// (e.g. unable to write out stream data). Nearly all other exceptions are treats as programming
- /// errors with the exception of <see cref="OperationCanceledException"/> and <see cref="ValidationException"/>.</remarks>
- internal abstract class EgressProvider<TProviderOptions, TStreamOptions>
- where TProviderOptions : EgressProviderOptions
- {
- protected EgressProvider(TProviderOptions options, ILogger logger = null)
- {
- Logger = logger;
- Options = options;
- }
-
- /// <summary>
- /// Egress a stream via a callback by returning the stream from the callback.
- /// </summary>
- /// <param name="action">Callback that is invoked in order to get the stream to be egressed.</param>
- /// <param name="name">The name of the stream, typically used as a file name.</param>
- /// <param name="streamOptions">Additional information to apply to the storage of the stream data.</param>
- /// <param name="token">The token to monitor for cancellation requests.</param>
- /// <returns>A task that completes with a value of the identifier of the egress result. Typically,
- /// this is a path to access the stream without any information indicating whether any particular
- /// user has access to it (e.g. no file system permissions or SAS tokens).</returns>
- public virtual Task<string> EgressAsync(
- Func<CancellationToken, Task<Stream>> action,
- string name,
- TStreamOptions streamOptions,
- CancellationToken token)
- {
- Func<Stream, CancellationToken, Task> wrappingAction = async (targetStream, token) =>
- {
- using var sourceStream = await action(token);
-
- int copyBufferSize = Options.CopyBufferSize.GetValueOrDefault(0x100000);
-
- Logger?.EgressCopyActionStreamToEgressStream(copyBufferSize);
-
- await sourceStream.CopyToAsync(
- targetStream,
- copyBufferSize,
- token);
- };
-
- return EgressAsync(
- wrappingAction,
- name,
- streamOptions,
- token);
- }
-
- /// <summary>
- /// Egress a stream via a callback by writing to the provided stream.
- /// </summary>
- /// <param name="action">Callback that is invoked in order to write data to the provided stream.</param>
- /// <param name="name">The name of the stream, typically used as a file name.</param>
- /// <param name="streamOptions">Additional information to apply to the storage of the stream data.</param>
- /// <param name="token">The token to monitor for cancellation requests.</param>
- /// <returns>A task that completes with a value of the identifier of the egress result. Typically,
- /// this is a path to access the stream without any information indicating whether any particular
- /// user has access to it (e.g. no file system permissions or SAS tokens).</returns>
- public abstract Task<string> EgressAsync(
- Func<Stream, CancellationToken, Task> action,
- string name,
- TStreamOptions streamOptions,
- CancellationToken token);
-
- protected void ValidateOptions()
- {
- ValidationContext context = new ValidationContext(Options);
- Validator.ValidateObject(Options, context, validateAllProperties: true);
- }
-
- protected ILogger Logger { get; }
-
- protected TProviderOptions Options { get; }
- }
-}
+++ /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.Egress
-{
- /// <summary>
- /// Egress provider options common to all egress providers.
- /// </summary>
- internal abstract class EgressProviderOptions
- {
- /// <summary>
- /// Buffer size used when copying data from an egress callback returning a stream
- /// to the egress callback that is provided a stream to which data is written.
- /// </summary>
- public int? CopyBufferSize { 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.
-
-namespace Microsoft.Diagnostics.Tools.Monitor.Egress
-{
- internal static class EgressProviderTypes
- {
- public const string AzureBlobStorage = nameof(AzureBlobStorage);
-
- public const string FileSystem = nameof(FileSystem);
- }
-}
+++ /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.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.ComponentModel.DataAnnotations;
-
-namespace Microsoft.Diagnostics.Tools.Monitor.Egress
-{
- /// <summary>
- /// Helper class for validating egress options.
- /// </summary>
- internal class EgressProviderValidation
- {
- private readonly ILogger _logger;
- private readonly string _providerName;
-
- public EgressProviderValidation(string providerName, ILogger logger = null)
- {
- _logger = logger;
- _providerName = providerName;
- }
-
- /// <summary>
- /// Validates that the egress options pass the self-described validation.
- /// </summary>
- /// <param name="value">The instance of the options object.</param>
- /// <returns>True if the options object is valid; otherwise, false.</returns>
- /// <remarks>
- /// Validation errors are logged as warnings.
- /// </remarks>
- public bool TryValidate(object value)
- {
- ValidationContext validationContext = new ValidationContext(value);
- ICollection<ValidationResult> results = new Collection<ValidationResult>();
- if (!Validator.TryValidateObject(value, validationContext, results, validateAllProperties: true))
- {
- if (null != _logger)
- {
- foreach (ValidationResult result in results)
- {
- if (ValidationResult.Success != result)
- {
- _logger.EgressProviderOptionsValidationWarning(_providerName, result.ErrorMessage);
- }
- }
- }
- return false;
- }
- 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.
-
-using Microsoft.Diagnostics.Monitoring;
-using Microsoft.Diagnostics.Monitoring.RestServer;
-using Microsoft.Diagnostics.Tools.Monitor.Egress.Configuration;
-using Microsoft.Extensions.Options;
-using System;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Tools.Monitor.Egress
-{
- /// <summary>
- /// Egress service implementation required by the REST server.
- /// </summary>
- internal class EgressService : IEgressService
- {
- private readonly IOptionsMonitor<EgressOptions> _egressOptions;
-
- public EgressService(IOptionsMonitor<EgressOptions> egressOptions)
- {
- _egressOptions = egressOptions;
- }
-
- public Task<EgressResult> EgressAsync(string providerName, Func<CancellationToken, Task<Stream>> action, string fileName, string contentType, IEndpointInfo source, CancellationToken token)
- {
- if (_egressOptions.CurrentValue.Providers.TryGetValue(providerName, out ConfiguredEgressProvider provider))
- {
- return provider.EgressAsync(action, fileName, contentType, source, token);
- }
- throw new EgressException($"Egress provider '{providerName}' does not exist.");
- }
-
- public Task<EgressResult> EgressAsync(string providerName, Func<Stream, CancellationToken, Task> action, string fileName, string contentType, IEndpointInfo source, CancellationToken token)
- {
- if (_egressOptions.CurrentValue.Providers.TryGetValue(providerName, out ConfiguredEgressProvider provider))
- {
- return provider.EgressAsync(action, fileName, contentType, source, token);
- }
- throw new EgressException($"Egress provider '{providerName}' does not exist.");
- }
- }
-}
+++ /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.IO;
-using System.Security;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Tools.Monitor.Egress.FileSystem
-{
- /// <summary>
- /// Egress provider for egressing stream data to the file system.
- /// </summary>
- internal class FileSystemEgressProvider :
- EgressProvider<FileSystemEgressProviderOptions, FileSystemEgressStreamOptions>
- {
- public FileSystemEgressProvider(FileSystemEgressProviderOptions options, ILogger logger = null)
- : base(options, logger)
- {
- }
-
- public override async Task<string> EgressAsync(
- Func<Stream, CancellationToken, Task> action,
- string name,
- FileSystemEgressStreamOptions streamOptions,
- CancellationToken token)
- {
- LogAndValidateOptions(name);
-
- if (!Directory.Exists(Options.DirectoryPath))
- {
- WrapException(() => Directory.CreateDirectory(Options.DirectoryPath));
- }
-
- string targetPath = Path.Combine(Options.DirectoryPath, name);
-
- if (!string.IsNullOrEmpty(Options.IntermediateDirectoryPath))
- {
- if (!Directory.Exists(Options.IntermediateDirectoryPath))
- {
- WrapException(() => Directory.CreateDirectory(Options.IntermediateDirectoryPath));
- }
-
- string intermediateFilePath = null;
- try
- {
- int remainingAttempts = 10;
- bool intermediatePathExists;
- do
- {
- intermediateFilePath = Path.Combine(Options.IntermediateDirectoryPath, Path.GetRandomFileName());
- intermediatePathExists = File.Exists(intermediateFilePath);
- remainingAttempts--;
- }
- while (intermediatePathExists && remainingAttempts > 0);
-
- if (intermediatePathExists)
- {
- throw CreateException($"Unable to create unique intermediate file in '{Options.IntermediateDirectoryPath}' directory.");
- }
-
- await WriteFileAsync(action, intermediateFilePath, token);
-
- WrapException(() => File.Move(intermediateFilePath, targetPath));
- }
- finally
- {
- // Attempt to delete the intermediate file if it exists.
- try
- {
- if (File.Exists(intermediateFilePath))
- {
- File.Delete(intermediateFilePath);
- }
- }
- catch (Exception)
- {
- }
- }
- }
- else
- {
- await WriteFileAsync(action, targetPath, token);
- }
-
- Logger?.EgressProviderSavedStream(EgressProviderTypes.FileSystem, targetPath);
- return targetPath;
- }
-
- private void LogAndValidateOptions(string fileName)
- {
- Logger?.EgressProviderOptionValue(EgressProviderTypes.FileSystem, nameof(Options.DirectoryPath), Options.DirectoryPath);
- Logger?.EgressProviderOptionValue(EgressProviderTypes.FileSystem, nameof(Options.IntermediateDirectoryPath), Options.IntermediateDirectoryPath);
- Logger?.EgressProviderFileName(EgressProviderTypes.FileSystem, fileName);
-
- ValidateOptions();
- }
-
- private async Task WriteFileAsync(Func<Stream, CancellationToken, Task> action, string filePath, CancellationToken token)
- {
- using Stream fileStream = WrapException(
- () => new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None));
-
- Logger?.EgressProviderInvokeStreamAction(EgressProviderTypes.FileSystem);
- await action(fileStream, token);
-
- await fileStream.FlushAsync(token);
- }
-
- private static void WrapException(Action action)
- {
- WrapException(() => { action(); return true; });
- }
-
- private static T WrapException<T>(Func<T> func)
- {
- try
- {
- return func();
- }
- catch (DirectoryNotFoundException ex)
- {
- throw CreateException(ex);
- }
- catch (PathTooLongException ex)
- {
- throw CreateException(ex);
- }
- catch (IOException ex)
- {
- throw CreateException(ex);
- }
- catch (NotSupportedException ex)
- {
- throw CreateException(ex);
- }
- catch (SecurityException ex)
- {
- throw CreateException(ex);
- }
- catch (UnauthorizedAccessException ex)
- {
- throw CreateException(ex);
- }
- }
-
- private static EgressException CreateException(string message)
- {
- return new EgressException(WrapMessage(message));
- }
-
- private static EgressException CreateException(Exception innerException)
- {
- return new EgressException(WrapMessage(innerException.Message), innerException);
- }
-
- private static string WrapMessage(string innerMessage)
- {
- if (!string.IsNullOrEmpty(innerMessage))
- {
- return $"File system egress failed: {innerMessage}";
- }
- else
- {
- return "File system egress failed.";
- }
- }
- }
-}
\ 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.ComponentModel.DataAnnotations;
-
-namespace Microsoft.Diagnostics.Tools.Monitor.Egress.FileSystem
-{
- /// <summary>
- /// Egress provider options for file system egress.
- /// </summary>
- internal class FileSystemEgressProviderOptions :
- EgressProviderOptions
- {
- /// <summary>
- /// The directory path to which the stream data will be egressed.
- /// </summary>
- [Required]
- public string DirectoryPath { get; set; }
-
- /// <summary>
- /// The directory path to which the stream data will initially be written, if specified; the file will then
- /// be moved/renamed to the directory specified in <see cref="FileSystemEgressProviderOptions.DirectoryPath"/>.
- /// </summary>
- public string IntermediateDirectoryPath { 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.
-
-namespace Microsoft.Diagnostics.Tools.Monitor.Egress.FileSystem
-{
- /// <summary>
- /// Egress stream options for file system egress.
- /// </summary>
- internal class FileSystemEgressStreamOptions
- {
- }
-}
+++ /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.RestServer;
-using Microsoft.Diagnostics.Tools.Monitor.Egress.FileSystem;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Tools.Monitor.Egress
-{
- /// <summary>
- /// Creates <see cref="ConfiguredEgressProvider"/> for file system egress.
- /// </summary>
- internal class FileSystemEgressFactory : EgressFactory
- {
- private ILoggerFactory _loggerFactory;
-
- public FileSystemEgressFactory(ILoggerFactory loggerFactory)
- : base(loggerFactory.CreateLogger<FileSystemEgressFactory>())
- {
- _loggerFactory = loggerFactory;
- }
-
- public override bool TryCreate(
- string providerName,
- IConfigurationSection providerSection,
- Dictionary<string, string> egressProperties,
- out ConfiguredEgressProvider provider)
- {
- var options = providerSection.Get<FileSystemEgressProviderOptions>();
-
- if (!TryValidateOptions(options, providerName))
- {
- provider = null;
- return false;
- }
-
- provider = new Provider(options, _loggerFactory);
- return true;
- }
-
- private class Provider : ConfiguredEgressProvider
- {
- private readonly FileSystemEgressProvider _provider;
-
- public Provider(FileSystemEgressProviderOptions options, ILoggerFactory loggerFactory)
- {
- _provider = new FileSystemEgressProvider(options, loggerFactory.CreateLogger<FileSystemEgressProvider>());
- }
-
- public override async Task<EgressResult> EgressAsync(
- Func<CancellationToken, Task<Stream>> action,
- string fileName,
- string contentType,
- IEndpointInfo source,
- CancellationToken token)
- {
- var streamOptions = new FileSystemEgressStreamOptions();
-
- string filepath = await _provider.EgressAsync(action, fileName, streamOptions, token);
-
- return new EgressResult("path", filepath);
- }
-
- public override async Task<EgressResult> EgressAsync(
- Func<Stream, CancellationToken, Task> action,
- string fileName,
- string contentType,
- IEndpointInfo source,
- CancellationToken token)
- {
- var streamOptions = new FileSystemEgressStreamOptions();
-
- string filepath = await _provider.EgressAsync(action, fileName, streamOptions, token);
-
- return new EgressResult("path", filepath);
- }
- }
- }
-}
+++ /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 const string NoAuthMessage = "WARNING: Authentication has been disabled. This can pose a security risk and is not intended for production environments.";
- private const string InsecureAuthMessage = "WARNING: Authentication is enabled over insecure http transport. This can pose a security risk and is not intended for production environments.";
-
- private readonly ILogger<ExperimentalToolLogger> _logger;
-
- public ExperimentalToolLogger(ILogger<ExperimentalToolLogger> logger)
- {
- _logger = logger;
- }
-
- public void LogExperimentMessage()
- {
- _logger.LogWarning(ExperimentMessage);
- }
-
- public void LogNoAuthMessage()
- {
- _logger.LogWarning(NoAuthMessage);
- }
-
- public void LogInsecureAuthMessage()
- {
- _logger.LogWarning(InsecureAuthMessage);
- }
-
- public static void AddLogFilter(ILoggingBuilder builder)
- {
- builder.AddFilter(typeof(ExperimentalToolLogger).FullName, LogLevel.Warning);
- }
- }
-}
+++ /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.NETCore.Client;
-using Microsoft.Extensions.Options;
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Tools.Monitor
-{
- /// <summary>
- /// Wraps an <see cref="IEndpointInfoSource"/> based on the provided configuration
- /// and filters the current process from consideration.
- /// </summary>
- internal class FilteredEndpointInfoSource : IEndpointInfoSourceInternal, IAsyncDisposable
- {
- private readonly DiagnosticPortOptions _portOptions;
- private readonly int? _processIdToFilterOut;
- private readonly Guid? _runtimeInstanceCookieToFilterOut;
- private readonly IEndpointInfoSourceInternal _source;
-
- public FilteredEndpointInfoSource(IOptions<DiagnosticPortOptions> portOptions)
- {
- _portOptions = portOptions.Value;
- switch (_portOptions.ConnectionMode)
- {
- case DiagnosticPortConnectionMode.Connect:
- _source = new ClientEndpointInfoSource();
- break;
- case DiagnosticPortConnectionMode.Listen:
- _source = new ServerEndpointInfoSource(_portOptions.EndpointName);
- break;
- default:
- throw new InvalidOperationException($"Unhandled connection mode: {_portOptions.ConnectionMode}");
- }
-
- // Filter out the current process based on the connection mode.
- if (RuntimeInfo.IsDiagnosticsEnabled)
- {
- int pid = Process.GetCurrentProcess().Id;
-
- // Regardless of connection mode, can use the runtime instance cookie to filter self out.
- try
- {
- var client = new DiagnosticsClient(pid);
- Guid runtimeInstanceCookie = client.GetProcessInfo().RuntimeInstanceCookie;
- if (Guid.Empty != runtimeInstanceCookie)
- {
- _runtimeInstanceCookieToFilterOut = runtimeInstanceCookie;
- }
- }
- catch (Exception)
- {
- }
-
- // If connecting to runtime instances, filter self out. In listening mode, it's likely
- // that multiple processes have the same PID in multi-container scenarios.
- if (DiagnosticPortConnectionMode.Connect == portOptions.Value.ConnectionMode)
- {
- _processIdToFilterOut = pid;
- }
- }
- }
-
- public async Task<IEnumerable<IEndpointInfo>> GetEndpointInfoAsync(CancellationToken token)
- {
- var endpointInfos = await _source.GetEndpointInfoAsync(token);
-
- // Apply process ID filter
- if (_processIdToFilterOut.HasValue)
- {
- endpointInfos = endpointInfos.Where(info => info.ProcessId != _processIdToFilterOut.Value);
- }
-
- // Apply runtime instance cookie filter
- if (_runtimeInstanceCookieToFilterOut.HasValue)
- {
- endpointInfos = endpointInfos.Where(info => info.RuntimeInstanceCookie != _runtimeInstanceCookieToFilterOut.Value);
- }
-
- return endpointInfos;
- }
-
- public async ValueTask DisposeAsync()
- {
- if (_source is IDisposable disposable)
- {
- disposable.Dispose();
- }
- else if (_source is IAsyncDisposable asyncDisposable)
- {
- await asyncDisposable.ConfigureAwait(false).DisposeAsync();
- }
- }
-
- public void Start()
- {
- if (_source is ServerEndpointInfoSource source)
- {
- source.Start(_portOptions.MaxConnections.GetValueOrDefault(ReversedDiagnosticsServer.MaxAllowedConnections));
- }
- }
- }
-}
+++ /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.Threading;
-using System.Threading.Tasks;
-using Microsoft.Diagnostics.Monitoring;
-using Microsoft.Extensions.Hosting;
-
-namespace Microsoft.Diagnostics.Tools.Monitor
-{
- /// <summary>
- /// A hosted service that ensures the <see cref="FilteredEndpointInfoSource"/>
- /// starts monitoring for connectable processes.
- /// </summary>
- internal class FilteredEndpointInfoSourceHostedService : IHostedService
- {
- private readonly FilteredEndpointInfoSource _source;
-
- public FilteredEndpointInfoSourceHostedService(IEndpointInfoSource source)
- {
- _source = (FilteredEndpointInfoSource)source;
- }
-
- public Task StartAsync(CancellationToken cancellationToken)
- {
- _source.Start();
-
- return Task.CompletedTask;
- }
-
- public Task StopAsync(CancellationToken cancellationToken)
- {
- return Task.CompletedTask;
- }
- }
-}
+++ /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.CommandLine;
-using System.CommandLine.IO;
-using System.Security.Cryptography;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Tools.Monitor
-{
- /// <summary>
- /// Used to generate Api Key for authentication. The first output is
- /// part of the Authorization header, and is the Base64 encoded key.
- /// The second output is a hex encoded string of the hash of the secret.
- /// </summary>
- internal sealed class GenerateApiKeyCommandHandler
- {
- public Task<int> GenerateApiKey(CancellationToken token, IConsole console)
- {
- using RandomNumberGenerator rng = RandomNumberGenerator.Create();
- using HashAlgorithm hashAlgorithm = SHA256.Create();
-
- byte[] secret = new byte[32];
- rng.GetBytes(secret);
-
- byte[] hash = hashAlgorithm.ComputeHash(secret);
-
- Console.Out.WriteLine(FormattableString.Invariant($"Authorization: {Monitoring.RestServer.AuthConstants.ApiKeySchema} {Convert.ToBase64String(secret)}"));
- Console.Out.Write("HashedSecret ");
- foreach (byte b in hash)
- {
- console.Out.Write(b.ToString("X2"));
- }
- console.Out.WriteLine();
-
- return Task.FromResult(0);
- }
- }
-}
+++ /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;
-
-namespace Microsoft.Diagnostics.Tools.Monitor
-{
- internal static class LoggingExtensions
- {
- private static readonly Action<ILogger, string, Exception> _egressProviderAdded =
- LoggerMessage.Define<string>(
- eventId: new EventId(1, "EgressProviderAdded"),
- logLevel: LogLevel.Debug,
- formatString: "Provider '{providerName}': Added.");
-
- private static readonly Action<ILogger, string, Exception> _egressProviderInvalidOptions =
- LoggerMessage.Define<string>(
- eventId: new EventId(2, "EgressProviderInvalidOptions"),
- logLevel: LogLevel.Error,
- formatString: "Provider '{providerName}': Invalid options.");
-
- private static readonly Action<ILogger, string, string, Exception> _egressProviderInvalidType =
- LoggerMessage.Define<string, string>(
- eventId: new EventId(3, "EgressProviderInvalidType"),
- logLevel: LogLevel.Error,
- formatString: "Provider '{providerName}': Type '{providerType}' is not supported.");
-
- private static readonly Action<ILogger, string, Exception> _egressProviderValidatingOptions =
- LoggerMessage.Define<string>(
- eventId: new EventId(4, "EgressProviderValidatingOptions"),
- logLevel: LogLevel.Debug,
- formatString: "Provider '{providerName}': Validating options.");
-
- private static readonly Action<ILogger, int, Exception> _egressCopyActionStreamToEgressStream =
- LoggerMessage.Define<int>(
- eventId: new EventId(5, "EgressCopyActionStreamToEgressStream"),
- logLevel: LogLevel.Debug,
- formatString: "Copying action stream to egress stream with buffer size {bufferSize}");
-
- private static readonly Action<ILogger, string, string, Exception> _egressProviderOptionsValidationWarning =
- LoggerMessage.Define<string, string>(
- eventId: new EventId(6, "EgressProviderOptionsValidationWarning"),
- logLevel: LogLevel.Warning,
- formatString: "Provider '{providerName}': {validationWarning}");
-
- private static readonly Action<ILogger, string, string, string, Exception> _egressProviderOptionValue =
- LoggerMessage.Define<string, string, string>(
- eventId: new EventId(7, "EgressProviderOptionValue"),
- logLevel: LogLevel.Debug,
- formatString: "Provider {providerType}: Provider option {optionName} = {optionValue}");
-
- private static readonly Action<ILogger, string, string, string, Exception> _egressStreamOptionValue =
- LoggerMessage.Define<string, string, string>(
- eventId: new EventId(8, "EgressStreamOptionValue"),
- logLevel: LogLevel.Debug,
- formatString: "Provider {providerType}: Stream option {optionName} = {optionValue}");
-
- private static readonly Action<ILogger, string, string, Exception> _egressProviderFileName =
- LoggerMessage.Define<string, string>(
- eventId: new EventId(9, "EgressProviderFileName"),
- logLevel: LogLevel.Debug,
- formatString: "Provider {providerType}: File name = {fileName}");
-
- private static readonly Action<ILogger, string, string, Exception> _egressProviderUnableToFindPropertyKey =
- LoggerMessage.Define<string, string>(
- eventId: new EventId(10, "EgressProvideUnableToFindPropertyKey"),
- logLevel: LogLevel.Warning,
- formatString: "Provider {providerType}: Unable to find '{keyName}' key in egress properties");
-
- private static readonly Action<ILogger, string, Exception> _egressProviderInvokeStreamAction =
- LoggerMessage.Define<string>(
- eventId: new EventId(11, "EgressProviderInvokeStreamAction"),
- logLevel: LogLevel.Debug,
- formatString: "Provider {providerType}: Invoking stream action.");
-
- private static readonly Action<ILogger, string, string, Exception> _egressProviderSavedStream =
- LoggerMessage.Define<string, string>(
- eventId: new EventId(12, "EgressProviderSavedStream"),
- logLevel: LogLevel.Debug,
- formatString: "Provider {providerType}: Saved stream to {path}");
-
- public static void EgressProviderAdded(this ILogger logger, string providerName)
- {
- _egressProviderAdded(logger, providerName, null);
- }
-
- public static void EgressProviderInvalidOptions(this ILogger logger, string providerName)
- {
- _egressProviderInvalidOptions(logger, providerName, null);
- }
-
- public static void EgressProviderInvalidType(this ILogger logger, string providerName, string providerType)
- {
- _egressProviderInvalidType(logger, providerName, providerType, null);
- }
-
- public static void EgressProviderValidatingOptions(this ILogger logger, string providerName)
- {
- _egressProviderValidatingOptions(logger, providerName, null);
- }
-
- public static void EgressCopyActionStreamToEgressStream(this ILogger logger, int bufferSize)
- {
- _egressCopyActionStreamToEgressStream(logger, bufferSize, null);
- }
-
- public static void EgressProviderOptionsValidationWarning(this ILogger logger, string providerName, string validationWarning)
- {
- _egressProviderOptionsValidationWarning(logger, providerName, validationWarning, null);
- }
-
- public static void EgressProviderOptionValue(this ILogger logger, string providerName, string optionName, Uri optionValue)
- {
- logger.EgressProviderOptionValue(providerName, optionName, optionValue?.ToString());
- }
-
- public static void EgressProviderOptionValue(this ILogger logger, string providerName, string optionName, string optionValue, bool redact = false)
- {
- if (redact)
- {
- optionValue = Redact(optionValue);
- }
-
- _egressProviderOptionValue(logger, providerName, optionName, optionValue, null);
- }
-
- public static void EgressStreamOptionValue(this ILogger logger, string providerName, string optionName, string optionValue, bool redact = false)
- {
- if (redact)
- {
- optionValue = Redact(optionValue);
- }
-
- _egressStreamOptionValue(logger, providerName, optionName, optionValue, null);
- }
-
- public static void EgressProviderFileName(this ILogger logger, string providerName, string fileName)
- {
- _egressProviderFileName(logger, providerName, fileName, null);
- }
-
- public static void EgressProviderUnableToFindPropertyKey(this ILogger logger, string providerName, string keyName)
- {
- _egressProviderUnableToFindPropertyKey(logger, providerName, keyName, null);
- }
-
- public static void EgressProviderInvokeStreamAction(this ILogger logger, string providerName)
- {
- _egressProviderInvokeStreamAction(logger, providerName, null);
- }
-
- public static void EgressProviderSavedStream(this ILogger logger, string providerName, string path)
- {
- _egressProviderSavedStream(logger, providerName, path, null);
- }
-
- private static string Redact(string value)
- {
- return string.IsNullOrEmpty(value) ? value : "<REDACTED>";
- }
- }
-}
+++ /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.Extensions.DependencyInjection;
-using Microsoft.Tools.Common;
-using System;
-using System.CommandLine;
-using System.CommandLine.Builder;
-using System.CommandLine.Invocation;
-using System.CommandLine.Parsing;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Tools.Monitor
-{
- [Flags]
- internal enum SinkType
- {
- None = 0,
- Console = 1,
- All = 0xff
- }
-
- class Program
- {
- private static Command GenerateApiKeyCommand() =>
- new Command(
- name: "generatekey",
- description: "Generate api key and hash for authentication")
- {
- CommandHandler.Create<CancellationToken, IConsole>(new GenerateApiKeyCommandHandler().GenerateApiKey)
- };
-
- 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, string[], string[], bool, string, bool>(new DiagnosticsMonitorCommandHandler().Start),
- Urls(), MetricUrls(), ProvideMetrics(), DiagnosticPort(), NoAuth()
- };
-
- private static Option Urls() =>
- new Option(
- aliases: new[] { "-u", "--urls" },
- description: "Bindings for the REST api.")
- {
- Argument = new Argument<string[]>(name: "urls", getDefaultValue: () => new[] { "https://localhost:52323" })
- };
-
- private static Option MetricUrls() =>
- new Option(
- aliases: new[] { "--metricUrls" },
- description: "Bindings for metrics")
- {
- Argument = new Argument<string[]>(name: "metricUrls", getDefaultValue: () => new[] { GetDefaultMetricsEndpoint() })
- };
-
- private static Option ProvideMetrics() =>
- new Option(
- aliases: new[] { "-m", "--metrics" },
- description: "Enable publishing of metrics")
- {
- Argument = new Argument<bool>(name: "metrics", getDefaultValue: () => true)
- };
-
- private static Option DiagnosticPort() =>
- new Option(
- alias: "--diagnostic-port",
- description: "The fully qualified path and filename of the diagnostic port to which runtime instances can connect.")
- {
- Argument = new Argument<string>(name: "diagnosticPort")
- };
-
- private static Option NoAuth() =>
- new Option(
- alias: "--no-auth",
- description: "Turn off authentication."
- )
- {
- Argument = new Argument<bool>(name: "noAuth", getDefaultValue: () => false)
- };
-
- private static string GetDefaultMetricsEndpoint()
- {
- string endpoint = "http://localhost:52325";
- if (RuntimeInfo.IsInDockerContainer)
- {
- //Necessary for prometheus scraping
- endpoint = "http://*:52325";
- }
- return endpoint;
- }
-
- public static Task<int> Main(string[] args)
- {
- var parser = new CommandLineBuilder()
- .AddCommand(CollectCommand())
- .AddCommand(GenerateApiKeyCommand())
- .UseDefaults()
- .Build();
- return parser.InvokeAsync(args);
- }
- }
-}
+++ /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.RestServer;
-using Microsoft.Diagnostics.Tools.Monitor.Egress;
-using Microsoft.Diagnostics.Tools.Monitor.Egress.Configuration;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Options;
-
-namespace Microsoft.Diagnostics.Tools.Monitor
-{
- internal static class ServiceCollectionExtensions
- {
- public static IServiceCollection ConfigureMetrics(this IServiceCollection services, IConfiguration configuration)
- {
- return ConfigureOptions<MetricsOptions>(services, configuration, MetricsOptions.ConfigurationKey);
- }
-
- public static IServiceCollection ConfigureApiKeyConfiguration(this IServiceCollection services, IConfiguration configuration)
- {
- return ConfigureOptions<ApiAuthenticationOptions>(services, configuration, ApiAuthenticationOptions.ConfigurationKey);
- }
-
- private static IServiceCollection ConfigureOptions<T>(IServiceCollection services, IConfiguration configuration, string key) where T : class
- {
- return services.Configure<T>(configuration.GetSection(key));
- }
-
- public static IServiceCollection ConfigureEgress(this IServiceCollection services, IConfiguration configuration)
- {
- // Register change token for EgressOptions binding
- services.AddSingleton<IOptionsChangeTokenSource<EgressOptions>>(new ConfigurationChangeTokenSource<EgressOptions>(configuration.GetEgressSection()));
-
- // Configure EgressOptions to bind to the Egress configuration key.
- // The options are manually created due to how the Providers property
- // holds concrete implementations that are based on the 'type' property
- // for each provider entry.
- services.AddSingleton<IConfigureOptions<EgressOptions>, EgressConfigureOptions>();
-
- // Register IEgressService implementation that provides egressing
- // of artifacts for the REST server.
- services.AddSingleton<IEgressService, EgressService>();
-
- return services;
- }
- }
-}
+++ /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.AspNetCore.Builder;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.ResponseCompression;
-using Microsoft.AspNetCore.Server.Kestrel.Core;
-using Microsoft.Diagnostics.Monitoring.RestServer;
-using Microsoft.Diagnostics.Monitoring.RestServer.Controllers;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
-using System;
-using System.Collections.Generic;
-using System.IO.Compression;
-using System.Linq;
-
-namespace Microsoft.Diagnostics.Tools.Monitor
-{
- internal class Startup
- {
- public Startup(IConfiguration configuration)
- {
- Configuration = configuration;
- }
-
- public IConfiguration Configuration { get; }
-
- // This method gets called by the runtime. Use this method to add services to the container.
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddMvc(options =>
- {
- options.Filters.Add(new ProducesAttribute("application/json"));
-
- options.EnableEndpointRouting = false;
- })
- .SetCompatibilityVersion(CompatibilityVersion.Latest)
- .AddApplicationPart(typeof(DiagController).Assembly);
-
- 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<BrotliCompressionProviderOptions>(options =>
- {
- options.Level = CompressionLevel.Optimal;
- });
-
- services.AddResponseCompression(configureOptions =>
- {
- configureOptions.Providers.Add<BrotliCompressionProvider>();
- configureOptions.MimeTypes = new List<string> { "application/octet-stream" };
- });
-
- // This is needed to allow the StreamingLogger to synchronously write to the output stream.
- // Eventually should switch StreamingLoggger to something that allows for async operations.
- services.Configure<KestrelServerOptions>(options =>
- {
- options.AllowSynchronousIO = true;
- });
-
- var metricsOptions = new MetricsOptions();
- Configuration.Bind(MetricsOptions.ConfigurationKey, metricsOptions);
- if (metricsOptions.Enabled)
- {
- services.AddSingleton<MetricsStoreService>();
- services.AddHostedService<MetricsService>();
- }
- }
-
- // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
- public void Configure(
- IApplicationBuilder app,
- IWebHostEnvironment env,
- ExperimentalToolLogger logger,
- IAuthOptions options)
- {
- logger.LogExperimentMessage();
- if (options.KeyAuthenticationMode == KeyAuthenticationMode.NoAuth)
- {
- logger.LogNoAuthMessage();
- }
- else
- {
- //Auth is enabled and we are binding on http. Make sure we log a warning.
-
- string hostingUrl = Configuration.GetValue<string>(WebHostDefaults.ServerUrlsKey);
- string[] urls = ConfigurationHelper.SplitValue(hostingUrl);
- foreach(BindingAddress address in urls.Select(BindingAddress.Parse))
- {
- if (string.Equals(Uri.UriSchemeHttp, address.Scheme, StringComparison.OrdinalIgnoreCase))
- {
- logger.LogInsecureAuthMessage();
- break;
- }
- }
- }
-
- if (env.IsDevelopment())
- {
- app.UseDeveloperExceptionPage();
- }
- else
- {
- app.UseHsts();
- }
-
- app.UseAuthentication();
- app.UseAuthorization();
-
- CorsConfiguration corsConfiguration = new CorsConfiguration();
- Configuration.Bind(nameof(CorsConfiguration), corsConfiguration);
- if (!string.IsNullOrEmpty(corsConfiguration.AllowedOrigins))
- {
- app.UseCors(builder => builder.WithOrigins(corsConfiguration.GetOrigins()).AllowAnyHeader().AllowAnyMethod());
- }
-
- app.UseResponseCompression();
- app.UseMvc();
- }
- }
-}
+++ /dev/null
-{
- "Logging": {
- "LogLevel": {
- "Default": "Debug",
- "System": "Information",
- "Microsoft": "Information"
- }
- }
-}
+++ /dev/null
-{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft": "Warning",
- "Microsoft.Diagnostics": "Information",
- "Microsoft.Hosting.Lifetime": "Information"
- },
- "Console": {
- "FormatterName": "simple",
- "FormatterOptions": {
- "IncludeScopes": true,
- "TimestampFormat": "HH:mm:ss "
- }
- },
- "EventLog": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft": "Warning",
- "Microsoft.Diagnostics": "Information",
- "Microsoft.Hosting.Lifetime": "Information"
- }
- }
- },
- "AllowedHosts": "*"
-}
+++ /dev/null
-<Project Sdk="Microsoft.NET.Sdk">
-
- <PropertyGroup>
- <TargetFramework>netcoreapp3.1</TargetFramework>
- <RuntimeIdentifiers>linux-x64;linux-musl-x64;win-x64</RuntimeIdentifiers>
- <PackAsToolShimRuntimeIdentifiers>linux-x64;linux-musl-x64;win-x64</PackAsToolShimRuntimeIdentifiers>
- <RootNamespace>Microsoft.Diagnostics.Tools.Monitor</RootNamespace>
- <ToolCommandName>dotnet-monitor</ToolCommandName>
- <Description>.NET Core Diagnostic Monitoring Tool</Description>
- <PackageTags>Diagnostic</PackageTags>
- <IsShipping>false</IsShipping>
- <PackageReleaseNotes>$(Description)</PackageReleaseNotes>
- <RollForward>Major</RollForward>
- <!-- This forces the creation of a checksum file and uploads it to blob storage
- using this name as part of the blob relative path. -->
- <BlobGroupPrefix>monitor</BlobGroupPrefix>
- <!-- Version information -->
- <VersionPrefix>5.0.0</VersionPrefix>
- <PreReleaseVersionLabel>preview</PreReleaseVersionLabel>
- <PreReleaseVersionIteration>4</PreReleaseVersionIteration>
- </PropertyGroup>
-
- <ItemGroup>
- <None Remove="appsettings.Development.json" />
- <None Remove="appsettings.json" />
- </ItemGroup>
-
- <ItemGroup>
- <PackageReference Include="Azure.Storage.Blobs" Version="$(AzureStorageBlobsVersion)" />
- <PackageReference Include="Microsoft.Extensions.Configuration.KeyPerFile" Version="$(MicrosoftExtensionsConfigurationKeyPerFileVersion)" />
- <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsoleVersion)" />
- <PackageReference Include="System.CommandLine" Version="$(SystemCommandLineVersion)" />
- <PackageReference Include="Microsoft.AspNetCore.Authentication.Negotiate" Version="$(MicrosoftAspNetCoreAuthenticationNegotiateVersion)" />
- <PackageReference Include="Microsoft.Diagnostics.Monitoring" Version="$(MicrosoftDiagnosticsMonitoringVersion)" />
- </ItemGroup>
-
- <ItemGroup>
- <Compile Include="..\Common\CommandExtensions.cs" Link="CommandExtensions.cs" />
- </ItemGroup>
-
- <ItemGroup>
- <Content Include="appsettings.Development.json">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="appsettings.json">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- </ItemGroup>
-
- <ItemGroup>
- <ProjectReference Include="..\..\Microsoft.Diagnostics.Monitoring.RestServer\Microsoft.Diagnostics.Monitoring.RestServer.csproj" />
- </ItemGroup>
-
-</Project>