diff --git a/.azure/pipelines/ci.yml b/.azure/pipelines/ci.yml
index e356ab3d42..af04da8a81 100644
--- a/.azure/pipelines/ci.yml
+++ b/.azure/pipelines/ci.yml
@@ -71,6 +71,7 @@ variables:
# The following extra properties are not set when testing. Use with final build.[cmd,sh] of asset-producing jobs.
- name: _PublishArgs
value: /p:Publish=true
+ /p:GenerateChecksums=true
/p:DotNetPublishBlobFeedKey=$(dotnetfeed-storage-access-key-1)
/p:DotNetPublishBlobFeedUrl=https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json
/p:DotNetPublishToBlobFeed=$(_DotNetPublishToBlobFeed)
@@ -92,27 +93,28 @@ stages:
displayName: Build
jobs:
# Code check
- - template: jobs/default-build.yml
- parameters:
- jobName: Code_check
- jobDisplayName: Code check
- agentOs: Windows
- steps:
- - ${{ if ne(variables['System.TeamProject'], 'public') }}:
- - task: PowerShell@2
- displayName: Setup Private Feeds Credentials
- inputs:
- filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.ps1
- arguments: -ConfigFile $(Build.SourcesDirectory)/NuGet.config -Password $Env:Token
- env:
- Token: $(dn-bot-dnceng-artifact-feeds-rw)
- - powershell: ./eng/scripts/CodeCheck.ps1 -ci $(_InternalRuntimeDownloadArgs)
- displayName: Run eng/scripts/CodeCheck.ps1
- artifacts:
- - name: Code_Check_Logs
- path: artifacts/log/
- publishOnError: true
- includeForks: true
+ - ${{ if or(eq(variables['System.TeamProject'], 'public'), in(variables['Build.Reason'], 'PullRequest')) }}:
+ - template: jobs/default-build.yml
+ parameters:
+ jobName: Code_check
+ jobDisplayName: Code check
+ agentOs: Windows
+ steps:
+ - ${{ if ne(variables['System.TeamProject'], 'public') }}:
+ - task: PowerShell@2
+ displayName: Setup Private Feeds Credentials
+ inputs:
+ filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.ps1
+ arguments: -ConfigFile $(Build.SourcesDirectory)/NuGet.config -Password $Env:Token
+ env:
+ Token: $(dn-bot-dnceng-artifact-feeds-rw)
+ - powershell: ./eng/scripts/CodeCheck.ps1 -ci $(_InternalRuntimeDownloadArgs)
+ displayName: Run eng/scripts/CodeCheck.ps1
+ artifacts:
+ - name: Code_Check_Logs
+ path: artifacts/log/
+ publishOnError: true
+ includeForks: true
# Build Windows (x64/x86)
- template: jobs/default-build.yml
@@ -594,7 +596,7 @@ stages:
parameters:
condition: ne(variables['SkipTests'], 'true')
jobName: MacOS_Test
- jobDisplayName: "Test: macOS 10.13"
+ jobDisplayName: "Test: macOS 10.14"
agentOs: macOS
isTestingJob: true
buildArgs: --all --test "/p:RunTemplateTests=false /p:SkipHelixReadyTests=true" $(_InternalRuntimeDownloadArgs)
diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md
new file mode 100644
index 0000000000..775f221c98
--- /dev/null
+++ b/CODE-OF-CONDUCT.md
@@ -0,0 +1,6 @@
+# Code of Conduct
+
+This project has adopted the code of conduct defined by the Contributor Covenant
+to clarify expected behavior in our community.
+
+For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 238c74186d..a125150797 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -57,4 +57,4 @@ Your pull request will now go through extensive checks by the subject matter exp
## Code of conduct
-This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
+See [CODE-OF-CONDUCT.md](./CODE-OF-CONDUCT.md)
diff --git a/Directory.Build.props b/Directory.Build.props
index 684bab1184..6e658ac596 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -189,7 +189,6 @@
.tar.gz
.zip
- .sha512
diff --git a/README.md b/README.md
index 4c2e975c42..327c3e7208 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ Follow the [Getting Started](https://docs.microsoft.com/aspnet/core/getting-star
Also check out the [.NET Homepage](https://www.microsoft.com/net) for released versions of .NET, getting started guides, and learning resources.
-See the [Issue Management Policies](https://github.com/dotnet/aspnetcore/blob/anurse/issue-policies/docs/IssueManagementPolicies.md) document for more information on how we handle incoming issues.
+See the [Issue Management Policies](https://github.com/dotnet/aspnetcore/blob/master/docs/IssueManagementPolicies.md) document for more information on how we handle incoming issues.
## How to Engage, Contribute, and Give Feedback
@@ -37,4 +37,4 @@ These are some other repos for related projects:
## Code of conduct
-This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
+See [CODE-OF-CONDUCT](./CODE-OF-CONDUCT.md)
diff --git a/eng/AfterSigning.targets b/eng/AfterSigning.targets
new file mode 100644
index 0000000000..4bbb0dcf03
--- /dev/null
+++ b/eng/AfterSigning.targets
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+ $(ArtifactsDir.Substring(0, $([MSBuild]::Subtract($(ArtifactsDir.Length), 1))))
+
+ $(ArtifactsDir)\installers\
+
+
+
+
+
+
+
+
+
+
+
+
+ %(FullPath).sha512
+
+
+
+
+
+
+
+
diff --git a/eng/Build.props b/eng/Build.props
index 6f1b3f1908..ef89408b47 100644
--- a/eng/Build.props
+++ b/eng/Build.props
@@ -152,6 +152,7 @@
$(RepoRoot)src\SiteExtensions\LoggingAggregate\test\**\*.csproj;
$(RepoRoot)src\Shared\**\*.*proj;
$(RepoRoot)src\Tools\**\*.*proj;
+ $(RepoRoot)src\Logging.AzureAppServices\**\src\*.csproj;
$(RepoRoot)src\Middleware\**\*.csproj;
$(RepoRoot)src\Razor\**\*.*proj;
$(RepoRoot)src\Mvc\**\*.*proj;
@@ -191,6 +192,7 @@
$(RepoRoot)src\Security\**\src\*.csproj;
$(RepoRoot)src\SiteExtensions\**\src\*.csproj;
$(RepoRoot)src\Tools\**\src\*.csproj;
+ $(RepoRoot)src\Logging.AzureAppServices\**\src\*.csproj;
$(RepoRoot)src\Middleware\**\src\*.csproj;
$(RepoRoot)src\Razor\**\src\*.csproj;
$(RepoRoot)src\Mvc\**\src\*.csproj;
diff --git a/eng/Dependencies.props b/eng/Dependencies.props
index 9fd65b327f..acbfb7e1c9 100644
--- a/eng/Dependencies.props
+++ b/eng/Dependencies.props
@@ -29,7 +29,6 @@ and are generated based on the last package release.
-
@@ -119,13 +118,11 @@ and are generated based on the last package release.
-
-
-
+
diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props
index dda9b06ce8..a7d42c3782 100644
--- a/eng/ProjectReferences.props
+++ b/eng/ProjectReferences.props
@@ -32,6 +32,7 @@
+
diff --git a/eng/Publishing.props b/eng/Publishing.props
index ab7456c178..2c13cb29bf 100644
--- a/eng/Publishing.props
+++ b/eng/Publishing.props
@@ -1,7 +1,7 @@
-
+
- $(ArtifactsDir.Substring(0, $([MSBuild]::Subtract($(ArtifactsDir.Length), 1))))
+ $(ArtifactsDir.Substring(0, $([MSBuild]::Subtract($(ArtifactsDir.Length), 1))))
$(PublishDependsOnTargets);_PublishInstallersAndChecksums
@@ -50,12 +50,10 @@
-
true
diff --git a/eng/SharedFramework.External.props b/eng/SharedFramework.External.props
index 9812152edd..59987b0a2c 100644
--- a/eng/SharedFramework.External.props
+++ b/eng/SharedFramework.External.props
@@ -18,9 +18,8 @@
$(SystemWindowsExtensionsPackageVersion.Split('.')[0]).$(SystemWindowsExtensionsPackageVersion.Split('.')[1]).0
-
-
+
@@ -54,8 +53,6 @@
-
-
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index 9fde48f524..39a62cd7ee 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -29,185 +29,169 @@
https://github.com/dotnet/aspnetcore-tooling
4ec71cb57e45db101bbd4ffcf64dafa1711de0af
-
+
https://github.com/dotnet/efcore
- 0f28f7168a1a6b1f34ccc4546eb6d5d667fee011
+ b0636ed8050797d0a9c16da8b98c2eea7d7e1f16
-
+
https://github.com/dotnet/efcore
- 0f28f7168a1a6b1f34ccc4546eb6d5d667fee011
+ b0636ed8050797d0a9c16da8b98c2eea7d7e1f16
-
+
https://github.com/dotnet/efcore
- 0f28f7168a1a6b1f34ccc4546eb6d5d667fee011
+ b0636ed8050797d0a9c16da8b98c2eea7d7e1f16
-
+
https://github.com/dotnet/efcore
- 0f28f7168a1a6b1f34ccc4546eb6d5d667fee011
+ b0636ed8050797d0a9c16da8b98c2eea7d7e1f16
-
+
https://github.com/dotnet/efcore
- 0f28f7168a1a6b1f34ccc4546eb6d5d667fee011
+ b0636ed8050797d0a9c16da8b98c2eea7d7e1f16
-
+
https://github.com/dotnet/efcore
- 0f28f7168a1a6b1f34ccc4546eb6d5d667fee011
+ b0636ed8050797d0a9c16da8b98c2eea7d7e1f16
-
+
https://github.com/dotnet/efcore
- 0f28f7168a1a6b1f34ccc4546eb6d5d667fee011
+ b0636ed8050797d0a9c16da8b98c2eea7d7e1f16
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
-
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
-
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
-
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
-
-
- https://github.com/dotnet/extensions
- 03c40031d618f923aa88da125cb078aabde9ebb1
+
+ https://github.com/dotnet/runtime
+ e1fa5d7648d46f067e265211fc2c695d409fe788
https://github.com/dotnet/runtime
diff --git a/eng/Versions.props b/eng/Versions.props
index 7090324ea7..b1190c5562 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -65,14 +65,47 @@
5.0.0-beta.20180.5
3.6.0-3.20201.6
-
+
5.0.0-preview.4-runtime.20201.1
5.0.0-preview.4.20201.1
5.0.0-preview.4.20201.1
5.0.0-preview.4.20201.1
-
5.0.0-preview.4.20201.1
5.0.0-preview.4.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4-runtime.20201.1
5.0.0-preview.4.20201.1
5.0.0-preview.4.20201.1
5.0.0-preview.4.20201.1
@@ -95,53 +128,14 @@
5.0.0-preview.4.20201.1
3.2.0-preview1.20067.1
-
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.2
- 5.0.0-preview.4.20201.1
- 5.0.0-preview.4.20201.1
- 5.0.0-preview.4.20201.1
- 5.0.0-preview.4.20201.1
- 5.0.0-preview.4.20201.1
- 5.0.0-preview.4.20201.1
- 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20203.1
+ 5.0.0-preview.4.20203.1
+ 5.0.0-preview.4.20203.1
+ 5.0.0-preview.4.20203.1
+ 5.0.0-preview.4.20203.1
+ 5.0.0-preview.4.20203.1
+ 5.0.0-preview.4.20203.1
5.0.0-preview.4.20201.4
5.0.0-preview.4.20201.4
diff --git a/eng/helix/content/RunTests/Program.cs b/eng/helix/content/RunTests/Program.cs
index 1535d6663e..f9d3ec2752 100644
--- a/eng/helix/content/RunTests/Program.cs
+++ b/eng/helix/content/RunTests/Program.cs
@@ -14,222 +14,41 @@ namespace RunTests
{
static async Task Main(string[] args)
{
- var command = new RootCommand()
+ try
{
- new Option(
- aliases: new string[] { "--target", "-t" },
- description: "The test dll to run")
- { Argument = new Argument(), Required = true },
+ var runner = new TestRunner(RunTestsOptions.Parse(args));
- new Option(
- aliases: new string[] { "--sdk" },
- description: "The version of the sdk being used")
- { Argument = new Argument(), Required = true },
-
- new Option(
- aliases: new string[] { "--runtime" },
- description: "The version of the runtime being used")
- { Argument = new Argument(), Required = true },
-
- new Option(
- aliases: new string[] { "--queue" },
- description: "The name of the Helix queue being run on")
- { Argument = new Argument(), Required = true },
-
- new Option(
- aliases: new string[] { "--arch" },
- description: "The architecture being run on")
- { Argument = new Argument(), Required = true },
-
- new Option(
- aliases: new string[] { "--quarantined" },
- description: "Whether quarantined tests should run or not")
- { Argument = new Argument(), Required = true },
-
- new Option(
- aliases: new string[] { "--ef" },
- description: "The version of the EF tool to use")
- { Argument = new Argument(), Required = true },
- };
-
- var parseResult = command.Parse(args);
- var target = parseResult.ValueForOption("--target");
- var sdkVersion = parseResult.ValueForOption("--sdk");
- var runtimeVersion = parseResult.ValueForOption("--runtime");
- var helixQueue = parseResult.ValueForOption("--queue");
- var architecture = parseResult.ValueForOption("--arch");
- var quarantined = parseResult.ValueForOption("--quarantined");
- var efVersion = parseResult.ValueForOption("--ef");
-
- var HELIX_WORKITEM_ROOT = Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT");
-
- var path = Environment.GetEnvironmentVariable("PATH");
- var dotnetRoot = Environment.GetEnvironmentVariable("DOTNET_ROOT");
-
- // Rename default.NuGet.config to NuGet.config if there is not a custom one from the project
- // We use a local NuGet.config file to avoid polluting global machine state and avoid relying on global machine state
- if (!File.Exists("NuGet.config"))
- {
- File.Copy("default.NuGet.config", "NuGet.config");
- }
-
- var environmentVariables = new Dictionary();
- environmentVariables.Add("PATH", path);
- environmentVariables.Add("DOTNET_ROOT", dotnetRoot);
- environmentVariables.Add("helix", helixQueue);
-
- Console.WriteLine($"Current Directory: {HELIX_WORKITEM_ROOT}");
- var helixDir = HELIX_WORKITEM_ROOT;
- Console.WriteLine($"Setting HELIX_DIR: {helixDir}");
- environmentVariables.Add("HELIX_DIR", helixDir);
- environmentVariables.Add("NUGET_FALLBACK_PACKAGES", helixDir);
- var nugetRestore = Path.Combine(helixDir, "nugetRestore");
- Console.WriteLine($"Creating nuget restore directory: {nugetRestore}");
- environmentVariables.Add("NUGET_RESTORE", nugetRestore);
- var dotnetEFFullPath = Path.Combine(nugetRestore, $"dotnet-ef/{efVersion}/tools/netcoreapp3.1/any/dotnet-ef.exe");
- Console.WriteLine($"Set DotNetEfFullPath: {dotnetEFFullPath}");
- environmentVariables.Add("DotNetEfFullPath", dotnetEFFullPath);
-
- Console.WriteLine("Checking for Microsoft.AspNetCore.App/");
- if (Directory.Exists("Microsoft.AspNetCore.App"))
- {
- Console.WriteLine($"Found Microsoft.AspNetCore.App/, copying to {dotnetRoot}/shared/Microsoft.AspNetCore.App/{runtimeVersion}");
- foreach (var file in Directory.EnumerateFiles("Microsoft.AspNetCore.App", "*.*", SearchOption.AllDirectories))
+ var keepGoing = runner.SetupEnvironment();
+ if (keepGoing)
{
- File.Copy(file, $"{dotnetRoot}/shared/Microsoft.AspNetCore.App/{runtimeVersion}", overwrite: true);
+ keepGoing = await runner.InstallAspNetAppIfNeededAsync();
}
- Console.WriteLine($"Adding current directory to nuget sources: {HELIX_WORKITEM_ROOT}");
+ runner.DisplayContents();
- await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet",
- $"nuget add source {HELIX_WORKITEM_ROOT} --configfile NuGet.config",
- environmentVariables: environmentVariables);
+ if (keepGoing)
+ {
+ if (!await runner.CheckTestDiscoveryAsync())
+ {
+ Console.WriteLine("RunTest stopping due to test discovery failure.");
+ Environment.Exit(1);
+ return;
+ }
- await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet",
- "nuget add source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json --configfile NuGet.config",
- environmentVariables: environmentVariables);
+ var exitCode = await runner.RunTestsAsync();
+ runner.UploadResults();
+ Console.WriteLine($"Completed Helix job with exit code '{exitCode}'");
+ Environment.Exit(exitCode);
+ }
- // Write nuget sources to console, useful for debugging purposes
- await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet",
- "nuget list source",
- environmentVariables: environmentVariables,
- outputDataReceived: Console.WriteLine,
- errorDataReceived: Console.WriteLine);
-
- await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet",
- $"tool install dotnet-ef --global --version {efVersion}",
- environmentVariables: environmentVariables);
-
- // ';' is the path separator on Windows, and ':' on Unix
- path += RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ";" : ":";
- path += $"{Environment.GetEnvironmentVariable("DOTNET_CLI_HOME")}/.dotnet/tools";
- environmentVariables["PATH"] = path;
- }
-
- Directory.CreateDirectory(nugetRestore);
-
- // Rename default.runner.json to xunit.runner.json if there is not a custom one from the project
- if (!File.Exists("xunit.runner.json"))
- {
- File.Copy("default.runner.json", "xunit.runner.json");
- }
-
- Console.WriteLine();
- Console.WriteLine("Displaying directory contents:");
- foreach (var file in Directory.EnumerateFiles("./"))
- {
- Console.WriteLine(Path.GetFileName(file));
- }
- foreach (var file in Directory.EnumerateDirectories("./"))
- {
- Console.WriteLine(Path.GetFileName(file));
- }
- Console.WriteLine();
-
- // Run test discovery so we know if there are tests to run
- var discoveryResult = await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet",
- $"vstest {target} -lt",
- environmentVariables: environmentVariables);
-
- if (discoveryResult.StandardOutput.Contains("Exception thrown"))
- {
- Console.WriteLine("Exception thrown during test discovery.");
- Console.WriteLine(discoveryResult.StandardOutput);
+ Console.WriteLine("Tests were not run due to previous failures. Exit code=1");
Environment.Exit(1);
- return;
}
-
- var exitCode = 0;
- var commonTestArgs = $"vstest {target} --logger:xunit --logger:\"console;verbosity=normal\" --blame";
- if (quarantined)
+ catch (Exception e)
{
- Console.WriteLine("Running quarantined tests.");
-
- // Filter syntax: https://github.com/Microsoft/vstest-docs/blob/master/docs/filter.md
- var result = await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet",
- commonTestArgs + " --TestCaseFilter:\"Quarantined=true\"",
- environmentVariables: environmentVariables,
- outputDataReceived: Console.WriteLine,
- errorDataReceived: Console.WriteLine,
- throwOnError: false);
-
- if (result.ExitCode != 0)
- {
- Console.WriteLine($"Failure in quarantined tests. Exit code: {result.ExitCode}.");
- }
+ Console.WriteLine($"RunTests uncaught exception: {e.ToString()}");
+ Environment.Exit(1);
}
- else
- {
- Console.WriteLine("Running non-quarantined tests.");
-
- // Filter syntax: https://github.com/Microsoft/vstest-docs/blob/master/docs/filter.md
- var result = await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet",
- commonTestArgs + " --TestCaseFilter:\"Quarantined!=true\"",
- environmentVariables: environmentVariables,
- outputDataReceived: Console.WriteLine,
- errorDataReceived: Console.Error.WriteLine,
- throwOnError: false);
-
- if (result.ExitCode != 0)
- {
- Console.WriteLine($"Failure in non-quarantined tests. Exit code: {result.ExitCode}.");
- exitCode = result.ExitCode;
- }
- }
-
- // 'testResults.xml' is the file Helix looks for when processing test results
- Console.WriteLine();
- if (File.Exists("TestResults/TestResults.xml"))
- {
- Console.WriteLine("Copying TestResults/TestResults.xml to ./testResults.xml");
- File.Copy("TestResults/TestResults.xml", "testResults.xml");
- }
- else
- {
- Console.WriteLine("No test results found.");
- }
-
- var HELIX_WORKITEM_UPLOAD_ROOT = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT");
- Console.WriteLine($"Copying artifacts/log/ to {HELIX_WORKITEM_UPLOAD_ROOT}/");
- if (Directory.Exists("artifacts/log"))
- {
- foreach (var file in Directory.EnumerateFiles("artifacts/log", "*.log", SearchOption.AllDirectories))
- {
- // Combine the directory name + log name for the copied log file name to avoid overwriting duplicate test names in different test projects
- var logName = $"{Path.GetFileName(Path.GetDirectoryName(file))}_{Path.GetFileName(file)}";
- Console.WriteLine($"Copying: {file} to {Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, logName)}");
- // Need to copy to HELIX_WORKITEM_UPLOAD_ROOT and HELIX_WORKITEM_UPLOAD_ROOT/../ in order for Azure Devops attachments to link properly and for Helix to store the logs
- File.Copy(file, Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, logName));
- File.Copy(file, Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, "..", logName));
- }
- }
- else
- {
- Console.WriteLine("No logs found in artifacts/log");
- }
-
- Console.WriteLine("Completed Helix job.");
- Environment.Exit(exitCode);
}
}
}
diff --git a/eng/helix/content/RunTests/RunTestsOptions.cs b/eng/helix/content/RunTests/RunTestsOptions.cs
new file mode 100644
index 0000000000..fcfdc84e42
--- /dev/null
+++ b/eng/helix/content/RunTests/RunTestsOptions.cs
@@ -0,0 +1,81 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.CommandLine;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+
+namespace RunTests
+{
+ public class RunTestsOptions
+ {
+ public static RunTestsOptions Parse(string[] args)
+ {
+ var command = new RootCommand()
+ {
+ new Option(
+ aliases: new string[] { "--target", "-t" },
+ description: "The test dll to run")
+ { Argument = new Argument(), Required = true },
+
+ new Option(
+ aliases: new string[] { "--sdk" },
+ description: "The version of the sdk being used")
+ { Argument = new Argument(), Required = true },
+
+ new Option(
+ aliases: new string[] { "--runtime" },
+ description: "The version of the runtime being used")
+ { Argument = new Argument(), Required = true },
+
+ new Option(
+ aliases: new string[] { "--queue" },
+ description: "The name of the Helix queue being run on")
+ { Argument = new Argument(), Required = true },
+
+ new Option(
+ aliases: new string[] { "--arch" },
+ description: "The architecture being run on")
+ { Argument = new Argument(), Required = true },
+
+ new Option(
+ aliases: new string[] { "--quarantined" },
+ description: "Whether quarantined tests should run or not")
+ { Argument = new Argument(), Required = true },
+
+ new Option(
+ aliases: new string[] { "--ef" },
+ description: "The version of the EF tool to use")
+ { Argument = new Argument(), Required = true },
+ };
+
+ var parseResult = command.Parse(args);
+ var options = new RunTestsOptions();
+ options.Target = parseResult.ValueForOption("--target");
+ options.SdkVersion = parseResult.ValueForOption("--sdk");
+ options.RuntimeVersion = parseResult.ValueForOption("--runtime");
+ options.HelixQueue = parseResult.ValueForOption("--queue");
+ options.Architecture = parseResult.ValueForOption("--arch");
+ options.Quarantined = parseResult.ValueForOption("--quarantined");
+ options.EfVersion = parseResult.ValueForOption("--ef");
+ options.HELIX_WORKITEM_ROOT = Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT");
+ options.Path = Environment.GetEnvironmentVariable("PATH");
+ options.DotnetRoot = Environment.GetEnvironmentVariable("DOTNET_ROOT");
+ return options;
+ }
+
+ public string Target { get; set;}
+ public string SdkVersion { get; set;}
+ public string RuntimeVersion { get; set;}
+ public string HelixQueue { get; set;}
+ public string Architecture { get; set;}
+ public bool Quarantined { get; set;}
+ public string EfVersion { get; set;}
+ public string HELIX_WORKITEM_ROOT { get; set;}
+ public string DotnetRoot { get; set; }
+ public string Path { get; set; }
+ }
+}
diff --git a/eng/helix/content/RunTests/TestRunner.cs b/eng/helix/content/RunTests/TestRunner.cs
new file mode 100644
index 0000000000..850d0b629d
--- /dev/null
+++ b/eng/helix/content/RunTests/TestRunner.cs
@@ -0,0 +1,251 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.CommandLine;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+
+namespace RunTests
+{
+ public class TestRunner
+ {
+ public TestRunner(RunTestsOptions options)
+ {
+ Options = options;
+ EnvironmentVariables = new Dictionary();
+ }
+
+ public RunTestsOptions Options { get; set; }
+ public Dictionary EnvironmentVariables { get; set; }
+
+ public bool SetupEnvironment()
+ {
+ try
+ {
+ // Rename default.NuGet.config to NuGet.config if there is not a custom one from the project
+ // We use a local NuGet.config file to avoid polluting global machine state and avoid relying on global machine state
+ if (!File.Exists("NuGet.config"))
+ {
+ File.Copy("default.NuGet.config", "NuGet.config");
+ }
+
+ EnvironmentVariables.Add("PATH", Options.Path);
+ EnvironmentVariables.Add("DOTNET_ROOT", Options.DotnetRoot);
+ EnvironmentVariables.Add("helix", Options.HelixQueue);
+
+ Console.WriteLine($"Current Directory: {Options.HELIX_WORKITEM_ROOT}");
+ var helixDir = Options.HELIX_WORKITEM_ROOT;
+ Console.WriteLine($"Setting HELIX_DIR: {helixDir}");
+ EnvironmentVariables.Add("HELIX_DIR", helixDir);
+ EnvironmentVariables.Add("NUGET_FALLBACK_PACKAGES", helixDir);
+ var nugetRestore = Path.Combine(helixDir, "nugetRestore");
+ EnvironmentVariables.Add("NUGET_RESTORE", nugetRestore);
+ var dotnetEFFullPath = Path.Combine(nugetRestore, $"dotnet-ef/{Options.EfVersion}/tools/netcoreapp3.1/any/dotnet-ef.exe");
+ Console.WriteLine($"Set DotNetEfFullPath: {dotnetEFFullPath}");
+ EnvironmentVariables.Add("DotNetEfFullPath", dotnetEFFullPath);
+
+ Console.WriteLine($"Creating nuget restore directory: {nugetRestore}");
+ Directory.CreateDirectory(nugetRestore);
+
+ // Rename default.runner.json to xunit.runner.json if there is not a custom one from the project
+ if (!File.Exists("xunit.runner.json"))
+ {
+ File.Copy("default.runner.json", "xunit.runner.json");
+ }
+
+ return true;
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine($"Exception in SetupEnvironment: {e.ToString()}");
+ return false;
+ }
+ }
+
+ public void DisplayContents()
+ {
+ try
+ {
+ Console.WriteLine();
+ Console.WriteLine("Displaying directory contents:");
+ foreach (var file in Directory.EnumerateFiles("./"))
+ {
+ Console.WriteLine(Path.GetFileName(file));
+ }
+ foreach (var file in Directory.EnumerateDirectories("./"))
+ {
+ Console.WriteLine(Path.GetFileName(file));
+ }
+ Console.WriteLine();
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine($"Exception in DisplayInitialState: {e.ToString()}");
+ }
+ }
+
+ public async Task InstallAspNetAppIfNeededAsync()
+ {
+ try
+ {
+ Console.WriteLine("Checking for Microsoft.AspNetCore.App/");
+ if (Directory.Exists("Microsoft.AspNetCore.App"))
+ {
+ var appRuntimePath = $"{Options.DotnetRoot}/shared/Microsoft.AspNetCore.App/{Options.RuntimeVersion}";
+ Console.WriteLine($"Found Microsoft.AspNetCore.App/, copying to {appRuntimePath}");
+ foreach (var file in Directory.EnumerateFiles("Microsoft.AspNetCore.App", "*.*", SearchOption.AllDirectories))
+ {
+ File.Copy(file, Path.Combine(appRuntimePath, file), overwrite: true);
+ }
+
+ Console.WriteLine($"Adding current directory to nuget sources: {Options.HELIX_WORKITEM_ROOT}");
+
+ await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet",
+ $"nuget add source {Options.HELIX_WORKITEM_ROOT} --configfile NuGet.config",
+ environmentVariables: EnvironmentVariables);
+
+ await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet",
+ "nuget add source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json --configfile NuGet.config",
+ environmentVariables: EnvironmentVariables);
+
+ // Write nuget sources to console, useful for debugging purposes
+ await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet",
+ "nuget list source",
+ environmentVariables: EnvironmentVariables,
+ outputDataReceived: Console.WriteLine,
+ errorDataReceived: Console.WriteLine);
+
+ await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet",
+ $"tool install dotnet-ef --global --version {Options.EfVersion}",
+ environmentVariables: EnvironmentVariables);
+
+ // ';' is the path separator on Windows, and ':' on Unix
+ Options.Path += RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ";" : ":";
+ Options.Path += $"{Environment.GetEnvironmentVariable("DOTNET_CLI_HOME")}/.dotnet/tools";
+ EnvironmentVariables["PATH"] = Options.Path;
+ }
+ else
+ {
+ Console.WriteLine($"No app runtime found, skipping...");
+ }
+ return true;
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine($"Exception in InstallAspNetAppIfNeeded: {e.ToString()}");
+ return false;
+ }
+ }
+
+ public async Task CheckTestDiscoveryAsync()
+ {
+ try
+ {
+ // Run test discovery so we know if there are tests to run
+ var discoveryResult = await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet",
+ $"vstest {Options.Target} -lt",
+ environmentVariables: EnvironmentVariables);
+
+ if (discoveryResult.StandardOutput.Contains("Exception thrown"))
+ {
+ Console.WriteLine("Exception thrown during test discovery.");
+ Console.WriteLine(discoveryResult.StandardOutput);
+ return false;
+ }
+ return true;
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine($"Exception in CheckTestDiscovery: {e.ToString()}");
+ return false;
+ }
+ }
+
+ public async Task RunTestsAsync()
+ {
+ var exitCode = 0;
+ try
+ {
+ var commonTestArgs = $"vstest {Options.Target} --logger:xunit --logger:\"console;verbosity=normal\" --blame";
+ if (Options.Quarantined)
+ {
+ Console.WriteLine("Running quarantined tests.");
+
+ // Filter syntax: https://github.com/Microsoft/vstest-docs/blob/master/docs/filter.md
+ var result = await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet",
+ commonTestArgs + " --TestCaseFilter:\"Quarantined=true\"",
+ environmentVariables: EnvironmentVariables,
+ outputDataReceived: Console.WriteLine,
+ errorDataReceived: Console.WriteLine,
+ throwOnError: false);
+
+ if (result.ExitCode != 0)
+ {
+ Console.WriteLine($"Failure in quarantined tests. Exit code: {result.ExitCode}.");
+ }
+ }
+ else
+ {
+ Console.WriteLine("Running non-quarantined tests.");
+
+ // Filter syntax: https://github.com/Microsoft/vstest-docs/blob/master/docs/filter.md
+ var result = await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet",
+ commonTestArgs + " --TestCaseFilter:\"Quarantined!=true\"",
+ environmentVariables: EnvironmentVariables,
+ outputDataReceived: Console.WriteLine,
+ errorDataReceived: Console.Error.WriteLine,
+ throwOnError: false);
+
+ if (result.ExitCode != 0)
+ {
+ Console.WriteLine($"Failure in non-quarantined tests. Exit code: {result.ExitCode}.");
+ exitCode = result.ExitCode;
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine($"Exception in RunTests: {e.ToString()}");
+ exitCode = 1;
+ }
+ return exitCode;
+ }
+
+ public void UploadResults()
+ {
+ // 'testResults.xml' is the file Helix looks for when processing test results
+ Console.WriteLine("Trying to upload results...");
+ if (File.Exists("TestResults/TestResults.xml"))
+ {
+ Console.WriteLine("Copying TestResults/TestResults.xml to ./testResults.xml");
+ File.Copy("TestResults/TestResults.xml", "testResults.xml");
+ }
+ else
+ {
+ Console.WriteLine("No test results found.");
+ }
+
+ var HELIX_WORKITEM_UPLOAD_ROOT = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT");
+ Console.WriteLine($"Copying artifacts/log/ to {HELIX_WORKITEM_UPLOAD_ROOT}/");
+ if (Directory.Exists("artifacts/log"))
+ {
+ foreach (var file in Directory.EnumerateFiles("artifacts/log", "*.log", SearchOption.AllDirectories))
+ {
+ // Combine the directory name + log name for the copied log file name to avoid overwriting duplicate test names in different test projects
+ var logName = $"{Path.GetFileName(Path.GetDirectoryName(file))}_{Path.GetFileName(file)}";
+ Console.WriteLine($"Copying: {file} to {Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, logName)}");
+ // Need to copy to HELIX_WORKITEM_UPLOAD_ROOT and HELIX_WORKITEM_UPLOAD_ROOT/../ in order for Azure Devops attachments to link properly and for Helix to store the logs
+ File.Copy(file, Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, logName));
+ File.Copy(file, Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, "..", logName));
+ }
+ }
+ else
+ {
+ Console.WriteLine("No logs found in artifacts/log");
+ }
+ }
+ }
+}
diff --git a/eng/helix/content/runtests.cmd b/eng/helix/content/runtests.cmd
index d71ff1b6ce..1078def35b 100644
--- a/eng/helix/content/runtests.cmd
+++ b/eng/helix/content/runtests.cmd
@@ -21,10 +21,12 @@ echo "Installing Runtime"
powershell.exe -NoProfile -ExecutionPolicy unrestricted -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; &([scriptblock]::Create((Invoke-WebRequest -useb 'https://dot.net/v1/dotnet-install.ps1'))) -Architecture %$arch% -Runtime dotnet -Version %$runtimeVersion% -InstallDir %DOTNET_ROOT%"
set exit_code=0
+echo "Restore for RunTests..."
dotnet restore RunTests\RunTests.csproj --source https://api.nuget.org/v3/index.json --ignore-failed-sources
+echo "Running tests..."
dotnet run --project RunTests\RunTests.csproj -- --target %1 --sdk %2 --runtime %3 --queue %4 --arch %5 --quarantined %6 --ef %7
if errorlevel 1 (
set exit_code=1
)
-
+echo "Finished running tests: exit_code=%exit_code%"
exit /b %exit_code%
diff --git a/eng/helix/content/runtests.sh b/eng/helix/content/runtests.sh
index a8665ab51c..2ce725910c 100755
--- a/eng/helix/content/runtests.sh
+++ b/eng/helix/content/runtests.sh
@@ -86,7 +86,10 @@ fi
sync
exit_code=0
+echo "Restore for RunTests..."
$DOTNET_ROOT/dotnet restore RunTests/RunTests.csproj --source https://api.nuget.org/v3/index.json --ignore-failed-sources
+echo "Running tests..."
$DOTNET_ROOT/dotnet run --project RunTests/RunTests.csproj -- --target $1 --sdk $2 --runtime $3 --queue $4 --arch $5 --quarantined $6 --ef $7
-
-exit $?
+exit_code = $?
+echo "Finished tests...exit_code=$exit_code"
+exit $exit_code
diff --git a/eng/scripts/KillProcesses.ps1 b/eng/scripts/KillProcesses.ps1
index 153234abd9..43d7c5ed92 100644
--- a/eng/scripts/KillProcesses.ps1
+++ b/eng/scripts/KillProcesses.ps1
@@ -38,6 +38,15 @@ function _killSeleniumTrackedProcesses() {
}
}
+function _listProcesses($processName) {
+ $processes = Get-WmiObject win32_process -Filter "name like '%$processName'" -ErrorAction SilentlyContinue;
+ if ($processes) {
+ Write-Host "These processes will be killed..."
+ $processes | select commandline | Out-String -Width 800
+ }
+}
+
+_listProcesses dotnet
_kill dotnet.exe
_kill testhost.exe
_kill iisexpress.exe
diff --git a/eng/scripts/KillProcesses.sh b/eng/scripts/KillProcesses.sh
index f52511739b..e491dc1693 100755
--- a/eng/scripts/KillProcesses.sh
+++ b/eng/scripts/KillProcesses.sh
@@ -1,4 +1,12 @@
#!/usr/bin/env bash
+# list processes that would be killed so they appear in the log
+p=$(pgrep dotnet)
+if [ $? -eq 0 ]
+then
+ echo "These processes will be killed..."
+ ps -p $p
+fi
+
pkill dotnet || true
exit 0
diff --git a/eng/targets/ResolveReferences.targets b/eng/targets/ResolveReferences.targets
index 37a17b7140..8cc5c717ec 100644
--- a/eng/targets/ResolveReferences.targets
+++ b/eng/targets/ResolveReferences.targets
@@ -216,16 +216,16 @@
Condition=" '$(CompileUsingReferenceAssemblies)' != false AND '$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)' ">true
-
+ IncludeAssets="Compile"
+ PrivateAssets="All"
+ GeneratePathProperty="true" />
diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs
index 77e0b06faa..5ecddb3ed6 100644
--- a/src/Components/Components/test/RendererTest.cs
+++ b/src/Components/Components/test/RendererTest.cs
@@ -2810,8 +2810,7 @@ namespace Microsoft.AspNetCore.Components.Test
Assert.Equal(10, component.OnAfterRenderCallCount);
}
- [ConditionalFact]
- [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/7487")]
+ [Fact]
public async Task CanTriggerEventHandlerDisposedInEarlierPendingBatchAsync()
{
// This represents the scenario where the same event handler is being triggered
diff --git a/src/Components/test/testassets/TestServer/Components.TestServer.csproj b/src/Components/test/testassets/TestServer/Components.TestServer.csproj
index 4eebc1f24b..d51a812210 100644
--- a/src/Components/test/testassets/TestServer/Components.TestServer.csproj
+++ b/src/Components/test/testassets/TestServer/Components.TestServer.csproj
@@ -14,8 +14,8 @@
+
-
diff --git a/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj b/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj
index f695e4d3ee..4f60448f35 100644
--- a/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj
+++ b/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj
@@ -53,6 +53,10 @@ This package is an internal implementation of the .NET Core SDK and is not meant
$(AspNetCoreMajorVersion).$(AspNetCoreMinorVersion).0
$(ReferencePackSharedFxVersion)-$(VersionSuffix)
+
+
+ $(PkgMicrosoft_Extensions_Internal_Transport)\ref\$(TargetFramework)\
+
@@ -109,14 +113,14 @@ This package is an internal implementation of the .NET Core SDK and is not meant
BeforeTargets="_GetPackageFiles"
DependsOnTargets="ResolveReferences;FindReferenceAssembliesForReferences">
- <_AvailableExtensionsRefAssemblies Include="$(MicrosoftInternalExtensionsRefsPath)\*.dll" />
+ <_AvailableExtensionsRefAssemblies Include="$(RuntimeExtensionsReferenceDirectory)*.dll" />
-
+
@@ -131,7 +135,7 @@ This package is an internal implementation of the .NET Core SDK and is not meant
-
+
<_ReferencedExtensionsRefAssembliesToExclude Include="@(_ReferencedExtensionsRefAssemblies)" Exclude="@(_DuplicatedExtensionsRefAssemblies)" />
@@ -147,10 +151,10 @@ This package is an internal implementation of the .NET Core SDK and is not meant
@(ReferencePathWithRefAssemblies->WithMetadataValue('ReferenceGrouping', 'Microsoft.NETCore.App'));" />
+ Include="@(_SelectedExtensionsRefAssemblies->'$(RuntimeExtensionsReferenceDirectory)%(FileName)%(Extension)')" />
-
+
@@ -165,7 +169,7 @@ This package is an internal implementation of the .NET Core SDK and is not meant
Outputs="$(TargetDir)$(PackageConflictManifestFileName)">
- <_AspNetCoreAppPackageOverrides Include="@(AspNetCoreReferenceAssemblyPath->'%(NuGetPackageId)|%(NuGetPackageVersion)')" Condition="!Exists('$(MicrosoftInternalExtensionsRefsPath)%(AspNetCoreReferenceAssemblyPath.NuGetPackageId).dll') AND '%(AspNetCoreReferenceAssemblyPath.NuGetPackageId)' != 'Microsoft.NETCore.App' AND '%(AspNetCoreReferenceAssemblyPath.NuGetPackageId)' != 'Microsoft.Internal.Extensions.Refs' AND '%(AspNetCoreReferenceAssemblyPath.NuGetSourceType)' == 'Package' " />
+ <_AspNetCoreAppPackageOverrides Include="@(AspNetCoreReferenceAssemblyPath->'%(NuGetPackageId)|%(NuGetPackageVersion)')" Condition="!Exists('$(RuntimeExtensionsReferenceDirectory)%(AspNetCoreReferenceAssemblyPath.NuGetPackageId).dll') AND '%(AspNetCoreReferenceAssemblyPath.NuGetPackageId)' != 'Microsoft.NETCore.App' AND '%(AspNetCoreReferenceAssemblyPath.NuGetPackageId)' != 'Microsoft.Extensions.Internal.Transport' AND '%(AspNetCoreReferenceAssemblyPath.NuGetSourceType)' == 'Package' " />
<_AspNetCoreAppPackageOverrides Include="@(_SelectedExtensionsRefAssemblies->'%(FileName)|$(MicrosoftInternalExtensionsRefsPackageOverrideVersion)')" />
diff --git a/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj b/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj
index 84b237a6ca..7397e945bf 100644
--- a/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj
+++ b/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj
@@ -156,12 +156,6 @@ This package is an internal implementation of the .NET Core SDK and is not meant
$(InstallersOutputPath)$(RedistArchiveOutputFileName)
-
-
- $(RedistArchiveOutputPath)$(ChecksumExtension)
-
-
-
diff --git a/src/Framework/test/Microsoft.AspNetCore.App.UnitTests.csproj b/src/Framework/test/Microsoft.AspNetCore.App.UnitTests.csproj
index 9abf901de4..8d9b26e7d0 100644
--- a/src/Framework/test/Microsoft.AspNetCore.App.UnitTests.csproj
+++ b/src/Framework/test/Microsoft.AspNetCore.App.UnitTests.csproj
@@ -36,6 +36,10 @@
<_Parameter1>TargetingPackLayoutRoot
<_Parameter2>$(TargetingPackLayoutRoot)
+
+ <_Parameter1>IsTargetingPackBuilding
+ <_Parameter2>$(IsTargetingPackBuilding)
+
<_Parameter1>VerifyAncmBinary
<_Parameter2>$(VerifyAncmBinary)
diff --git a/src/Framework/test/TargetingPackTests.cs b/src/Framework/test/TargetingPackTests.cs
index 82b43cb831..54da276513 100644
--- a/src/Framework/test/TargetingPackTests.cs
+++ b/src/Framework/test/TargetingPackTests.cs
@@ -20,17 +20,24 @@ namespace Microsoft.AspNetCore
private readonly string _expectedRid;
private readonly string _targetingPackRoot;
private readonly ITestOutputHelper _output;
+ private readonly bool _isTargetingPackBuilding;
public TargetingPackTests(ITestOutputHelper output)
{
_output = output;
_expectedRid = TestData.GetSharedFxRuntimeIdentifier();
_targetingPackRoot = Path.Combine(TestData.GetTestDataValue("TargetingPackLayoutRoot"), "packs", "Microsoft.AspNetCore.App.Ref", TestData.GetTestDataValue("TargetingPackVersion"));
+ _isTargetingPackBuilding = bool.Parse(TestData.GetTestDataValue("IsTargetingPackBuilding"));
}
[Fact]
public void AssembliesAreReferenceAssemblies()
{
+ if (!_isTargetingPackBuilding)
+ {
+ return;
+ }
+
IEnumerable dlls = Directory.GetFiles(_targetingPackRoot, "*.dll", SearchOption.AllDirectories);
Assert.NotEmpty(dlls);
@@ -58,6 +65,11 @@ namespace Microsoft.AspNetCore
[Fact]
public void PlatformManifestListsAllFiles()
{
+ if (!_isTargetingPackBuilding)
+ {
+ return;
+ }
+
var platformManifestPath = Path.Combine(_targetingPackRoot, "data", "PlatformManifest.txt");
var expectedAssemblies = TestData.GetSharedFxDependencies()
.Split(';', StringSplitOptions.RemoveEmptyEntries)
diff --git a/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.csproj b/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.csproj
index d6b79361e7..4f83bbda83 100644
--- a/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.csproj
+++ b/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.csproj
@@ -9,6 +9,7 @@
+
@@ -16,6 +17,7 @@
+
diff --git a/src/HealthChecks/HealthChecks/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj b/src/HealthChecks/HealthChecks/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj
index a737e01d45..44e1d8d95f 100644
--- a/src/HealthChecks/HealthChecks/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj
+++ b/src/HealthChecks/HealthChecks/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj
@@ -28,6 +28,7 @@ Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder
+
diff --git a/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj b/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj
index ba625f4332..939dfcf376 100644
--- a/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj
+++ b/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj
@@ -23,7 +23,6 @@
-
diff --git a/src/Hosting/test/FunctionalTests/ShutdownTests.cs b/src/Hosting/test/FunctionalTests/ShutdownTests.cs
index 96ba91bcaa..58abe91b33 100644
--- a/src/Hosting/test/FunctionalTests/ShutdownTests.cs
+++ b/src/Hosting/test/FunctionalTests/ShutdownTests.cs
@@ -32,6 +32,7 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests
await ExecuteShutdownTest(nameof(ShutdownTestRun), "Run");
}
+ [QuarantinedTest]
[ConditionalFact]
[OSSkipCondition(OperatingSystems.Windows)]
[OSSkipCondition(OperatingSystems.MacOSX)]
@@ -133,7 +134,7 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests
private static void WaitForExitOrKill(Process process)
{
- process.WaitForExit(1000);
+ process.WaitForExit(5 * 1000);
if (!process.HasExited)
{
process.Kill();
diff --git a/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/ConfigureSigningCredentialsTests.cs b/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/ConfigureSigningCredentialsTests.cs
index 099fdf73e5..dda0233d77 100644
--- a/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/ConfigureSigningCredentialsTests.cs
+++ b/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/ConfigureSigningCredentialsTests.cs
@@ -23,7 +23,6 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer
X509KeyStorageFlags.DefaultKeySet);
[ConditionalFact]
- [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720")]
[FrameworkSkipCondition(RuntimeFrameworks.CLR)]
public void Configure_AddsDevelopmentKeyFromConfiguration()
{
@@ -64,7 +63,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer
}
[ConditionalFact]
- [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720")]
+ [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
public void Configure_LoadsPfxCertificateCredentialFromConfiguration()
{
// Arrange
@@ -94,7 +93,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer
}
[ConditionalFact]
- [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720")]
+ [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
public void Configure_LoadsCertificateStoreCertificateCredentialFromConfiguration()
{
try
diff --git a/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/SigningKeysLoaderTests.cs b/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/SigningKeysLoaderTests.cs
index c5d543dfe9..893be873ab 100644
--- a/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/SigningKeysLoaderTests.cs
+++ b/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/SigningKeysLoaderTests.cs
@@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration
}
[ConditionalFact]
- [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720")]
+ [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
public static void LoadFromStoreCert_SkipsCertificatesNotYetValid()
{
try
@@ -82,7 +82,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration
}
[ConditionalFact]
- [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720")]
+ [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
public static void LoadFromStoreCert_PrefersCertificatesCloserToExpirationDate()
{
try
@@ -105,7 +105,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration
}
[ConditionalFact]
- [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720")]
+ [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
public static void LoadFromStoreCert_SkipsExpiredCertificates()
{
try
diff --git a/src/Identity/Extensions.Core/ref/Microsoft.Extensions.Identity.Core.csproj b/src/Identity/Extensions.Core/ref/Microsoft.Extensions.Identity.Core.csproj
index 1ddecfe522..846f73feee 100644
--- a/src/Identity/Extensions.Core/ref/Microsoft.Extensions.Identity.Core.csproj
+++ b/src/Identity/Extensions.Core/ref/Microsoft.Extensions.Identity.Core.csproj
@@ -11,6 +11,7 @@
+
diff --git a/src/Identity/Extensions.Core/src/Microsoft.Extensions.Identity.Core.csproj b/src/Identity/Extensions.Core/src/Microsoft.Extensions.Identity.Core.csproj
index 7555dda1fe..5f6681e261 100644
--- a/src/Identity/Extensions.Core/src/Microsoft.Extensions.Identity.Core.csproj
+++ b/src/Identity/Extensions.Core/src/Microsoft.Extensions.Identity.Core.csproj
@@ -13,7 +13,8 @@
-
+
+
diff --git a/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/IIS-Common/README.md b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/IIS-Common/README.md
index 77fc8404c4..511b1088fc 100644
--- a/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/IIS-Common/README.md
+++ b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/IIS-Common/README.md
@@ -2,17 +2,3 @@ Microsoft IIS Common
--------------------------------
The repository contains common resources shared by IIS Out-Of-Band (OOB) products.
-
-### Contributing
-
-This project welcomes contributions and suggestions. Most contributions require you to agree to a
-Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
-the rights to use your contribution. For details, visit https://cla.microsoft.com.
-
-When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
-a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
-provided by the bot. You will only need to do this once across all repos using our CLA.
-
-This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
-For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
-contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
diff --git a/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/README.md b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/README.md
index 22e594aa6c..5a4135df83 100644
--- a/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/README.md
+++ b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/README.md
@@ -2,17 +2,3 @@ Microsoft IIS Setup
--------------------------------
The repository contains setup resources shared by IIS Out-Of-Band (OOB) products.
-
-### Contributing
-
-This project welcomes contributions and suggestions. Most contributions require you to agree to a
-Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
-the rights to use your contribution. For details, visit https://cla.microsoft.com.
-
-When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
-a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
-provided by the bot. You will only need to do this once across all repos using our CLA.
-
-This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
-For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
-contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
diff --git a/src/Installers/Windows/Wix.targets b/src/Installers/Windows/Wix.targets
index 2e4b8dca03..de6636d51d 100644
--- a/src/Installers/Windows/Wix.targets
+++ b/src/Installers/Windows/Wix.targets
@@ -64,7 +64,7 @@
+ BeforeTargets="Build">
<_cabs Include="$(TargetDir)**/*.cab" />
@@ -72,10 +72,4 @@
-
-
- $(InstallersOutputPath)$(PackageFileName)$(ChecksumExtension)
-
-
-
diff --git a/src/Logging.AzureAppServices/Directory.Build.props b/src/Logging.AzureAppServices/Directory.Build.props
new file mode 100644
index 0000000000..68f87d4f24
--- /dev/null
+++ b/src/Logging.AzureAppServices/Directory.Build.props
@@ -0,0 +1,8 @@
+
+
+
+
+
+ true
+
+
diff --git a/src/Logging.AzureAppServices/src/AzureAppServicesLoggerFactoryExtensions.cs b/src/Logging.AzureAppServices/src/AzureAppServicesLoggerFactoryExtensions.cs
new file mode 100644
index 0000000000..9b680e9138
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/AzureAppServicesLoggerFactoryExtensions.cs
@@ -0,0 +1,98 @@
+// 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.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging.AzureAppServices;
+using Microsoft.Extensions.Logging.Configuration;
+using Microsoft.Extensions.Options;
+using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor;
+
+namespace Microsoft.Extensions.Logging
+{
+ ///
+ /// Extension methods for adding Azure diagnostics logger.
+ ///
+ public static class AzureAppServicesLoggerFactoryExtensions
+ {
+ ///
+ /// Adds an Azure Web Apps diagnostics logger.
+ ///
+ /// The extension method argument
+ public static ILoggingBuilder AddAzureWebAppDiagnostics(this ILoggingBuilder builder)
+ {
+ var context = WebAppContext.Default;
+
+ // Only add the provider if we're in Azure WebApp. That cannot change once the apps started
+ return AddAzureWebAppDiagnostics(builder, context);
+ }
+
+ internal static ILoggingBuilder AddAzureWebAppDiagnostics(this ILoggingBuilder builder, IWebAppContext context)
+ {
+ if (!context.IsRunningInAzureWebApp)
+ {
+ return builder;
+ }
+
+ builder.AddConfiguration();
+
+ var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(context);
+ var services = builder.Services;
+
+ var addedFileLogger = TryAddEnumerable(services, Singleton());
+ var addedBlobLogger = TryAddEnumerable(services, Singleton());
+
+ if (addedFileLogger || addedBlobLogger)
+ {
+ services.AddSingleton(context);
+ services.AddSingleton>(
+ new ConfigurationChangeTokenSource(config));
+ }
+
+ if (addedFileLogger)
+ {
+ services.AddSingleton>(CreateFileFilterConfigureOptions(config));
+ services.AddSingleton>(new FileLoggerConfigureOptions(config, context));
+ services.AddSingleton>(
+ new ConfigurationChangeTokenSource(config));
+ LoggerProviderOptions.RegisterProviderOptions(builder.Services);
+ }
+
+ if (addedBlobLogger)
+ {
+ services.AddSingleton>(CreateBlobFilterConfigureOptions(config));
+ services.AddSingleton>(new BlobLoggerConfigureOptions(config, context));
+ services.AddSingleton>(
+ new ConfigurationChangeTokenSource(config));
+ LoggerProviderOptions.RegisterProviderOptions(builder.Services);
+ }
+
+ return builder;
+ }
+
+ private static bool TryAddEnumerable(IServiceCollection collection, ServiceDescriptor descriptor)
+ {
+ var beforeCount = collection.Count;
+ collection.TryAddEnumerable(descriptor);
+ return beforeCount != collection.Count;
+ }
+
+ private static ConfigurationBasedLevelSwitcher CreateBlobFilterConfigureOptions(IConfiguration config)
+ {
+ return new ConfigurationBasedLevelSwitcher(
+ configuration: config,
+ provider: typeof(BlobLoggerProvider),
+ levelKey: "AzureBlobTraceLevel");
+ }
+
+ private static ConfigurationBasedLevelSwitcher CreateFileFilterConfigureOptions(IConfiguration config)
+ {
+ return new ConfigurationBasedLevelSwitcher(
+ configuration: config,
+ provider: typeof(FileLoggerProvider),
+ levelKey: "AzureDriveTraceLevel");
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/AzureBlobLoggerOptions.cs b/src/Logging.AzureAppServices/src/AzureBlobLoggerOptions.cs
new file mode 100644
index 0000000000..1e1285b358
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/AzureBlobLoggerOptions.cs
@@ -0,0 +1,39 @@
+// 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;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// Options for Azure diagnostics blob logging.
+ ///
+ public class AzureBlobLoggerOptions: BatchingLoggerOptions
+ {
+ private string _blobName = "applicationLog.txt";
+
+ ///
+ /// Gets or sets the last section of log blob name.
+ /// Defaults to "applicationLog.txt".
+ ///
+ public string BlobName
+ {
+ get { return _blobName; }
+ set
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ throw new ArgumentException(nameof(value), $"{nameof(BlobName)} must be non-empty string.");
+ }
+ _blobName = value;
+ }
+ }
+
+ internal string ContainerUrl { get; set; }
+
+ internal string ApplicationName { get; set; }
+
+ internal string ApplicationInstanceId { get; set; }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/AzureFileLoggerOptions.cs b/src/Logging.AzureAppServices/src/AzureFileLoggerOptions.cs
new file mode 100644
index 0000000000..af8b5a112e
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/AzureFileLoggerOptions.cs
@@ -0,0 +1,73 @@
+// 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;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// Options for Azure diagnostics file logging.
+ ///
+ public class AzureFileLoggerOptions: BatchingLoggerOptions
+ {
+ private int? _fileSizeLimit = 10 * 1024 * 1024;
+ private int? _retainedFileCountLimit = 2;
+ private string _fileName = "diagnostics-";
+
+ ///
+ /// Gets or sets a strictly positive value representing the maximum log size in bytes or null for no limit.
+ /// Once the log is full, no more messages will be appended.
+ /// Defaults to 10MB.
+ ///
+ public int? FileSizeLimit
+ {
+ get { return _fileSizeLimit; }
+ set
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FileSizeLimit)} must be positive.");
+ }
+ _fileSizeLimit = value;
+ }
+ }
+
+ ///
+ /// Gets or sets a strictly positive value representing the maximum retained file count or null for no limit.
+ /// Defaults to 2.
+ ///
+ public int? RetainedFileCountLimit
+ {
+ get { return _retainedFileCountLimit; }
+ set
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(RetainedFileCountLimit)} must be positive.");
+ }
+ _retainedFileCountLimit = value;
+ }
+ }
+
+ ///
+ /// Gets or sets a string representing the prefix of the file name used to store the logging information.
+ /// The current date, in the format YYYYMMDD will be added after the given value.
+ /// Defaults to diagnostics-.
+ ///
+ public string FileName
+ {
+ get { return _fileName; }
+ set
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ throw new ArgumentException(nameof(value));
+ }
+ _fileName = value;
+ }
+ }
+
+ internal string LogDirectory { get; set; }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/BatchLoggerConfigureOptions.cs b/src/Logging.AzureAppServices/src/BatchLoggerConfigureOptions.cs
new file mode 100644
index 0000000000..8dc8727b3a
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/BatchLoggerConfigureOptions.cs
@@ -0,0 +1,37 @@
+// 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.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal class BatchLoggerConfigureOptions : IConfigureOptions
+ {
+ private readonly IConfiguration _configuration;
+ private readonly string _isEnabledKey;
+
+ public BatchLoggerConfigureOptions(IConfiguration configuration, string isEnabledKey)
+ {
+ _configuration = configuration;
+ _isEnabledKey = isEnabledKey;
+ }
+
+ public void Configure(BatchingLoggerOptions options)
+ {
+ options.IsEnabled = TextToBoolean(_configuration.GetSection(_isEnabledKey)?.Value);
+ }
+
+ private static bool TextToBoolean(string text)
+ {
+ if (string.IsNullOrEmpty(text) ||
+ !bool.TryParse(text, out var result))
+ {
+ result = false;
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/BatchingLogger.cs b/src/Logging.AzureAppServices/src/BatchingLogger.cs
new file mode 100644
index 0000000000..bd192169f3
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/BatchingLogger.cs
@@ -0,0 +1,75 @@
+// 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.Text;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal class BatchingLogger : ILogger
+ {
+ private readonly BatchingLoggerProvider _provider;
+ private readonly string _category;
+
+ public BatchingLogger(BatchingLoggerProvider loggerProvider, string categoryName)
+ {
+ _provider = loggerProvider;
+ _category = categoryName;
+ }
+
+ public IDisposable BeginScope(TState state)
+ {
+ return null;
+ }
+
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ return _provider.IsEnabled;
+ }
+
+ public void Log(DateTimeOffset timestamp, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ if (!IsEnabled(logLevel))
+ {
+ return;
+ }
+
+ var builder = new StringBuilder();
+ builder.Append(timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff zzz"));
+ builder.Append(" [");
+ builder.Append(logLevel.ToString());
+ builder.Append("] ");
+ builder.Append(_category);
+
+ var scopeProvider = _provider.ScopeProvider;
+ if (scopeProvider != null)
+ {
+ scopeProvider.ForEachScope((scope, stringBuilder) =>
+ {
+ stringBuilder.Append(" => ").Append(scope);
+ }, builder);
+
+ builder.AppendLine(":");
+ }
+ else
+ {
+ builder.Append(": ");
+ }
+
+ builder.AppendLine(formatter(state, exception));
+
+ if (exception != null)
+ {
+ builder.AppendLine(exception.ToString());
+ }
+
+ _provider.AddMessage(timestamp, builder.ToString());
+ }
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ Log(DateTimeOffset.Now, logLevel, eventId, state, exception, formatter);
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/BatchingLoggerOptions.cs b/src/Logging.AzureAppServices/src/BatchingLoggerOptions.cs
new file mode 100644
index 0000000000..9fbd964800
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/BatchingLoggerOptions.cs
@@ -0,0 +1,80 @@
+// 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;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// Options for a logger which batches up log messages.
+ ///
+ public class BatchingLoggerOptions
+ {
+ private int? _batchSize;
+ private int? _backgroundQueueSize = 1000;
+ private TimeSpan _flushPeriod = TimeSpan.FromSeconds(1);
+
+ ///
+ /// Gets or sets the period after which logs will be flushed to the store.
+ ///
+ public TimeSpan FlushPeriod
+ {
+ get { return _flushPeriod; }
+ set
+ {
+ if (value <= TimeSpan.Zero)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FlushPeriod)} must be positive.");
+ }
+ _flushPeriod = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the maximum size of the background log message queue or null for no limit.
+ /// After maximum queue size is reached log event sink would start blocking.
+ /// Defaults to 1000.
+ ///
+ public int? BackgroundQueueSize
+ {
+ get { return _backgroundQueueSize; }
+ set
+ {
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(BackgroundQueueSize)} must be non-negative.");
+ }
+ _backgroundQueueSize = value;
+ }
+ }
+
+ ///
+ /// Gets or sets a maximum number of events to include in a single batch or null for no limit.
+ ///
+ /// Defaults to null.
+ public int? BatchSize
+ {
+ get { return _batchSize; }
+ set
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(BatchSize)} must be positive.");
+ }
+ _batchSize = value;
+ }
+ }
+
+ ///
+ /// Gets or sets value indicating if logger accepts and queues writes.
+ ///
+ public bool IsEnabled { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether scopes should be included in the message.
+ /// Defaults to false.
+ ///
+ public bool IncludeScopes { get; set; } = false;
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/BatchingLoggerProvider.cs b/src/Logging.AzureAppServices/src/BatchingLoggerProvider.cs
new file mode 100644
index 0000000000..227a616f3b
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/BatchingLoggerProvider.cs
@@ -0,0 +1,208 @@
+// 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.Concurrent;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// A provider of instances.
+ ///
+ public abstract class BatchingLoggerProvider : ILoggerProvider, ISupportExternalScope
+ {
+ private readonly List _currentBatch = new List();
+ private readonly TimeSpan _interval;
+ private readonly int? _queueSize;
+ private readonly int? _batchSize;
+ private readonly IDisposable _optionsChangeToken;
+
+ private int _messagesDropped;
+
+ private BlockingCollection _messageQueue;
+ private Task _outputTask;
+ private CancellationTokenSource _cancellationTokenSource;
+
+ private bool _includeScopes;
+ private IExternalScopeProvider _scopeProvider;
+
+ internal IExternalScopeProvider ScopeProvider => _includeScopes ? _scopeProvider : null;
+
+ internal BatchingLoggerProvider(IOptionsMonitor options)
+ {
+ // NOTE: Only IsEnabled is monitored
+
+ var loggerOptions = options.CurrentValue;
+ if (loggerOptions.BatchSize <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(loggerOptions.BatchSize), $"{nameof(loggerOptions.BatchSize)} must be a positive number.");
+ }
+ if (loggerOptions.FlushPeriod <= TimeSpan.Zero)
+ {
+ throw new ArgumentOutOfRangeException(nameof(loggerOptions.FlushPeriod), $"{nameof(loggerOptions.FlushPeriod)} must be longer than zero.");
+ }
+
+ _interval = loggerOptions.FlushPeriod;
+ _batchSize = loggerOptions.BatchSize;
+ _queueSize = loggerOptions.BackgroundQueueSize;
+
+ _optionsChangeToken = options.OnChange(UpdateOptions);
+ UpdateOptions(options.CurrentValue);
+ }
+
+ ///
+ /// Checks if the queue is enabled.
+ ///
+ public bool IsEnabled { get; private set; }
+
+ private void UpdateOptions(BatchingLoggerOptions options)
+ {
+ var oldIsEnabled = IsEnabled;
+ IsEnabled = options.IsEnabled;
+ _includeScopes = options.IncludeScopes;
+
+ if (oldIsEnabled != IsEnabled)
+ {
+ if (IsEnabled)
+ {
+ Start();
+ }
+ else
+ {
+ Stop();
+ }
+ }
+
+ }
+
+ internal abstract Task WriteMessagesAsync(IEnumerable messages, CancellationToken token);
+
+ private async Task ProcessLogQueue()
+ {
+ while (!_cancellationTokenSource.IsCancellationRequested)
+ {
+ var limit = _batchSize ?? int.MaxValue;
+
+ while (limit > 0 && _messageQueue.TryTake(out var message))
+ {
+ _currentBatch.Add(message);
+ limit--;
+ }
+
+ var messagesDropped = Interlocked.Exchange(ref _messagesDropped, 0);
+ if (messagesDropped != 0)
+ {
+ _currentBatch.Add(new LogMessage(DateTimeOffset.Now, $"{messagesDropped} message(s) dropped because of queue size limit. Increase the queue size or decrease logging verbosity to avoid this.{Environment.NewLine}"));
+ }
+
+ if (_currentBatch.Count > 0)
+ {
+ try
+ {
+ await WriteMessagesAsync(_currentBatch, _cancellationTokenSource.Token);
+ }
+ catch
+ {
+ // ignored
+ }
+
+ _currentBatch.Clear();
+ }
+ else
+ {
+ await IntervalAsync(_interval, _cancellationTokenSource.Token);
+ }
+ }
+ }
+
+ ///
+ /// Wait for the given .
+ ///
+ /// The amount of time to wait.
+ /// A that can be used to cancel the delay.
+ /// A which completes when the has passed or the has been canceled.
+ protected virtual Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
+ {
+ return Task.Delay(interval, cancellationToken);
+ }
+
+ internal void AddMessage(DateTimeOffset timestamp, string message)
+ {
+ if (!_messageQueue.IsAddingCompleted)
+ {
+ try
+ {
+ if (!_messageQueue.TryAdd(new LogMessage(timestamp, message), millisecondsTimeout: 0, cancellationToken: _cancellationTokenSource.Token))
+ {
+ Interlocked.Increment(ref _messagesDropped);
+ }
+ }
+ catch
+ {
+ //cancellation token canceled or CompleteAdding called
+ }
+ }
+ }
+
+ private void Start()
+ {
+ _messageQueue = _queueSize == null ?
+ new BlockingCollection(new ConcurrentQueue()) :
+ new BlockingCollection(new ConcurrentQueue(), _queueSize.Value);
+
+ _cancellationTokenSource = new CancellationTokenSource();
+ _outputTask = Task.Run(ProcessLogQueue);
+ }
+
+ private void Stop()
+ {
+ _cancellationTokenSource.Cancel();
+ _messageQueue.CompleteAdding();
+
+ try
+ {
+ _outputTask.Wait(_interval);
+ }
+ catch (TaskCanceledException)
+ {
+ }
+ catch (AggregateException ex) when (ex.InnerExceptions.Count == 1 && ex.InnerExceptions[0] is TaskCanceledException)
+ {
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ _optionsChangeToken?.Dispose();
+ if (IsEnabled)
+ {
+ Stop();
+ }
+ }
+
+ ///
+ /// Creates a with the given .
+ ///
+ /// The name of the category to create this logger with.
+ /// The that was created.
+ public ILogger CreateLogger(string categoryName)
+ {
+ return new BatchingLogger(this, categoryName);
+ }
+
+ ///
+ /// Sets the scope on this provider.
+ ///
+ /// Provides the scope.
+ void ISupportExternalScope.SetScopeProvider(IExternalScopeProvider scopeProvider)
+ {
+ _scopeProvider = scopeProvider;
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/BlobAppendReferenceWrapper.cs b/src/Logging.AzureAppServices/src/BlobAppendReferenceWrapper.cs
new file mode 100644
index 0000000000..e9805128b7
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/BlobAppendReferenceWrapper.cs
@@ -0,0 +1,97 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ internal class BlobAppendReferenceWrapper : ICloudAppendBlob
+ {
+ private readonly Uri _fullUri;
+ private readonly HttpClient _client;
+ private readonly Uri _appendUri;
+
+ public BlobAppendReferenceWrapper(string containerUrl, string name, HttpClient client)
+ {
+ var uriBuilder = new UriBuilder(containerUrl);
+ uriBuilder.Path += "/" + name;
+ _fullUri = uriBuilder.Uri;
+
+ AppendBlockQuery(uriBuilder);
+ _appendUri = uriBuilder.Uri;
+ _client = client;
+ }
+
+ ///
+ public async Task AppendAsync(ArraySegment data, CancellationToken cancellationToken)
+ {
+ Task AppendDataAsync()
+ {
+ var message = new HttpRequestMessage(HttpMethod.Put, _appendUri)
+ {
+ Content = new ByteArrayContent(data.Array, data.Offset, data.Count)
+ };
+ AddCommonHeaders(message);
+
+ return _client.SendAsync(message, cancellationToken);
+ }
+
+ var response = await AppendDataAsync();
+
+ if (response.StatusCode == HttpStatusCode.NotFound)
+ {
+ // If no blob exists try creating it
+ var message = new HttpRequestMessage(HttpMethod.Put, _fullUri)
+ {
+ // Set Content-Length to 0 to create "Append Blob"
+ Content = new ByteArrayContent(Array.Empty()),
+ Headers =
+ {
+ { "If-None-Match", "*" }
+ }
+ };
+
+ AddCommonHeaders(message);
+
+ response = await _client.SendAsync(message, cancellationToken);
+
+ // If result is 2** or 412 try to append again
+ if (response.IsSuccessStatusCode ||
+ response.StatusCode == HttpStatusCode.PreconditionFailed)
+ {
+ // Retry sending data after blob creation
+ response = await AppendDataAsync();
+ }
+ }
+
+ response.EnsureSuccessStatusCode();
+ }
+
+ private static void AddCommonHeaders(HttpRequestMessage message)
+ {
+ message.Headers.Add("x-ms-blob-type", "AppendBlob");
+ message.Headers.Add("x-ms-version", "2016-05-31");
+ message.Headers.Date = DateTimeOffset.UtcNow;
+ }
+
+ private static void AppendBlockQuery(UriBuilder uriBuilder)
+ {
+ // See https://msdn.microsoft.com/en-us/library/system.uribuilder.query.aspx for:
+ // Note: Do not append a string directly to Query property.
+ // If the length of Query is greater than 1, retrieve the property value
+ // as a string, remove the leading question mark, append the new query string,
+ // and set the property with the combined string.
+ var queryToAppend = "comp=appendblock";
+ if (uriBuilder.Query != null && uriBuilder.Query.Length > 1)
+ uriBuilder.Query = uriBuilder.Query.Substring(1) + "&" + queryToAppend;
+ else
+ uriBuilder.Query = queryToAppend;
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/BlobLoggerConfigureOptions.cs b/src/Logging.AzureAppServices/src/BlobLoggerConfigureOptions.cs
new file mode 100644
index 0000000000..f9a186872b
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/BlobLoggerConfigureOptions.cs
@@ -0,0 +1,30 @@
+// 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.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal class BlobLoggerConfigureOptions : BatchLoggerConfigureOptions, IConfigureOptions
+ {
+ private readonly IConfiguration _configuration;
+ private readonly IWebAppContext _context;
+
+ public BlobLoggerConfigureOptions(IConfiguration configuration, IWebAppContext context)
+ : base(configuration, "AzureBlobEnabled")
+ {
+ _configuration = configuration;
+ _context = context;
+ }
+
+ public void Configure(AzureBlobLoggerOptions options)
+ {
+ base.Configure(options);
+ options.ContainerUrl = _configuration.GetSection("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL")?.Value;
+ options.ApplicationName = _context.SiteName;
+ options.ApplicationInstanceId = _context.SiteInstanceId;
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/BlobLoggerProvider.cs b/src/Logging.AzureAppServices/src/BlobLoggerProvider.cs
new file mode 100644
index 0000000000..3d62ea2ac6
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/BlobLoggerProvider.cs
@@ -0,0 +1,92 @@
+// 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.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// The implementation that stores messages by appending them to Azure Blob in batches.
+ ///
+ [ProviderAlias("AzureAppServicesBlob")]
+ public class BlobLoggerProvider : BatchingLoggerProvider
+ {
+ private readonly string _appName;
+ private readonly string _fileName;
+ private readonly Func _blobReferenceFactory;
+ private readonly HttpClient _httpClient;
+
+ ///
+ /// Creates a new instance of
+ ///
+ /// The options to use when creating a provider.
+ public BlobLoggerProvider(IOptionsMonitor options)
+ : this(options, null)
+ {
+ _blobReferenceFactory = name => new BlobAppendReferenceWrapper(
+ options.CurrentValue.ContainerUrl,
+ name,
+ _httpClient);
+ }
+
+ ///
+ /// Creates a new instance of
+ ///
+ /// The container to store logs to.
+ /// Options to be used in creating a logger.
+ internal BlobLoggerProvider(
+ IOptionsMonitor options,
+ Func blobReferenceFactory) :
+ base(options)
+ {
+ var value = options.CurrentValue;
+ _appName = value.ApplicationName;
+ _fileName = value.ApplicationInstanceId + "_" + value.BlobName;
+ _blobReferenceFactory = blobReferenceFactory;
+ _httpClient = new HttpClient();
+ }
+
+ internal override async Task WriteMessagesAsync(IEnumerable messages, CancellationToken cancellationToken)
+ {
+ var eventGroups = messages.GroupBy(GetBlobKey);
+ foreach (var eventGroup in eventGroups)
+ {
+ var key = eventGroup.Key;
+ var blobName = $"{_appName}/{key.Year}/{key.Month:00}/{key.Day:00}/{key.Hour:00}/{_fileName}";
+
+ var blob = _blobReferenceFactory(blobName);
+
+ using (var stream = new MemoryStream())
+ using (var writer = new StreamWriter(stream))
+ {
+ foreach (var logEvent in eventGroup)
+ {
+ writer.Write(logEvent.Message);
+ }
+
+ await writer.FlushAsync();
+ var tryGetBuffer = stream.TryGetBuffer(out var buffer);
+ Debug.Assert(tryGetBuffer);
+ await blob.AppendAsync(buffer, cancellationToken);
+ }
+ }
+ }
+
+ private (int Year, int Month, int Day, int Hour) GetBlobKey(LogMessage e)
+ {
+ return (e.Timestamp.Year,
+ e.Timestamp.Month,
+ e.Timestamp.Day,
+ e.Timestamp.Hour);
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/ConfigurationBasedLevelSwitcher.cs b/src/Logging.AzureAppServices/src/ConfigurationBasedLevelSwitcher.cs
new file mode 100644
index 0000000000..c62ccb2331
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/ConfigurationBasedLevelSwitcher.cs
@@ -0,0 +1,51 @@
+// 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 Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal class ConfigurationBasedLevelSwitcher: IConfigureOptions
+ {
+ private readonly IConfiguration _configuration;
+ private readonly Type _provider;
+ private readonly string _levelKey;
+
+ public ConfigurationBasedLevelSwitcher(IConfiguration configuration, Type provider, string levelKey)
+ {
+ _configuration = configuration;
+ _provider = provider;
+ _levelKey = levelKey;
+ }
+
+ public void Configure(LoggerFilterOptions options)
+ {
+ options.Rules.Add(new LoggerFilterRule(_provider.FullName, null, GetLogLevel(), null));
+ }
+
+ private LogLevel GetLogLevel()
+ {
+ return TextToLogLevel(_configuration.GetSection(_levelKey)?.Value);
+ }
+
+ private static LogLevel TextToLogLevel(string text)
+ {
+ switch (text?.ToUpperInvariant())
+ {
+ case "ERROR":
+ return LogLevel.Error;
+ case "WARNING":
+ return LogLevel.Warning;
+ case "INFORMATION":
+ return LogLevel.Information;
+ case "VERBOSE":
+ return LogLevel.Trace;
+ default:
+ return LogLevel.None;
+ }
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/FileLoggerConfigureOptions.cs b/src/Logging.AzureAppServices/src/FileLoggerConfigureOptions.cs
new file mode 100644
index 0000000000..8cd1f5eb91
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/FileLoggerConfigureOptions.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.IO;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal class FileLoggerConfigureOptions : BatchLoggerConfigureOptions, IConfigureOptions
+ {
+ private readonly IWebAppContext _context;
+
+ public FileLoggerConfigureOptions(IConfiguration configuration, IWebAppContext context)
+ : base(configuration, "AzureDriveEnabled")
+ {
+ _context = context;
+ }
+
+ public void Configure(AzureFileLoggerOptions options)
+ {
+ base.Configure(options);
+ options.LogDirectory = Path.Combine(_context.HomeFolder, "LogFiles", "Application");
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/FileLoggerProvider.cs b/src/Logging.AzureAppServices/src/FileLoggerProvider.cs
new file mode 100644
index 0000000000..1143d38c07
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/FileLoggerProvider.cs
@@ -0,0 +1,89 @@
+// 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.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// A which writes out to a file.
+ ///
+ [ProviderAlias("AzureAppServicesFile")]
+ public class FileLoggerProvider : BatchingLoggerProvider
+ {
+ private readonly string _path;
+ private readonly string _fileName;
+ private readonly int? _maxFileSize;
+ private readonly int? _maxRetainedFiles;
+
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The options to use when creating a provider.
+ public FileLoggerProvider(IOptionsMonitor options) : base(options)
+ {
+ var loggerOptions = options.CurrentValue;
+ _path = loggerOptions.LogDirectory;
+ _fileName = loggerOptions.FileName;
+ _maxFileSize = loggerOptions.FileSizeLimit;
+ _maxRetainedFiles = loggerOptions.RetainedFileCountLimit;
+ }
+
+ internal override async Task WriteMessagesAsync(IEnumerable messages, CancellationToken cancellationToken)
+ {
+ Directory.CreateDirectory(_path);
+
+ foreach (var group in messages.GroupBy(GetGrouping))
+ {
+ var fullName = GetFullName(group.Key);
+ var fileInfo = new FileInfo(fullName);
+ if (_maxFileSize > 0 && fileInfo.Exists && fileInfo.Length > _maxFileSize)
+ {
+ return;
+ }
+
+ using (var streamWriter = File.AppendText(fullName))
+ {
+ foreach (var item in group)
+ {
+ await streamWriter.WriteAsync(item.Message);
+ }
+ }
+ }
+
+ RollFiles();
+ }
+
+ private string GetFullName((int Year, int Month, int Day) group)
+ {
+ return Path.Combine(_path, $"{_fileName}{group.Year:0000}{group.Month:00}{group.Day:00}.txt");
+ }
+
+ private (int Year, int Month, int Day) GetGrouping(LogMessage message)
+ {
+ return (message.Timestamp.Year, message.Timestamp.Month, message.Timestamp.Day);
+ }
+
+ private void RollFiles()
+ {
+ if (_maxRetainedFiles > 0)
+ {
+ var files = new DirectoryInfo(_path)
+ .GetFiles(_fileName + "*")
+ .OrderByDescending(f => f.Name)
+ .Skip(_maxRetainedFiles.Value);
+
+ foreach (var item in files)
+ {
+ item.Delete();
+ }
+ }
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/ICloudAppendBlob.cs b/src/Logging.AzureAppServices/src/ICloudAppendBlob.cs
new file mode 100644
index 0000000000..2f55bbb0d1
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/ICloudAppendBlob.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// Represents an append blob, a type of blob where blocks of data are always committed to the end of the blob.
+ ///
+ internal interface ICloudAppendBlob
+ {
+ ///
+ /// Initiates an asynchronous operation to open a stream for writing to the blob.
+ ///
+ /// A object of type that represents the asynchronous operation.
+ Task AppendAsync(ArraySegment data, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/IWebAppContext.cs b/src/Logging.AzureAppServices/src/IWebAppContext.cs
new file mode 100644
index 0000000000..f8c826ceb8
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/IWebAppContext.cs
@@ -0,0 +1,32 @@
+// 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.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// Represents an Azure WebApp context
+ ///
+ internal interface IWebAppContext
+ {
+ ///
+ /// Gets the path to the home folder if running in Azure WebApp
+ ///
+ string HomeFolder { get; }
+
+ ///
+ /// Gets the name of site if running in Azure WebApp
+ ///
+ string SiteName { get; }
+
+ ///
+ /// Gets the id of site if running in Azure WebApp
+ ///
+ string SiteInstanceId { get; }
+
+ ///
+ /// Gets a value indicating whether or new we're in an Azure WebApp
+ ///
+ bool IsRunningInAzureWebApp { get; }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/LogMessage.cs b/src/Logging.AzureAppServices/src/LogMessage.cs
new file mode 100644
index 0000000000..4a1179ceb3
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/LogMessage.cs
@@ -0,0 +1,20 @@
+// 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;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal readonly struct LogMessage
+ {
+ public LogMessage(DateTimeOffset timestamp, string message)
+ {
+ Timestamp = timestamp;
+ Message = message;
+ }
+
+ public DateTimeOffset Timestamp { get; }
+ public string Message { get; }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/Microsoft.Extensions.Logging.AzureAppServices.csproj b/src/Logging.AzureAppServices/src/Microsoft.Extensions.Logging.AzureAppServices.csproj
new file mode 100644
index 0000000000..5bedde8c6d
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/Microsoft.Extensions.Logging.AzureAppServices.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Logger implementation to support Azure App Services 'Diagnostics logs' and 'Log stream' features.
+ netstandard2.0
+ $(NoWarn);CS1591
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Logging.AzureAppServices/src/Properties/AssemblyInfo.cs b/src/Logging.AzureAppServices/src/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..7c7d332545
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/Properties/AssemblyInfo.cs
@@ -0,0 +1,8 @@
+// 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.Runtime.CompilerServices;
+
+
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
diff --git a/src/Logging.AzureAppServices/src/SiteConfigurationProvider.cs b/src/Logging.AzureAppServices/src/SiteConfigurationProvider.cs
new file mode 100644
index 0000000000..452c936f93
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/SiteConfigurationProvider.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.IO;
+using Microsoft.Extensions.Configuration;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal class SiteConfigurationProvider
+ {
+ public static IConfiguration GetAzureLoggingConfiguration(IWebAppContext context)
+ {
+ var settingsFolder = Path.Combine(context.HomeFolder, "site", "diagnostics");
+ var settingsFile = Path.Combine(settingsFolder, "settings.json");
+
+ return new ConfigurationBuilder()
+ .AddEnvironmentVariables()
+ .AddJsonFile(settingsFile, optional: true, reloadOnChange: true)
+ .Build();
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/WebAppContext.cs b/src/Logging.AzureAppServices/src/WebAppContext.cs
new file mode 100644
index 0000000000..8bdd3f1c76
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/WebAppContext.cs
@@ -0,0 +1,34 @@
+// 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;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// Represents the default implementation of .
+ ///
+ internal class WebAppContext : IWebAppContext
+ {
+ ///
+ /// Gets the default instance of the WebApp context.
+ ///
+ public static WebAppContext Default { get; } = new WebAppContext();
+
+ private WebAppContext() { }
+
+ ///
+ public string HomeFolder { get; } = Environment.GetEnvironmentVariable("HOME");
+
+ ///
+ public string SiteName { get; } = Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME");
+
+ ///
+ public string SiteInstanceId { get; } = Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID");
+
+ ///
+ public bool IsRunningInAzureWebApp => !string.IsNullOrEmpty(HomeFolder) &&
+ !string.IsNullOrEmpty(SiteName);
+ }
+}
diff --git a/src/Logging.AzureAppServices/test/AzureAppendBlobTests.cs b/src/Logging.AzureAppServices/test/AzureAppendBlobTests.cs
new file mode 100644
index 0000000000..2fd5955e86
--- /dev/null
+++ b/src/Logging.AzureAppServices/test/AzureAppendBlobTests.cs
@@ -0,0 +1,186 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class AzureAppendBlobTests
+ {
+ public string _containerUrl = "https://host/container?query=1";
+ public string _blobName = "blob/path";
+
+ [Fact]
+ public async Task SendsDataAsStream()
+ {
+ var testMessageHandler = new TestMessageHandler(async message =>
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
+ Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
+ AssertDefaultHeaders(message);
+
+ return new HttpResponseMessage(HttpStatusCode.OK);
+ });
+
+ var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler));
+ await blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None);
+ }
+
+ private static void AssertDefaultHeaders(HttpRequestMessage message)
+ {
+ Assert.Equal(new[] {"AppendBlob"}, message.Headers.GetValues("x-ms-blob-type"));
+ Assert.Equal(new[] {"2016-05-31"}, message.Headers.GetValues("x-ms-version"));
+ Assert.NotNull(message.Headers.Date);
+ }
+
+ [Theory]
+ [InlineData(HttpStatusCode.Created)]
+ [InlineData(HttpStatusCode.PreconditionFailed)]
+ public async Task CreatesBlobIfNotExist(HttpStatusCode createStatusCode)
+ {
+ var stage = 0;
+ var testMessageHandler = new TestMessageHandler(async message =>
+ {
+ // First PUT request
+ if (stage == 0)
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
+ Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
+ Assert.Equal(3, message.Content.Headers.ContentLength);
+
+ AssertDefaultHeaders(message);
+
+ stage++;
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ }
+ // Create request
+ if (stage == 1)
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1", message.RequestUri.ToString());
+ Assert.Equal(0, message.Content.Headers.ContentLength);
+ Assert.Equal(new[] { "*" }, message.Headers.GetValues("If-None-Match"));
+
+ AssertDefaultHeaders(message);
+
+ stage++;
+ return new HttpResponseMessage(createStatusCode);
+ }
+ // First PUT request
+ if (stage == 2)
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
+ Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
+ Assert.Equal(3, message.Content.Headers.ContentLength);
+
+ AssertDefaultHeaders(message);
+
+ stage++;
+ return new HttpResponseMessage(HttpStatusCode.Created);
+ }
+ throw new NotImplementedException();
+ });
+
+ var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler));
+ await blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None);
+
+ Assert.Equal(3, stage);
+ }
+
+ [Fact]
+ public async Task ThrowsForUnknownStatus()
+ {
+ var stage = 0;
+ var testMessageHandler = new TestMessageHandler(async message =>
+ {
+ // First PUT request
+ if (stage == 0)
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
+ Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
+ Assert.Equal(3, message.Content.Headers.ContentLength);
+
+ AssertDefaultHeaders(message);
+
+ stage++;
+ return new HttpResponseMessage(HttpStatusCode.InternalServerError);
+ }
+
+ throw new NotImplementedException();
+ });
+
+ var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler));
+ await Assert.ThrowsAsync(() => blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None));
+
+ Assert.Equal(1, stage);
+ }
+
+ [Fact]
+ public async Task ThrowsForUnknownStatusDuringCreation()
+ {
+ var stage = 0;
+ var testMessageHandler = new TestMessageHandler(async message =>
+ {
+ // First PUT request
+ if (stage == 0)
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
+ Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
+ Assert.Equal(3, message.Content.Headers.ContentLength);
+
+ AssertDefaultHeaders(message);
+
+ stage++;
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ }
+ // Create request
+ if (stage == 1)
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1", message.RequestUri.ToString());
+ Assert.Equal(0, message.Content.Headers.ContentLength);
+ Assert.Equal(new[] { "*" }, message.Headers.GetValues("If-None-Match"));
+
+ AssertDefaultHeaders(message);
+
+ stage++;
+ return new HttpResponseMessage(HttpStatusCode.InternalServerError);
+ }
+
+ throw new NotImplementedException();
+ });
+
+ var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler));
+ await Assert.ThrowsAsync(() => blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None));
+
+ Assert.Equal(2, stage);
+ }
+
+
+ private class TestMessageHandler : HttpMessageHandler
+ {
+ private readonly Func> _callback;
+
+ public TestMessageHandler(Func> callback)
+ {
+ _callback = callback;
+ }
+
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return await _callback(request);
+ }
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/test/AzureBlobSinkTests.cs b/src/Logging.AzureAppServices/test/AzureBlobSinkTests.cs
new file mode 100644
index 0000000000..4d9125335a
--- /dev/null
+++ b/src/Logging.AzureAppServices/test/AzureBlobSinkTests.cs
@@ -0,0 +1,98 @@
+// 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.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class AzureBlobSinkTests
+ {
+ DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero);
+
+ [Fact]
+ public async Task WritesMessagesInBatches()
+ {
+ var blob = new Mock();
+ var buffers = new List();
+ blob.Setup(b => b.AppendAsync(It.IsAny>(), It.IsAny()))
+ .Callback((ArraySegment s, CancellationToken ct) => buffers.Add(ToArray(s)))
+ .Returns(Task.CompletedTask);
+
+ var sink = new TestBlobSink(name => blob.Object);
+ var logger = (BatchingLogger)sink.CreateLogger("Cat");
+
+ await sink.IntervalControl.Pause;
+
+ for (int i = 0; i < 5; i++)
+ {
+ logger.Log(_timestampOne, LogLevel.Information, 0, "Text " + i, null, (state, ex) => state);
+ }
+
+ sink.IntervalControl.Resume();
+ await sink.IntervalControl.Pause;
+
+ Assert.Single(buffers);
+ Assert.Equal(
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 0" + Environment.NewLine +
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 1" + Environment.NewLine +
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 2" + Environment.NewLine +
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 3" + Environment.NewLine +
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 4" + Environment.NewLine,
+ Encoding.UTF8.GetString(buffers[0]));
+ }
+
+ [Fact]
+ public async Task GroupsByHour()
+ {
+ var blob = new Mock();
+ var buffers = new List();
+ var names = new List();
+
+ blob.Setup(b => b.AppendAsync(It.IsAny>(), It.IsAny()))
+ .Callback((ArraySegment s, CancellationToken ct) => buffers.Add(ToArray(s)))
+ .Returns(Task.CompletedTask);
+
+ var sink = new TestBlobSink(name =>
+ {
+ names.Add(name);
+ return blob.Object;
+ });
+ var logger = (BatchingLogger)sink.CreateLogger("Cat");
+
+ await sink.IntervalControl.Pause;
+
+ var startDate = _timestampOne;
+ for (int i = 0; i < 3; i++)
+ {
+ logger.Log(startDate, LogLevel.Information, 0, "Text " + i, null, (state, ex) => state);
+
+ startDate = startDate.AddHours(1);
+ }
+
+ sink.IntervalControl.Resume();
+ await sink.IntervalControl.Pause;
+
+ Assert.Equal(3, buffers.Count);
+
+ Assert.Equal("appname/2016/05/04/03/42_filename", names[0]);
+ Assert.Equal("appname/2016/05/04/04/42_filename", names[1]);
+ Assert.Equal("appname/2016/05/04/05/42_filename", names[2]);
+ }
+
+ private byte[] ToArray(ArraySegment inputStream)
+ {
+ return inputStream.Array
+ .Skip(inputStream.Offset)
+ .Take(inputStream.Count)
+ .ToArray();
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/test/AzureDiagnosticsConfigurationProviderTests.cs b/src/Logging.AzureAppServices/test/AzureDiagnosticsConfigurationProviderTests.cs
new file mode 100644
index 0000000000..00d7dcd58d
--- /dev/null
+++ b/src/Logging.AzureAppServices/test/AzureDiagnosticsConfigurationProviderTests.cs
@@ -0,0 +1,70 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class AzureDiagnosticsConfigurationProviderTests
+ {
+ [Fact]
+ public void NoConfigFile()
+ {
+ var tempFolder = Path.Combine(Path.GetTempPath(), "AzureWebAppLoggerThisFolderShouldNotExist");
+
+ var contextMock = new Mock();
+ contextMock.SetupGet(c => c.HomeFolder)
+ .Returns(tempFolder);
+
+ var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(contextMock.Object);
+
+ Assert.NotNull(config);
+ }
+
+ [Fact]
+ public void ReadsSettingsFileAndEnvironment()
+ {
+ var tempFolder = Path.Combine(Path.GetTempPath(), "WebAppLoggerConfigurationDisabledInSettingsFile");
+
+ try
+ {
+ var settingsFolder = Path.Combine(tempFolder, "site", "diagnostics");
+ var settingsFile = Path.Combine(settingsFolder, "settings.json");
+
+ if (!Directory.Exists(settingsFolder))
+ {
+ Directory.CreateDirectory(settingsFolder);
+ }
+ Environment.SetEnvironmentVariable("RANDOM_ENVIRONMENT_VARIABLE", "USEFUL_VALUE");
+ File.WriteAllText(settingsFile, @"{ ""key"":""test value"" }");
+
+ var contextMock = new Mock();
+ contextMock.SetupGet(c => c.HomeFolder)
+ .Returns(tempFolder);
+
+ var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(contextMock.Object);
+
+ Assert.Equal("test value", config["key"]);
+ Assert.Equal("USEFUL_VALUE", config["RANDOM_ENVIRONMENT_VARIABLE"]);
+ }
+ finally
+ {
+ if (Directory.Exists(tempFolder))
+ {
+ try
+ {
+ Directory.Delete(tempFolder, recursive: true);
+ }
+ catch
+ {
+ // Don't break the test if temp folder deletion fails.
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/test/BatchingLoggerProviderTests.cs b/src/Logging.AzureAppServices/test/BatchingLoggerProviderTests.cs
new file mode 100644
index 0000000000..9ab0c0cb45
--- /dev/null
+++ b/src/Logging.AzureAppServices/test/BatchingLoggerProviderTests.cs
@@ -0,0 +1,136 @@
+// 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.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class BatchingLoggerProviderTests
+ {
+ private DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero);
+ private string _nl = Environment.NewLine;
+ private Regex _timeStampRegex = new Regex(@"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3} .\d{2}:\d{2} ");
+
+ [Fact]
+ public async Task LogsInIntervals()
+ {
+ var provider = new TestBatchingLoggingProvider();
+ var logger = (BatchingLogger)provider.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+
+ logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ Assert.Equal("2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + _nl, provider.Batches[0][0].Message);
+ Assert.Equal("2016-05-04 04:02:01.000 +00:00 [Error] Cat: Error message" + _nl, provider.Batches[0][1].Message);
+ }
+
+ [Fact]
+ public async Task IncludesScopes()
+ {
+ var provider = new TestBatchingLoggingProvider(includeScopes: true);
+ var factory = new LoggerFactory(new [] { provider });
+ var logger = factory.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+
+ using (logger.BeginScope("Scope"))
+ {
+ using (logger.BeginScope("Scope2"))
+ {
+ logger.Log(LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ }
+ }
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ Assert.Matches(_timeStampRegex, provider.Batches[0][0].Message);
+ Assert.EndsWith(
+ " [Information] Cat => Scope => Scope2:" + _nl +
+ "Info message" + _nl,
+ provider.Batches[0][0].Message);
+ }
+
+ [Fact]
+ public async Task RespectsBatchSize()
+ {
+ var provider = new TestBatchingLoggingProvider(maxBatchSize: 1);
+ var logger = (BatchingLogger)provider.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+
+ logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ Assert.Equal(2, provider.Batches.Count);
+ Assert.Single(provider.Batches[0]);
+ Assert.Equal("2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + _nl, provider.Batches[0][0].Message);
+
+ Assert.Single(provider.Batches[1]);
+ Assert.Equal("2016-05-04 04:02:01.000 +00:00 [Error] Cat: Error message" + _nl, provider.Batches[1][0].Message);
+ }
+
+ [Fact]
+ public async Task DropsMessagesWhenReachingMaxQueue()
+ {
+ var provider = new TestBatchingLoggingProvider(maxQueueSize: 1);
+ var logger = (BatchingLogger)provider.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+
+ logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ Assert.Equal(2, provider.Batches[0].Length);
+ Assert.Equal("2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + _nl, provider.Batches[0][0].Message);
+ Assert.Equal("1 message(s) dropped because of queue size limit. Increase the queue size or decrease logging verbosity to avoid this." + _nl, provider.Batches[0][1].Message);
+ }
+
+ private class TestBatchingLoggingProvider: BatchingLoggerProvider
+ {
+ public List Batches { get; } = new List();
+ public ManualIntervalControl IntervalControl { get; } = new ManualIntervalControl();
+
+ public TestBatchingLoggingProvider(TimeSpan? interval = null, int? maxBatchSize = null, int? maxQueueSize = null, bool includeScopes = false)
+ : base(new OptionsWrapperMonitor(new BatchingLoggerOptions
+ {
+ FlushPeriod = interval ?? TimeSpan.FromSeconds(1),
+ BatchSize = maxBatchSize,
+ BackgroundQueueSize = maxQueueSize,
+ IsEnabled = true,
+ IncludeScopes = includeScopes
+ }))
+ {
+ }
+
+ internal override Task WriteMessagesAsync(IEnumerable messages, CancellationToken token)
+ {
+ Batches.Add(messages.ToArray());
+ return Task.CompletedTask;
+ }
+
+ protected override Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
+ {
+ return IntervalControl.IntervalAsync();
+ }
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/test/ConfigureOptionsTests.cs b/src/Logging.AzureAppServices/test/ConfigureOptionsTests.cs
new file mode 100644
index 0000000000..46b72c7a0d
--- /dev/null
+++ b/src/Logging.AzureAppServices/test/ConfigureOptionsTests.cs
@@ -0,0 +1,71 @@
+// 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 Microsoft.Extensions.Configuration;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class ConfigureOptionsTests
+ {
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ [InlineData(null)]
+ public void InitializesIsEnabled(bool? enabled)
+ {
+ var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[]
+ {
+ new KeyValuePair("IsEnabledKey", Convert.ToString(enabled))
+ }).Build();
+
+ var options = new BatchingLoggerOptions();
+ new BatchLoggerConfigureOptions(configuration, "IsEnabledKey").Configure(options);
+
+ Assert.Equal(enabled ?? false, options.IsEnabled);
+ }
+
+ [Fact]
+ public void InitializesLogDirectory()
+ {
+ var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[]
+ {
+ new KeyValuePair("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL", "http://container/url")
+ }).Build();
+
+ var contextMock = new Mock();
+ contextMock.SetupGet(c => c.HomeFolder).Returns("Home");
+
+ var options = new AzureFileLoggerOptions();
+ new FileLoggerConfigureOptions(configuration, contextMock.Object).Configure(options);
+
+ Assert.Equal(Path.Combine("Home", "LogFiles", "Application"), options.LogDirectory);
+ }
+
+ [Fact]
+ public void InitializesBlobUriSiteInstanceAndName()
+ {
+ var configuration = new ConfigurationBuilder().AddInMemoryCollection(new []
+ {
+ new KeyValuePair("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL", "http://container/url")
+ }).Build();
+
+ var contextMock = new Mock();
+ contextMock.SetupGet(c => c.HomeFolder).Returns("Home");
+ contextMock.SetupGet(c => c.SiteInstanceId).Returns("InstanceId");
+ contextMock.SetupGet(c => c.SiteName).Returns("Name");
+
+ var options = new AzureBlobLoggerOptions();
+ new BlobLoggerConfigureOptions(configuration, contextMock.Object).Configure(options);
+
+ Assert.Equal("http://container/url", options.ContainerUrl);
+ Assert.Equal("InstanceId", options.ApplicationInstanceId);
+ Assert.Equal("Name", options.ApplicationName);
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/test/FileLoggerTests.cs b/src/Logging.AzureAppServices/test/FileLoggerTests.cs
new file mode 100644
index 0000000000..a3fcd2587d
--- /dev/null
+++ b/src/Logging.AzureAppServices/test/FileLoggerTests.cs
@@ -0,0 +1,122 @@
+// 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.Linq;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class FileLoggerTests: IDisposable
+ {
+ DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero);
+
+ public FileLoggerTests()
+ {
+ TempPath = Path.GetTempFileName() + "_";
+ }
+
+ public string TempPath { get; }
+
+ public void Dispose()
+ {
+ try
+ {
+ if (Directory.Exists(TempPath))
+ {
+ Directory.Delete(TempPath, true);
+ }
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+
+ [Fact]
+ public async Task WritesToTextFile()
+ {
+ var provider = new TestFileLoggerProvider(TempPath);
+ var logger = (BatchingLogger)provider.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+
+ logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ Assert.Equal(
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + Environment.NewLine +
+ "2016-05-04 04:02:01.000 +00:00 [Error] Cat: Error message" + Environment.NewLine,
+ File.ReadAllText(Path.Combine(TempPath, "LogFile.20160504.txt")));
+ }
+
+ [Fact]
+ public async Task RollsTextFile()
+ {
+ var provider = new TestFileLoggerProvider(TempPath);
+ var logger = (BatchingLogger)provider.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+
+ logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ logger.Log(_timestampOne.AddDays(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ Assert.Equal(
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + Environment.NewLine,
+ File.ReadAllText(Path.Combine(TempPath, "LogFile.20160504.txt")));
+
+ Assert.Equal(
+ "2016-05-05 03:02:01.000 +00:00 [Error] Cat: Error message" + Environment.NewLine,
+ File.ReadAllText(Path.Combine(TempPath, "LogFile.20160505.txt")));
+ }
+
+ [Fact]
+ public async Task RespectsMaxFileCount()
+ {
+ Directory.CreateDirectory(TempPath);
+ File.WriteAllText(Path.Combine(TempPath, "randomFile.txt"), "Text");
+
+ var provider = new TestFileLoggerProvider(TempPath, maxRetainedFiles: 5);
+ var logger = (BatchingLogger)provider.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+ var timestamp = _timestampOne;
+
+ for (int i = 0; i < 10; i++)
+ {
+ logger.Log(timestamp, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ logger.Log(timestamp.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
+
+ timestamp = timestamp.AddDays(1);
+ }
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ var actualFiles = new DirectoryInfo(TempPath)
+ .GetFiles()
+ .Select(f => f.Name)
+ .OrderBy(f => f)
+ .ToArray();
+
+ Assert.Equal(6, actualFiles.Length);
+ Assert.Equal(new[] {
+ "LogFile.20160509.txt",
+ "LogFile.20160510.txt",
+ "LogFile.20160511.txt",
+ "LogFile.20160512.txt",
+ "LogFile.20160513.txt",
+ "randomFile.txt"
+ }, actualFiles);
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/test/LoggerBuilderExtensionsTests.cs b/src/Logging.AzureAppServices/test/LoggerBuilderExtensionsTests.cs
new file mode 100644
index 0000000000..cf8bede1a5
--- /dev/null
+++ b/src/Logging.AzureAppServices/test/LoggerBuilderExtensionsTests.cs
@@ -0,0 +1,79 @@
+// 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.Linq;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class LoggerBuilderExtensionsTests
+ {
+ private IWebAppContext _appContext;
+
+ public LoggerBuilderExtensionsTests()
+ {
+ var contextMock = new Mock();
+ contextMock.SetupGet(c => c.IsRunningInAzureWebApp).Returns(true);
+ contextMock.SetupGet(c => c.HomeFolder).Returns(".");
+ _appContext = contextMock.Object;
+ }
+
+ [Fact]
+ public void BuilderExtensionAddsSingleSetOfServicesWhenCalledTwice()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext));
+ var count = serviceCollection.Count;
+
+ Assert.NotEqual(0, count);
+
+ serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext));
+
+ Assert.Equal(count, serviceCollection.Count);
+ }
+
+ [Fact]
+ public void BuilderExtensionAddsConfigurationChangeTokenSource()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging(builder => builder.AddConfiguration(new ConfigurationBuilder().Build()));
+
+ // Tracking for main configuration
+ Assert.Equal(1, serviceCollection.Count(d => d.ServiceType == typeof(IOptionsChangeTokenSource)));
+
+ serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext));
+
+ // Make sure we add another config change token for azure diagnostic configuration
+ Assert.Equal(2, serviceCollection.Count(d => d.ServiceType == typeof(IOptionsChangeTokenSource)));
+ }
+
+ [Fact]
+ public void BuilderExtensionAddsIConfigureOptions()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging(builder => builder.AddConfiguration(new ConfigurationBuilder().Build()));
+
+ // Tracking for main configuration
+ Assert.Equal(2, serviceCollection.Count(d => d.ServiceType == typeof(IConfigureOptions)));
+
+ serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext));
+
+ Assert.Equal(4, serviceCollection.Count(d => d.ServiceType == typeof(IConfigureOptions)));
+ }
+
+ [Fact]
+ public void LoggerProviderIsResolvable()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext));
+
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+ var loggerFactory = serviceProvider.GetService();
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/test/ManualIntervalControl.cs b/src/Logging.AzureAppServices/test/ManualIntervalControl.cs
new file mode 100644
index 0000000000..29cc883a28
--- /dev/null
+++ b/src/Logging.AzureAppServices/test/ManualIntervalControl.cs
@@ -0,0 +1,31 @@
+// 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.Tasks;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ internal class ManualIntervalControl
+ {
+
+ private TaskCompletionSource