Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Azure storage integration #1247

Merged
merged 33 commits into from
Apr 8, 2021
Merged

Conversation

russcam
Copy link
Contributor

@russcam russcam commented Mar 26, 2021

This PR provides integrations for Azure storage, using the newer version 12 libraries.

The version 12 libraries is still relatively new, and I suspect that many folks are still using the previous library versions. The new libraries have emit Diagnostic source events for every operation, which is how this integration creates transactions and spans.

The previous libraries do not appear to emit diagnostic events, so would require inspecting HTTP calls to ascertain if the request is to a known service. This could be done as part of this PR, or could be done in a follow up PR, if we agree to support the previous major library versions (different nuget packages).

Closes #1156
Closes #1155

@russcam russcam added the enhancement New feature or request label Mar 26, 2021
@russcam russcam changed the title Azure storage integraton Azure storage integration Mar 26, 2021
@russcam
Copy link
Contributor Author

russcam commented Mar 26, 2021

Opened as a draft PR for the moment, as this PR branch is branched from #1225 in order to use the terraform components.

@apmmachine
Copy link
Contributor

apmmachine commented Mar 26, 2021

💔 Tests Failed

the below badges are clickable and redirect to their specific view in the CI or DOCS
Pipeline View Test View Changes Artifacts preview

Expand to view the summary

Build stats

  • Build Cause: Pull request #1247 updated

  • Start Time: 2021-04-08T01:25:57.532+0000

  • Duration: 60 min 18 sec

  • Commit: 71ca99a

Test stats 🧪

Test Results
Failed 1
Passed 19245
Skipped 85
Total 19331

Trends 🧪

Image of Build Times

Image of Tests

Test errors 1

Expand to view the tests failures

Initializing / Parallel / Windows .NET Framework / IIS Tests / CentralConfigTests+MaxSpansAndSampleRateTests.Elastic.Apm.AspNetFullFramework.Tests.CentralConfigTests+MaxSpansAndSampleRateTests.Combination_of_both_options(maxSpansLocalConfig: 10, isSampledLocalConfig: null) – Elastic.Apm.AspNetFullFramework.Tests.CentralConfigTests+MaxSpansAndSampleRateTests
    Expand to view the error details

     System.AggregateException : One or more errors occurred. (A task was canceled.)
    ---- System.Threading.Tasks.TaskCanceledException : A task was canceled. 
    

    Expand to view the stacktrace

     System.AggregateException : One or more errors occurred. (A task was canceled.)
    ---- System.Threading.Tasks.TaskCanceledException : A task was canceled.
    Stack Trace:
       at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
       at Elastic.Apm.Tests.MockApmServer.Startup.ConfigureServices(IServiceCollection services) in C:\Users\jenkins\workspace\net_apm-agent-dotnet-mbp_PR-1247\apm-agent-dotnet\test\Elastic.Apm.Tests.MockApmServer\Startup.cs:line 29
    --- End of stack trace from previous location where exception was thrown ---
       at Microsoft.AspNetCore.Hosting.ConventionBasedStartup.ConfigureServices(IServiceCollection services)
       at Microsoft.AspNetCore.Hosting.Internal.WebHost.EnsureApplicationServices()
       at Microsoft.AspNetCore.Hosting.Internal.WebHost.Initialize()
       at Microsoft.AspNetCore.Hosting.WebHostBuilder.Build()
       at Elastic.Apm.Tests.MockApmServer.MockApmServer.RunInBackground(Int32 port) in C:\Users\jenkins\workspace\net_apm-agent-dotnet-mbp_PR-1247\apm-agent-dotnet\test\Elastic.Apm.Tests.MockApmServer\MockApmServer.cs:line 150
       at Elastic.Apm.AspNetFullFramework.Tests.TestsBase.InitializeAsync() in C:\Users\jenkins\workspace\net_apm-agent-dotnet-mbp_PR-1247\apm-agent-dotnet\test\Elastic.Apm.AspNetFullFramework.Tests\TestsBase.cs:line 209
    ----- Inner Stack Trace -----
    
    Standard Output:
        Elastic APM .NET Tests> 95> Xunit> [2021-04-08 02:22:08.832 +00:00][Info] - {LoggingTestBase} Starting test: Elastic.Apm.AspNetFullFramework.Tests.CentralConfigTests+MaxSpansAndSampleRateTests.Combination_of_both_options(maxSpansLocalConfig: 10, isSampledLocalConfig: null)...
        Elastic APM .NET Tests> 95> Xunit> [2021-04-08 02:23:49.495 +00:00][Info] - {LoggingTestBase} Finished test: Elastic.Apm.AspNetFullFramework.Tests.CentralConfigTests+MaxSpansAndSampleRateTests.Combination_of_both_options(maxSpansLocalConfig: 10, isSampledLocalConfig: null) 
    

Steps errors 2

Expand to view the steps failures

Test IIS
  • Took 19 min 21 sec . View more details on here
  • Description: .ci/windows/test-iis.bat
Build
  • Took 1 min 33 sec . View more details on here
  • Description: .ci/windows/dotnet.bat

Log output

Expand to view the last 100 lines of log output

[2021-04-08T02:26:01.307Z] info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
[2021-04-08T02:26:01.307Z]       Executed action Elastic.Apm.Tests.MockApmServer.Controllers.IntakeV2EventsController.Post (Elastic.Apm.Tests.MockApmServer) in 4.8505ms
[2021-04-08T02:26:01.307Z] info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
[2021-04-08T02:26:01.307Z]       Request finished in 5.2009ms 200 text/plain; charset=utf-8
[2021-04-08T02:26:01.307Z] info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
[2021-04-08T02:26:01.307Z]       Request starting HTTP/1.1 POST http://localhost:24157/intake/v2/events application/x-ndjson; charset=utf-8 11051
[2021-04-08T02:26:01.307Z] info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3]
[2021-04-08T02:26:01.308Z]       Route matched with {action = "Post", controller = "IntakeV2Events"}. Executing controller action with signature System.Threading.Tasks.Task`1[Microsoft.AspNetCore.Mvc.IActionResult] Post() on controller Elastic.Apm.Tests.MockApmServer.Controllers.IntakeV2EventsController (Elastic.Apm.Tests.MockApmServer).
[2021-04-08T02:26:01.308Z] info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
[2021-04-08T02:26:01.308Z]       Executing action method Elastic.Apm.Tests.MockApmServer.Controllers.IntakeV2EventsController.Post (Elastic.Apm.Tests.MockApmServer) - Validation state: Valid
[2021-04-08T02:26:01.308Z] info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
[2021-04-08T02:26:01.308Z]       Executed action method Elastic.Apm.Tests.MockApmServer.Controllers.IntakeV2EventsController.Post (Elastic.Apm.Tests.MockApmServer), returned result Microsoft.AspNetCore.Mvc.OkObjectResult in 4.1861ms.
[2021-04-08T02:26:01.308Z] info: Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor[1]
[2021-04-08T02:26:01.308Z]       Executing ObjectResult, writing value of type 'System.String'.
[2021-04-08T02:26:01.308Z] info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
[2021-04-08T02:26:01.308Z]       Executed action Elastic.Apm.Tests.MockApmServer.Controllers.IntakeV2EventsController.Post (Elastic.Apm.Tests.MockApmServer) in 4.4485ms
[2021-04-08T02:26:01.308Z] info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
[2021-04-08T02:26:01.308Z]       Request finished in 4.7266ms 200 text/plain; charset=utf-8
[2021-04-08T02:26:01.308Z] info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
[2021-04-08T02:26:01.308Z]       Request starting HTTP/1.1 POST http://localhost:24157/intake/v2/events application/x-ndjson; charset=utf-8 3337
[2021-04-08T02:26:01.308Z] info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3]
[2021-04-08T02:26:01.308Z]       Route matched with {action = "Post", controller = "IntakeV2Events"}. Executing controller action with signature System.Threading.Tasks.Task`1[Microsoft.AspNetCore.Mvc.IActionResult] Post() on controller Elastic.Apm.Tests.MockApmServer.Controllers.IntakeV2EventsController (Elastic.Apm.Tests.MockApmServer).
[2021-04-08T02:26:01.308Z] info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
[2021-04-08T02:26:01.308Z]       Executing action method Elastic.Apm.Tests.MockApmServer.Controllers.IntakeV2EventsController.Post (Elastic.Apm.Tests.MockApmServer) - Validation state: Valid
[2021-04-08T02:26:01.308Z] info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
[2021-04-08T02:26:01.308Z]       Executed action method Elastic.Apm.Tests.MockApmServer.Controllers.IntakeV2EventsController.Post (Elastic.Apm.Tests.MockApmServer), returned result Microsoft.AspNetCore.Mvc.OkObjectResult in 2.916ms.
[2021-04-08T02:26:01.308Z] info: Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor[1]
[2021-04-08T02:26:01.308Z]       Executing ObjectResult, writing value of type 'System.String'.
[2021-04-08T02:26:01.308Z] info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
[2021-04-08T02:26:01.308Z]       Executed action Elastic.Apm.Tests.MockApmServer.Controllers.IntakeV2EventsController.Post (Elastic.Apm.Tests.MockApmServer) in 3.3007ms
[2021-04-08T02:26:01.308Z] info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
[2021-04-08T02:26:01.308Z]       Request finished in 3.614ms 200 text/plain; charset=utf-8
[2021-04-08T02:26:01.308Z] info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
[2021-04-08T02:26:01.308Z]       Request starting HTTP/1.1 GET http://localhost:24157/config/v1/agents?service.name=AspNetFullFramework_Tests_CentralConfigTests-CustomServiceName&service.environment=AspNetFullFramework_Tests_CentralConfigTests-CustomEnvironment  
[2021-04-08T02:26:01.308Z] info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3]
[2021-04-08T02:26:01.308Z]       Route matched with {action = "Get", controller = "AgentsConfig"}. Executing controller action with signature Microsoft.AspNetCore.Mvc.IActionResult Get() on controller Elastic.Apm.Tests.MockApmServer.Controllers.AgentsConfigController (Elastic.Apm.Tests.MockApmServer).
[2021-04-08T02:26:01.308Z] info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
[2021-04-08T02:26:01.308Z]       Executing action method Elastic.Apm.Tests.MockApmServer.Controllers.AgentsConfigController.Get (Elastic.Apm.Tests.MockApmServer) - Validation state: Valid
[2021-04-08T02:26:01.308Z] info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
[2021-04-08T02:26:01.308Z]       Executed action method Elastic.Apm.Tests.MockApmServer.Controllers.AgentsConfigController.Get (Elastic.Apm.Tests.MockApmServer), returned result Microsoft.AspNetCore.Mvc.StatusCodeResult in 0.1008ms.
[2021-04-08T02:26:01.309Z] info: Microsoft.AspNetCore.Mvc.StatusCodeResult[1]
[2021-04-08T02:26:01.309Z]       Executing HttpStatusCodeResult, setting HTTP status code 304
[2021-04-08T02:26:01.309Z] info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
[2021-04-08T02:26:01.309Z]       Executed action Elastic.Apm.Tests.MockApmServer.Controllers.AgentsConfigController.Get (Elastic.Apm.Tests.MockApmServer) in 0.2314ms
[2021-04-08T02:26:01.309Z] info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
[2021-04-08T02:26:01.309Z]       Request finished in 0.5283ms 304 
[2021-04-08T02:26:01.309Z] info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
[2021-04-08T02:26:01.309Z]       Request starting HTTP/1.1 POST http://localhost:24157/intake/v2/events application/x-ndjson; charset=utf-8 1800
[2021-04-08T02:26:01.309Z] info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3]
[2021-04-08T02:26:01.309Z]       Route matched with {action = "Post", controller = "IntakeV2Events"}. Executing controller action with signature System.Threading.Tasks.Task`1[Microsoft.AspNetCore.Mvc.IActionResult] Post() on controller Elastic.Apm.Tests.MockApmServer.Controllers.IntakeV2EventsController (Elastic.Apm.Tests.MockApmServer).
[2021-04-08T02:26:01.309Z] info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
[2021-04-08T02:26:01.309Z]       Executing action method Elastic.Apm.Tests.MockApmServer.Controllers.IntakeV2EventsController.Post (Elastic.Apm.Tests.MockApmServer) - Validation state: Valid
[2021-04-08T02:26:01.309Z] info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
[2021-04-08T02:26:01.309Z]       Executed action method Elastic.Apm.Tests.MockApmServer.Controllers.IntakeV2EventsController.Post (Elastic.Apm.Tests.MockApmServer), returned result Microsoft.AspNetCore.Mvc.OkObjectResult in 2.1426ms.
[2021-04-08T02:26:01.309Z] info: Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor[1]
[2021-04-08T02:26:01.309Z]       Executing ObjectResult, writing value of type 'System.String'.
[2021-04-08T02:26:01.309Z] info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
[2021-04-08T02:26:01.309Z]       Executed action Elastic.Apm.Tests.MockApmServer.Controllers.IntakeV2EventsController.Post (Elastic.Apm.Tests.MockApmServer) in 2.3783ms
[2021-04-08T02:26:01.309Z] info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
[2021-04-08T02:26:01.309Z]       Request finished in 2.6472ms 200 text/plain; charset=utf-8
[2021-04-08T02:26:03.218Z] Elastic APM .NET Tests> 100> Console> [2021-04-08 02:26:02.585 +00:00][Info] - {LoggingTestBase} Finished test: Elastic.Apm.AspNetFullFramework.Tests.CentralConfigTests+MaxSpansAndSampleRateTests.MaxSpans_invalid_value
[2021-04-08T02:26:03.218Z] [xUnit.net 00:19:16.65]   Finished:    Elastic.Apm.AspNetFullFramework.Tests
[2021-04-08T02:26:03.218Z]   Passed Elastic.Apm.AspNetFullFramework.Tests.CentralConfigTests+MaxSpansAndSampleRateTests.MaxSpans_invalid_value [9 s]
[2021-04-08T02:26:03.218Z] 
[2021-04-08T02:26:03.218Z] JunitXML Logger - Results File: C:\Users\jenkins\workspace\net_apm-agent-dotnet-mbp_PR-1247\apm-agent-dotnet\test\Elastic.Apm.AspNetFullFramework.Tests\junit-NETCoreApp21-Elastic.Apm.AspNetFullFramework.Tests.xml
[2021-04-08T02:26:03.218Z] 
[2021-04-08T02:26:03.218Z] Test Run Failed.
[2021-04-08T02:26:03.218Z] Total tests: 115
[2021-04-08T02:26:03.218Z]      Passed: 114
[2021-04-08T02:26:03.218Z]      Failed: 1
[2021-04-08T02:26:03.218Z]  Total time: 19.3249 Minutes
[2021-04-08T02:26:03.218Z] 
[2021-04-08T02:26:03.218Z] Build FAILED.
[2021-04-08T02:26:03.218Z]     0 Warning(s)
[2021-04-08T02:26:03.218Z]     0 Error(s)
[2021-04-08T02:26:03.218Z] 
[2021-04-08T02:26:03.218Z] Time Elapsed 00:19:20.38
[2021-04-08T02:26:04.181Z] Post stage
[2021-04-08T02:26:04.195Z] Running in C:\Users\jenkins\workspace\net_apm-agent-dotnet-mbp_PR-1247\apm-agent-dotnet
[2021-04-08T02:26:04.221Z] Archiving artifacts
[2021-04-08T02:26:05.033Z] Recording test results
[2021-04-08T02:26:07.748Z] [Checks API] No suitable checks publisher found.
[2021-04-08T02:26:07.831Z] Post stage
[2021-04-08T02:26:07.853Z] [WS-CLEANUP] Deleting project workspace...
[2021-04-08T02:26:07.853Z] [WS-CLEANUP] Deferred wipeout is disabled by the job configuration...
[2021-04-08T02:26:14.010Z] [WS-CLEANUP] done
[2021-04-08T02:26:14.118Z] Failed in branch Windows .NET Framework
[2021-04-08T02:26:14.234Z] Stage "Release to feedz.io" skipped due to earlier failure(s)
[2021-04-08T02:26:14.292Z] Stage "Release" skipped due to earlier failure(s)
[2021-04-08T02:26:14.317Z] Stage "Release" skipped due to earlier failure(s)
[2021-04-08T02:26:14.373Z] Stage "Release" skipped due to earlier failure(s)
[2021-04-08T02:26:14.452Z] Stage "AfterRelease" skipped due to earlier failure(s)
[2021-04-08T02:26:14.474Z] Stage "AfterRelease" skipped due to earlier failure(s)
[2021-04-08T02:26:14.777Z] Running on worker-1225339 in /var/lib/jenkins/workspace/net_apm-agent-dotnet-mbp_PR-1247
[2021-04-08T02:26:14.836Z] [INFO] getVaultSecret: Getting secrets
[2021-04-08T02:26:15.084Z] Masking supported pattern matches of $VAULT_ADDR or $VAULT_ROLE_ID or $VAULT_SECRET_ID
[2021-04-08T02:26:17.179Z] + chmod 755 generate-build-data.sh
[2021-04-08T02:26:17.179Z] + ./generate-build-data.sh https://apm-ci.elastic.co/blue/rest/organizations/jenkins/pipelines/apm-agent-dotnet/apm-agent-dotnet-mbp/PR-1247/ https://apm-ci.elastic.co/blue/rest/organizations/jenkins/pipelines/apm-agent-dotnet/apm-agent-dotnet-mbp/PR-1247/runs/9 FAILURE 3618260
[2021-04-08T02:26:17.180Z] INFO: curl https://apm-ci.elastic.co/blue/rest/organizations/jenkins/pipelines/apm-agent-dotnet/apm-agent-dotnet-mbp/PR-1247/runs/9/steps/?limit=10000 -o steps-info.json
[2021-04-08T02:26:17.879Z] INFO: curl https://apm-ci.elastic.co/blue/rest/organizations/jenkins/pipelines/apm-agent-dotnet/apm-agent-dotnet-mbp/PR-1247/runs/9/tests/?status=FAILED -o tests-errors.json

@AlexanderWert AlexanderWert added this to the 7.13 milestone Apr 1, 2021
This commit captures sending messages to, and receiving messages
from Azure Queue Storage.
Move common functionality for working with Terraform and Azure credentials
to Elastic.Apm.Test.Utilities.
This commit captures spans for the following Azure storage
events

- Create container
- Delete container
- GetBlobs in container
- Create page blob
- Upload block blob
- Upload page blob pages
- Download blob
- Download streaming blob
- Copy from URI into blob
- Delete blob
Move constants, mark types internal, add to Elastic.Apm.NetCoreAll
@russcam
Copy link
Contributor Author

russcam commented Apr 7, 2021

This is ready for review. It instruments the new Azure storage packages, with #1251 as a proposal to instrument the previous Azure storage packages.

@russcam russcam requested a review from gregkalapos April 7, 2021 01:25
Copy link
Contributor

@gregkalapos gregkalapos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👍 I found some small things - other than those I think it's ready to merge.

docs/setup.asciidoc Outdated Show resolved Hide resolved

protected override void HandleOnNext(KeyValuePair<string, object> kv)
{
Logger.Trace()?.Log("Called with key: `{DiagnosticEventKey}'", kv.Key);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit off-topic comment, but maybe we could move this (and the same logs from the other new listeners) into DiagnosticListenerBase - we do this logging in lots of types that derive from DiagnosticListenerBase - we could do this once in the base class.

Doesn't have to be in this PR - this was just a thought when I looked at this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, be good to move to the base class. Will open a follow up PR to do so.

return;
}

if (!(kv.Value is Activity activity))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Activity.Id according to the API could be null - it's very hard for me to imagine that, but to be on the safe side I think null-checking for activity.Id would be better, something like this:

Suggested change
if (!(kv.Value is Activity activity))
if (!(kv.Value is Activity activity) || string.IsNullOrEmpty(activity.Id))

We have the same in the Elastic.Apm.Azure.ServiceBus and at other places here (also in the OnStop and OnException methods).

Other nice thing about null-checking these is that it'll make Rider code analysis happy (currently it shows this as warning for me).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An Activity is assigned an id when it's started and Activity.Current validates that the value passed to the setter is either a null value or a started Activity that hasn't finished [1] [2].

Where the Activity is retrieved from the diagnostic event, I think we're safe to assume that the library has started the activity, and where the Activity is retrieved from Activity.Current, I think we can also assume that this is OK because it will either be the activity from the diagnostic event, or an activity started by the agent in case of a transaction.

{
case "BlobContainerClient.Create.Start":
case "PageBlobClient.Create.Start":
OnStart(kv, "Create");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason we use capital letter here ("Create")? This will end up being the action for the span and I think we use lower case at other places. Definitely won't break anything, just wondering if this is intentional.

There are some other instances in the PR as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the convention used for AWS S3 span names (the spec says operation name is camel case, but I think this should read pascal case based on the examples for S3, DynamoDB and the agent weekly conversations). Note that the span action is lowercase.

I don't really have an opinion about which might be best to use for this. Happy to follow up afterwards

@russcam
Copy link
Contributor Author

russcam commented Apr 8, 2021

The one failed test doesn't look related to these changes so merging in.

@russcam russcam merged commit 5db57e6 into elastic:master Apr 8, 2021
@russcam russcam deleted the feature/azure-storage branch April 8, 2021 02:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[META 410] Instrumentation for Azure Queue [META 410] Instrumentation for Azure Storage
4 participants