diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 1799bc96c4..488cad2e31 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -92,6 +92,7 @@ "northcentralus", "NTSTATUS", "okhttp", + "opentelemetry", "otel", "PBYTE", "pdbs", diff --git a/doc/DistributedTracing.md b/doc/DistributedTracing.md new file mode 100644 index 0000000000..ecf6e3be3f --- /dev/null +++ b/doc/DistributedTracing.md @@ -0,0 +1,250 @@ +--- +# cspell:words openetelemetry +--- +# Distributed Tracing in the C++ SDK + +Azure has adopted [W3C Distributed Tracing](https://www.w3.org/TR/trace-context/) as a paradigm for correlating +requests from clients across multiple services. + +This document explains how the Azure C++ SDK implements distributed tracing, how clients integrate with distributed tracing, how +services should integrate with distributed tracing and finally how the network pipeline and other functionality should +integrate with distributed tracing. + +## Tracing Overview + +The Azure SDK for C++ Tracing APIs are modeled after the opentelemetry-cpp API surface defined in the [OpenTelemetry Tracing Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md). +Additional architectural information about OpenTelemetry can be found in [OpenTelemetry Concepts](https://opentelemetry.io/docs/concepts/). + +There are three major components which the Azure SDK components interact with: + +- `TracerProvider` - this is a factory which creates `Tracer` objects. +- `Tracer` - this is a factory which creates `Span` objects. +- `Span` - Span objects are the APIs which allow tracing an operation. +Each `span` has a name, a type and a "status". `Spans` also contain "attributes" and "events" which describe an operation. + +There is typically a single `TracerProvider` for each application, and for the Azure SDK, each +service will have a `Tracer` implementation which creates `Span` objects for each service client. + +A `Span` can be considered a "unit of work" for a service. Each service method (method which calls into the service) will have a single `Span` reflecting the client method which +was called. + +`Span`'s are hierarchical and each span can have multiple children (each `Span` can only have a single parent). The typical way that this manifests itself during a +service method call is: + +- Service Method "MyServiceMethod" creates a span named "MyServiceMethod" and starts an HTTP request to communicate with the service. + - The HTTP pipeline (specifically the `RequestActivityPolicy`) will create a child `span` under the service method `span` named `"HTTP #0"`. This span + + reflects the HTTP call into the service. + - If the HTTP call needs to be retried, the existing `span` will be closed an a new span named `HTTP #1` will be created for the retry. + +## Distributed Tracing Client Integration + +Applications which wish to integrate Distributed Tracing are strongly encouraged +to use the [opentelemetry-cpp](https://github.com/open-telemetry/opentelemetry-cpp) vcpkg package. + +There are numerous examples on the OpenTelemetry web site which demonstrate how to integrate +opentelemetry into a customer application and integrate the generated traces +with Azure monitoring infrastructure such as Geneva Monitoring. + +Following the examples from opentelemetry-cpp, the following can be used +to establish an OpenTelemetry exporter which logs to the console or to an +in-memory logger. + +```c++ +opentelemetry::nostd::shared_ptr +CreateOpenTelemetryProvider() +{ +#if USE_MEMORY_EXPORTER + auto exporter = std::make_unique(); +#else + auto exporter = std::make_unique(); +#endif + + // simple processor + auto simple_processor = std::unique_ptr( + new opentelemetry::sdk::trace::SimpleSpanProcessor(std::move(exporter))); + + auto always_on_sampler = std::unique_ptr( + new opentelemetry::sdk::trace::AlwaysOnSampler); + + auto resource_attributes = opentelemetry::sdk::resource::ResourceAttributes{ + {"service.name", "telemetryTest"}, {"service.instance.id", "instance-1"}}; + auto resource = opentelemetry::sdk::resource::Resource::Create(resource_attributes); + // Create using SDK configurations as parameter + return opentelemetry::nostd::shared_ptr( + new opentelemetry::sdk::trace::TracerProvider( + std::move(simple_processor), resource, std::move(always_on_sampler))); +} +``` + +Other exporters exist to export to [Jaeger](https://github.com/open-telemetry/opentelemetry-cpp/tree/main/exporters/jaeger), +[Windows ETW](https://github.com/open-telemetry/opentelemetry-cpp/tree/main/exporters/etw) and others. + +Once the `opentelemetry::trace::TracerProvider` has been created, The client needs to create a new `Azure::Core::Tracing::OpenTelemetry::OpenTelemetryProvider` which +functions as an abstract class integration between OpenTelemetry and Azure Core: + +```c++ +std::shared_ptr traceProvider + = std::make_shared(CreateOpenTelemetryProvider()); +``` + +To finish the integration with Azure clients, there are two mechanisms to integrate OpenTelemetry into a client application: + +1) `Azure::Core::Context` integration. +1) Service Client Options integration. + +### Integrate an OpenTelemetryProvider via the ApplicationContext + +To integrate OpenTelemetry for all Azure Clients in the application, the customer can call `Azure::Core::Context::ApplicationContext.SetTracerProvider` to establish the +tracer provider for the application. + +```c++ + Azure::Core::Context::ApplicationContext.SetTracerProvider(provider); +``` + +### Integrate an OpenTelemetryProvider via Service ClientOptions + +While using the ApplicationContext is the simplest mechanism for integration OpenTelemetry with a customer application, there may be times the customer needs more flexibility when creating service clients. +To enable customers to further customize how tracing works, the application can set the `Telemetry.TracingProvider` field in the service client options, which will establish the tracer provider used by +the service client. + +```c++ +auto tracerProvider(CreateOpenTelemetryProvider()); +auto provider(std::make_shared(tracerProvider)); + +ServiceClientOptions clientOptions; +clientOptions.Telemetry.TracingProvider = provider; +clientOptions.Telemetry.ApplicationId = "MyApplication"; +ServiceClient myServiceClient(clientOptions); +``` + +## Distributed Tracing Service Integration + +There are two steps needed to integrate Distributed Tracing with a Service Client. + +1. Add a `DiagnosticTracingFactory` object to the ServiceClient object +1. Update each service method as follows: + 1. Add a call to the `CreateSpan` method on the diagnostic tracing factory. This will create a new span for the client operation. + 1. Call `SetStatus` on the created span when the service method successfully completes. + 1. Wrap the client method code with a try/catch handler which catches exceptions and call AddEvent with the value of the exception. + +### Add a `DiagnosticTracingFactory` to the serviceClient class + +To add a new `DiagnosticTracingFactory` to the client, simply add the class as a member: + +```c++ + Azure::Core::Tracing::_internal::DiagnosticTracingFactory m_tracingFactory; + +``` + +And construct the new tracing factory in the service constructor: + +```c++ + explicit ServiceClient(ServiceClientOptions const& clientOptions = ServiceClientOptions{}) + : m_tracingFactory(clientOptions, "Azure.Core.OpenTelemetry.Test.Service", PackageVersion::ToString()) + ``` + +### Update Each Service Method + + There are three methods of interest when updating the service method: + + 1. `DiagnosticTracingFactory::CreateSpan` - this creates and returns a `Span` and `Context` object for the service method. The returned Context object must be used for subsequent service operations. + 1. `Span::AddEvent(std::exception&)` - This registers the exception with the distributed tracing infrastructure. + 1. `Span::SetStatus` - This sets the status of the operation in the trace. + + ```c++ + Azure::Response ServiceMethod( + std::string const&, + Azure::Core::Context const& context = Azure::Core::Context{}) + { + // Create a new context and span for this request. + auto contextAndSpan = m_tracingFactory.CreateSpan( + "ServiceMethod", Azure::Core::Tracing::_internal::SpanKind::Internal, context); + + // contextAndSpan.first is the new context for the operation. + // contextAndSpan.second is the new span for the operation. + + try + { + // + Azure::Core::Http::Request requestToSend( + HttpMethod::Get, Azure::Core::Url("")); + + std::unique_ptr response + = m_pipeline->Send(requestToSend, contextAndSpan.first); + contextAndSpan.second.SetStatus(Azure::Core::Tracing::_internal::SpanStatus::Ok); + return Azure::Response("", std::move(response)); + } + catch (std::exception const& ex) + { + // Register that the exception has happened and that the span is now in error. + contextAndSpan.second.AddEvent(ex); + contextAndSpan.second.SetStatus(Azure::Core::Tracing::_internal::SpanStatus::Error); + throw; + } + + // When contextAndSpan.second goes out of scope, it ends the span, which will record it. + } +}; +``` + +## Implementation Details + +### Distributed Tracing components + +In order to maintain flexibility, the opentelemetry-cpp APIs are implemented in a separate package - azure-core-tracing-opentelemetry. +This is consistent with how opentelemetry is distributed for +the other Azure SDKs. + +The Azure Core API surface interacts with a set of pure virtual base classes (aka "interfaces") in +the `Azure::Core::Tracing` and `Azure::Core::Tracing::_internal` namespace. These allow a level of separation +between the Azure Core API surface and the OpenTelemetry API surface - an alternative tracing mechanism needs +to provide APIs consistent with the `Azure::Core::Tracing` APIs. + +The azure-core-tracing-openetelemetry-cpp package implements a set of APIs in the `Azure::Core::Tracing::OpenTelemetry` +and `Azure::Core::Tracing::OpenTelemetry::_detail` namespace. These provide an Azure Core compatable API surface for distributed tracing. + +The core service client interface is the `DiagnosticTracingFactory` class which implements two APIs: `CreateSpan` and +`CreateSpanFromContext`. `CreateSpan` is intended to be used by service methods which have direct access to a +`DiagnosticTracingFactory` object, `CreateSpanFromContext` in intended to be used from code which does NOT have +direct access to the `DiagnosticTracingFactory`. + +The final significant piece of the distributed tracing infrastructure is the `RequestActivityPolicy` - this policy MUST be +inserted into the HTTP pipeline AFTER the `RetryPolicy`. It is responsible for creating the span associated with the HTTP request, it will +also propagate the W3C distributed tracing headers from the span into the HTTP request. + +### Generated traces + +The Azure standards for distributed tracing are define in [Azure Distributed Tracing Conventions](https://github.com/Azure/azure-sdk/blob/main/docs/tracing/distributed-tracing-conventions.md). +The actual tracing elements generated by Azure services are defined in [Azure Tracing Conventions YAML](https://github.com/Azure/azure-sdk/blob/main/docs/tracing/distributed-tracing-conventions.yml). + +In summary, these are the traces and attributes which should be generated +for azure services: + +#### Spans + +The distributed tracing standards define the following traces: + +##### Public APIs + +All public APIs MUST create a span which will describes the API. +The name of the span MUST be the API name. + +##### HTTP Calls + +Each HTTP request sent to the service MUST create a span describing the request to the service. +The name of the span MUST be of the form `HTTP #`. + +#### Attributes + +Generated traces have the following attributes: + +| Attribute Name | Semantics | Where Used +|-----------|--------|------- +| `az.namespace` |Namespace of the azure service request| All spans. +| `http.method`| HTTP Method ("GET", "PUT", etc)| HTTP Spans. +| `http.url`| URL being retrieved (sanitized)| HTTP Spans. +| `http.status_code` | HTTP status code returned by the service | HTTP Spans. +| `http.user_agent` | The value of the `User-Agent` HTTP header sent to the service | HTTP Spans. +| `requestId` | The value of the `x-ms-client-request-id` header sent by the client | HTTP Spans. +| `serviceRequestId` | The value -f the `x-ms-request-id` sent by the server | HTTP Spans. diff --git a/eng/pipelines/templates/stages/platform-matrix-live.json b/eng/pipelines/templates/stages/platform-matrix-live.json index 07787623a9..151c57a2ae 100644 --- a/eng/pipelines/templates/stages/platform-matrix-live.json +++ b/eng/pipelines/templates/stages/platform-matrix-live.json @@ -73,15 +73,15 @@ "Win_x86_with_unit_test_winHttp": { "VcpkgInstall": "openssl", "CMAKE_GENERATOR_PLATFORM": "Win32", - "CmakeArgs": " -DBUILD_TESTING=ON -DRUN_LONG_UNIT_TESTS=ON -DBUILD_PERFORMANCE_TESTS=ON ", + "CmakeArgs": " -DBUILD_TESTING=ON -DRUN_LONG_UNIT_TESTS=ON -DBUILD_PERFORMANCE_TESTS=ON -DMSVC_USE_STATIC_CRT=ON ", "VCPKG_DEFAULT_TRIPLET": "x86-windows-static", "WindowsCtestConfig": "-C Release", "BuildArgs": "-v --parallel 8 --config Release" }, - "Win_x86_no_rtti_whit_unit_test": { + "Win_x86_no_rtti_with_unit_test": { "VcpkgInstall": "libxml2 openssl", "CMAKE_GENERATOR_PLATFORM": "Win32", - "CmakeArgs": " -DBUILD_RTTI=OFF -DBUILD_TESTING=ON -DBUILD_SAMPLES=ON", + "CmakeArgs": " -DBUILD_RTTI=OFF -DBUILD_TESTING=ON -DBUILD_SAMPLES=ON -DMSVC_USE_STATIC_CRT=ON", "VCPKG_DEFAULT_TRIPLET": "x86-windows-static", "WindowsCtestConfig": "-C Release", "BuildArgs": "-v --parallel 8 --config Release" @@ -89,32 +89,49 @@ "Win_x86_with_unit_test_libcurl": { "CMAKE_GENERATOR_PLATFORM": "Win32", "VCPKG_DEFAULT_TRIPLET": "x86-windows-static", - "CmakeArgs": " -DBUILD_TRANSPORT_CURL=ON -DBUILD_TESTING=ON -DRUN_LONG_UNIT_TESTS=ON -DBUILD_PERFORMANCE_TESTS=ON ", + "CmakeArgs": " -DBUILD_TRANSPORT_CURL=ON -DBUILD_TESTING=ON -DRUN_LONG_UNIT_TESTS=ON -DBUILD_PERFORMANCE_TESTS=ON -DMSVC_USE_STATIC_CRT=ON ", "BuildArgs": "-v --parallel 8" }, - "Win_x64_with_unit_test_winHttp": { + "Win_x64_with_json_unit_test_winHttp": { "VcpkgInstall": "openssl", "CMAKE_GENERATOR_PLATFORM": "x64", - "CmakeArgs": " -DBUILD_TESTING=ON -DRUN_LONG_UNIT_TESTS=ON -DBUILD_PERFORMANCE_TESTS=ON ", + "CmakeArgs": " -DBUILD_TESTING=ON -DRUN_LONG_UNIT_TESTS=ON -DBUILD_PERFORMANCE_TESTS=ON -DDISABLE_AZURE_CORE_OPENTELEMETRY=ON ", "BuildArgs": "-v --parallel 8 --config Release", "AZURE_CORE_ENABLE_JSON_TESTS": 1, "VCPKG_DEFAULT_TRIPLET": "x64-windows-static", "WindowsCtestConfig": "-C Release" }, - "Win_x64_with_unit_samples_winHttp": { + "Win_x64_with_json_unit_samples_winHttp": { "VcpkgInstall": "openssl", "VCPKG_DEFAULT_TRIPLET": "x64-windows-static", "CMAKE_GENERATOR_PLATFORM": "x64", - "CmakeArgs": " -DBUILD_TESTING=ON -DRUN_LONG_UNIT_TESTS=ON -DBUILD_PERFORMANCE_TESTS=ON -DBUILD_SAMPLES=ON ", + "CmakeArgs": " -DBUILD_TESTING=ON -DRUN_LONG_UNIT_TESTS=ON -DBUILD_PERFORMANCE_TESTS=ON -DBUILD_SAMPLES=ON -DDISABLE_AZURE_CORE_OPENTELEMETRY=ON ", "BuildArgs": "-v --parallel 8 --config Release", "AZURE_CORE_ENABLE_JSON_TESTS": 1, "RunSamples": 1, "WindowsCtestConfig": "-C Release" }, + "Win_x64_with_unit_test_winHttp": { + "VcpkgInstall": "openssl", + "CMAKE_GENERATOR_PLATFORM": "x64", + "CmakeArgs": " -DBUILD_TESTING=ON -DRUN_LONG_UNIT_TESTS=ON -DBUILD_PERFORMANCE_TESTS=ON -DMSVC_USE_STATIC_CRT=ON ", + "BuildArgs": "-v --parallel 8 --config Release", + "VCPKG_DEFAULT_TRIPLET": "x64-windows-static", + "WindowsCtestConfig": "-C Release" + }, + "Win_x64_with_unit_samples_winHttp": { + "VcpkgInstall": "openssl", + "VCPKG_DEFAULT_TRIPLET": "x64-windows-static", + "CMAKE_GENERATOR_PLATFORM": "x64", + "CmakeArgs": " -DBUILD_TESTING=ON -DRUN_LONG_UNIT_TESTS=ON -DBUILD_PERFORMANCE_TESTS=ON -DBUILD_SAMPLES=ON -DMSVC_USE_STATIC_CRT=ON ", + "BuildArgs": "-v --parallel 8 --config Release", + "RunSamples": 1, + "WindowsCtestConfig": "-C Release" + }, "Win_x64_with_unit_test_libcurl": { "VCPKG_DEFAULT_TRIPLET": "x64-windows-static", "CMAKE_GENERATOR_PLATFORM": "x64", - "CmakeArgs": " -DBUILD_TRANSPORT_CURL=ON -DBUILD_TESTING=ON -DRUN_LONG_UNIT_TESTS=ON -DBUILD_PERFORMANCE_TESTS=ON ", + "CmakeArgs": " -DBUILD_TRANSPORT_CURL=ON -DBUILD_TESTING=ON -DRUN_LONG_UNIT_TESTS=ON -DBUILD_PERFORMANCE_TESTS=ON -DMSVC_USE_STATIC_CRT=ON ", "BuildArgs": "-v --parallel 8 --config Release", "WindowsCtestConfig": "-C Release" }, @@ -122,7 +139,7 @@ "VcpkgInstall": "curl[winssl] openssl", "VCPKG_DEFAULT_TRIPLET": "x64-windows-static", "CMAKE_GENERATOR_PLATFORM": "x64", - "CmakeArgs": " -DBUILD_TRANSPORT_CURL=ON -DBUILD_TESTING=ON -DRUN_LONG_UNIT_TESTS=ON -DBUILD_PERFORMANCE_TESTS=ON -DBUILD_SAMPLES=ON ", + "CmakeArgs": " -DBUILD_TRANSPORT_CURL=ON -DBUILD_TESTING=ON -DRUN_LONG_UNIT_TESTS=ON -DBUILD_PERFORMANCE_TESTS=ON -DBUILD_SAMPLES=ON -DMSVC_USE_STATIC_CRT=ON ", "BuildArgs": "-v --parallel 8 --config Release", "RunSamples": 1, "WindowsCtestConfig": "-C Release" diff --git a/sdk/attestation/azure-security-attestation/README.md b/sdk/attestation/azure-security-attestation/README.md index 01b6750642..b191f1a48b 100644 --- a/sdk/attestation/azure-security-attestation/README.md +++ b/sdk/attestation/azure-security-attestation/README.md @@ -1,3 +1,7 @@ +--- +# cspell:words opentelemetry +--- + # Azure Attestation Package client library for C++ Microsoft Azure Attestation is a unified solution for remotely verifying the trustworthiness of a platform and integrity of the binaries running inside it. The service supports attestation of the platforms backed by Trusted Platform Modules (TPMs) alongside the ability to attest to the state of Trusted Execution Environments (TEEs) such as Intel(tm) Software Guard Extensions (SGX) enclaves and Virtualization-based Security (VBS) enclaves. diff --git a/sdk/attestation/test-resources.json b/sdk/attestation/test-resources.json index 987b5d769f..674f208a82 100644 --- a/sdk/attestation/test-resources.json +++ b/sdk/attestation/test-resources.json @@ -42,7 +42,7 @@ "metadata": { "description": "The application client secret used to run tests." } - }, + } }, "variables": { "isolatedTenantName": "[concat('cp', concat(parameters('baseName'), 'iso'))]", @@ -66,7 +66,7 @@ "type": "Microsoft.Attestation/attestationProviders", "apiVersion": "2020-10-01", "name": "[variables('aadTenantName')]", - "location": "[parameters('location')]", + "location": "[parameters('location')]" }, { "type": "Microsoft.Attestation/attestationProviders", diff --git a/sdk/core/azure-core-tracing-opentelemetry/CHANGELOG.md b/sdk/core/azure-core-tracing-opentelemetry/CHANGELOG.md index 0877171bac..3f2cb968cb 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/CHANGELOG.md +++ b/sdk/core/azure-core-tracing-opentelemetry/CHANGELOG.md @@ -2,4 +2,6 @@ ## 1.0.0-beta.1 (Unreleased) +### Features Added + - Initial release diff --git a/sdk/core/azure-core-tracing-opentelemetry/CMakeLists.txt b/sdk/core/azure-core-tracing-opentelemetry/CMakeLists.txt index e0dfb353de..4f81df1937 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/CMakeLists.txt +++ b/sdk/core/azure-core-tracing-opentelemetry/CMakeLists.txt @@ -56,7 +56,7 @@ target_include_directories( add_library(Azure::azure-core-tracing-opentelemetry ALIAS azure-core-tracing-opentelemetry) # coverage. Has no effect if BUILD_CODE_COVERAGE is OFF -create_code_coverage(core-tracing-opentelemetry azure-core-tracing-opentelemetry azure-core-tracing-opentelemetry-test "tests?/*;samples?/*") +create_code_coverage(core azure-core-tracing-opentelemetry azure-core-tracing-opentelemetry-test "tests?/*;samples?/*") target_link_libraries(azure-core-tracing-opentelemetry INTERFACE Threads::Threads) diff --git a/sdk/core/azure-core-tracing-opentelemetry/inc/azure/core/tracing/opentelemetry/opentelemetry.hpp b/sdk/core/azure-core-tracing-opentelemetry/inc/azure/core/tracing/opentelemetry/opentelemetry.hpp index e840cfd2d4..7ac27cdbe0 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/inc/azure/core/tracing/opentelemetry/opentelemetry.hpp +++ b/sdk/core/azure-core-tracing-opentelemetry/inc/azure/core/tracing/opentelemetry/opentelemetry.hpp @@ -11,6 +11,7 @@ #pragma warning(push) #pragma warning(disable : 4100) #pragma warning(disable : 4244) +#pragma warning(disable : 6323) // Disable "Use of arithmetic operator on Boolean type" warning. #endif #include #include @@ -114,6 +115,8 @@ namespace Azure { namespace Core { namespace Tracing { namespace OpenTelemetry { virtual void AddAttributes( Azure::Core::Tracing::_internal::AttributeSet const& attributeToAdd) override; + virtual void AddAttribute(std::string const& attributeName, std::string const& attributeValue) + override; /** * Add an Event to the span. An event is identified by a name and an optional set of @@ -129,6 +132,14 @@ namespace Azure { namespace Core { namespace Tracing { namespace OpenTelemetry { Azure::Core::Tracing::_internal::SpanStatus const& status, std::string const& statusMessage) override; + /** + * @brief Propogate information from the current span to the HTTP request headers. + * + * @param request HTTP Request to the service. If there is an active tracing span, this will + * add required headers to the HTTP Request. + */ + virtual void PropagateToHttpHeaders(Azure::Core::Http::Request& request) override; + opentelemetry::trace::SpanContext GetContext() { return m_span->GetContext(); } }; diff --git a/sdk/core/azure-core-tracing-opentelemetry/src/opentelemetry.cpp b/sdk/core/azure-core-tracing-opentelemetry/src/opentelemetry.cpp index 8f0cd0b480..e3968e0837 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/src/opentelemetry.cpp +++ b/sdk/core/azure-core-tracing-opentelemetry/src/opentelemetry.cpp @@ -1,5 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT #include "azure/core/tracing/opentelemetry/opentelemetry.hpp" +#include #include #include #include @@ -9,7 +12,9 @@ #pragma warning(push) #pragma warning(disable : 4100) #pragma warning(disable : 4244) +#pragma warning(disable : 6323) #endif +#include #include #include #if defined(_MSC_VER) @@ -185,5 +190,67 @@ namespace Azure { namespace Core { namespace Tracing { namespace OpenTelemetry { m_span->SetStatus(statusCode, statusMessage); } + void OpenTelemetrySpan::AddAttribute( + std::string const& attributeName, + std::string const& attributeValue) + { + m_span->SetAttribute(attributeName, opentelemetry::common::AttributeValue(attributeValue)); + } + + /** + * @brief Text map propagator used to read or write properties from an HTTP request. + * + * @details OpenTelemetry defines a `TextMapCarrier` class as a class which allows reading and + * writing to a map of text elements. The OpenTelemetry + * [HttpTraceContext](https://opentelemetry-cpp.readthedocs.io/en/latest/otel_docs/classopentelemetry_1_1trace_1_1propagation_1_1HttpTraceContext.html) + * uses a TextMapCarrier to propogate the required HTTP headers from an OpenTelemetry context + * into an HTTP request. + */ + class HttpRequestTextMapPropagator + : public opentelemetry::context::propagation::TextMapCarrier { + Azure::Core::Http::Request& m_request; + // Inherited via TextMapCarrier + + /** @brief Retrieves the value of an HTTP header from the request. + */ + virtual opentelemetry::nostd::string_view Get( + opentelemetry::nostd::string_view key) const noexcept override + { + auto header = m_request.GetHeader(std::string(key)); + if (header) + { + return header.Value(); + } + return std::string(); + } + + /** @brief Sets the value of an HTTP header in the request. + */ + virtual void Set( + opentelemetry::nostd::string_view key, + opentelemetry::nostd::string_view value) noexcept override + { + m_request.SetHeader(std::string(key), std::string(value)); + } + + public: + HttpRequestTextMapPropagator(Azure::Core::Http::Request& request) : m_request(request) {} + }; + + void OpenTelemetrySpan::PropagateToHttpHeaders(Azure::Core::Http::Request& request) + { + if (m_span) + { + HttpRequestTextMapPropagator propagator(request); + + // Establish the current runtime context from the span. + auto scope = opentelemetry::trace::Tracer::WithActiveSpan(m_span); + auto currentContext = opentelemetry::context::RuntimeContext::GetCurrent(); + + // And inject all required headers into the Request. + opentelemetry::trace::propagation::HttpTraceContext().Inject(propagator, currentContext); + } + } + } // namespace _detail }}}} // namespace Azure::Core::Tracing::OpenTelemetry diff --git a/sdk/core/azure-core-tracing-opentelemetry/test/ut/azure_core_otel_test.cpp b/sdk/core/azure-core-tracing-opentelemetry/test/ut/azure_core_otel_test.cpp index 4d1735581b..d51ad72513 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/test/ut/azure_core_otel_test.cpp +++ b/sdk/core/azure-core-tracing-opentelemetry/test/ut/azure_core_otel_test.cpp @@ -11,6 +11,7 @@ #pragma warning(push) #pragma warning(disable : 4100) #pragma warning(disable : 4244) +#pragma warning(disable : 6323) // Disable "Use of arithmetic operator on Boolean type" warning. #endif #include #include diff --git a/sdk/core/azure-core-tracing-opentelemetry/test/ut/service_support_test.cpp b/sdk/core/azure-core-tracing-opentelemetry/test/ut/service_support_test.cpp index c48f4abfda..38b0a42d17 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/test/ut/service_support_test.cpp +++ b/sdk/core/azure-core-tracing-opentelemetry/test/ut/service_support_test.cpp @@ -4,6 +4,7 @@ #define USE_MEMORY_EXPORTER 1 #include "azure/core/internal/tracing/service_tracing.hpp" #include "azure/core/tracing/opentelemetry/opentelemetry.hpp" +#include #include #include @@ -13,6 +14,7 @@ #pragma warning(push) #pragma warning(disable : 4100) #pragma warning(disable : 4244) +#pragma warning(disable : 6323) // Disable "Use of arithmetic operator on Boolean type" warning. #endif #include #include @@ -22,11 +24,17 @@ #include #include #include +#include #if defined(_MSC_VER) #pragma warning(pop) #endif #include #include +#include + +using namespace Azure::Core::Http::Policies; +using namespace Azure::Core::Http::Policies::_internal; +using namespace Azure::Core::Http; class CustomLogHandler : public opentelemetry::sdk::common::internal_log::LogHandler { void Handle( @@ -165,7 +173,7 @@ class OpenTelemetryServiceTests : public Azure::Core::Test::TestBase { switch (span->GetSpanKind()) { case opentelemetry::trace::SpanKind::kClient: - EXPECT_EQ(expectedKind, "internal"); + EXPECT_EQ(expectedKind, "client"); break; case opentelemetry::trace::SpanKind::kConsumer: EXPECT_EQ(expectedKind, "consumer"); @@ -207,7 +215,11 @@ class OpenTelemetryServiceTests : public Azure::Core::Test::TestBase { case opentelemetry::common::kTypeString: { EXPECT_TRUE(expectedAttributes[foundAttribute.first].is_string()); const auto& actualVal = opentelemetry::nostd::get(foundAttribute.second); - EXPECT_EQ(expectedAttributes[foundAttribute.first].get(), actualVal); + std::string expectedVal(expectedAttributes[foundAttribute.first].get()); + std::regex expectedRegex(expectedVal); + GTEST_LOG_(INFO) << "expected Regex: " << expectedVal << std::endl; + GTEST_LOG_(INFO) << "actual val: " << actualVal << std::endl; + EXPECT_TRUE(std::regex_match(actualVal, expectedRegex)); break; } case opentelemetry::common::kTypeDouble: { @@ -275,7 +287,8 @@ TEST_F(OpenTelemetryServiceTests, SimplestTest) Azure::Core::Tracing::_internal::DiagnosticTracingFactory serviceTrace( clientOptions, "my-service-cpp", "1.0b2"); - auto contextAndSpan = serviceTrace.CreateSpan("My API", {}); + auto contextAndSpan = serviceTrace.CreateSpan( + "My API", Azure::Core::Tracing::_internal::SpanKind::Internal, {}); EXPECT_FALSE(contextAndSpan.first.IsCancelled()); } } @@ -311,7 +324,8 @@ TEST_F(OpenTelemetryServiceTests, CreateWithExplicitProvider) clientOptions, "my-service", "1.0beta-2"); Azure::Core::Context clientContext; - auto contextAndSpan = serviceTrace.CreateSpan("My API", clientContext); + auto contextAndSpan = serviceTrace.CreateSpan( + "My API", Azure::Core::Tracing::_internal::SpanKind::Internal, clientContext); EXPECT_FALSE(contextAndSpan.first.IsCancelled()); } // Now let's verify what was logged via OpenTelemetry. @@ -321,6 +335,7 @@ TEST_F(OpenTelemetryServiceTests, CreateWithExplicitProvider) VerifySpan(spans[0], R"( { "name": "My API", + "kind": "internal", "attributes": { "az.namespace": "my-service" }, @@ -349,7 +364,8 @@ TEST_F(OpenTelemetryServiceTests, CreateWithImplicitProvider) clientOptions, "my-service", "1.0beta-2"); Azure::Core::Context clientContext; - auto contextAndSpan = serviceTrace.CreateSpan("My API", clientContext); + auto contextAndSpan = serviceTrace.CreateSpan( + "My API", Azure::Core::Tracing::_internal::SpanKind::Internal, clientContext); EXPECT_FALSE(contextAndSpan.first.IsCancelled()); } @@ -360,6 +376,7 @@ TEST_F(OpenTelemetryServiceTests, CreateWithImplicitProvider) VerifySpan(spans[0], R"( { "name": "My API", + "kind": "internal", "attributes": { "az.namespace": "my-service" }, @@ -383,6 +400,10 @@ TEST_F(OpenTelemetryServiceTests, NestSpans) Azure::Core::Context::ApplicationContext.SetTracerProvider(provider); + Azure::Core::Http::Request outerRequest( + HttpMethod::Post, Azure::Core::Url("https://www.microsoft.com")); + Azure::Core::Http::Request innerRequest( + HttpMethod::Post, Azure::Core::Url("https://www.microsoft.com")); { Azure::Core::_internal::ClientOptions clientOptions; clientOptions.Telemetry.ApplicationId = "MyApplication"; @@ -391,13 +412,17 @@ TEST_F(OpenTelemetryServiceTests, NestSpans) clientOptions, "my-service", "1.0beta-2"); Azure::Core::Context parentContext; - auto contextAndSpan = serviceTrace.CreateSpan("My API", parentContext); + auto contextAndSpan = serviceTrace.CreateSpan( + "My API", Azure::Core::Tracing::_internal::SpanKind::Client, parentContext); EXPECT_FALSE(contextAndSpan.first.IsCancelled()); parentContext = contextAndSpan.first; + contextAndSpan.second.PropagateToHttpHeaders(outerRequest); { - auto innerContextAndSpan = serviceTrace.CreateSpan("Nested API", parentContext); + auto innerContextAndSpan = serviceTrace.CreateSpan( + "Nested API", Azure::Core::Tracing::_internal::SpanKind::Server, parentContext); EXPECT_FALSE(innerContextAndSpan.first.IsCancelled()); + innerContextAndSpan.second.PropagateToHttpHeaders(innerRequest); } } // Now let's verify what was logged via OpenTelemetry. @@ -431,9 +456,73 @@ TEST_F(OpenTelemetryServiceTests, NestSpans) EXPECT_EQ("my-service", spans[1]->GetInstrumentationLibrary().GetName()); EXPECT_EQ("1.0beta-2", spans[0]->GetInstrumentationLibrary().GetVersion()); EXPECT_EQ("1.0beta-2", spans[1]->GetInstrumentationLibrary().GetVersion()); + + // The trace ID for the inner and outer requests must be the same, the parent-id/span-id must be + // different. + // + // Returns a 4 element array. + // Array[0] is the version of the TraceParent header. + // Array[1] is the trace-id of the TraceParent header. + // Array[2] is the parent-id/span-id of the TraceParent header. + // Array[3] is the trace-flags of the TraceParent header. + auto ParseTraceParent = [](const std::string& traceParent) -> std::array { + std::array returnedComponents; + std::string component; + size_t index = 0; + for (auto ch : traceParent) + { + if (ch != '-') + { + component.push_back(ch); + } + else + { + returnedComponents[index] = component; + component.clear(); + index += 1; + } + } + EXPECT_EQ(3ul, index); + returnedComponents[3] = component; + return returnedComponents; + }; + auto outerTraceId = ParseTraceParent(outerRequest.GetHeader("traceparent").Value()); + auto innerTraceId = ParseTraceParent(innerRequest.GetHeader("traceparent").Value()); + // Version should always match. + EXPECT_EQ(outerTraceId[0], innerTraceId[0]); + // Trace ID should always match. + EXPECT_EQ(outerTraceId[1], innerTraceId[1]); + // Span-Id should never match. + EXPECT_NE(outerTraceId[2], innerTraceId[2]); } } +namespace { +class NoOpPolicy final : public HttpPolicy { + std::function(Request&)> m_createResponse{}; + +public: + std::unique_ptr Clone() const override { return std::make_unique(*this); } + + std::unique_ptr Send(Request& request, NextHttpPolicy, Azure::Core::Context const&) + const override + { + if (m_createResponse) + { + return m_createResponse(request); + } + else + { + return std::make_unique(1, 1, HttpStatusCode::Ok, "Something"); + } + } + + NoOpPolicy() = default; + NoOpPolicy(std::function(Request&)> createResponse) + : HttpPolicy(), m_createResponse(createResponse){}; +}; +} // namespace + // Create a serviceTrace and span using a provider specified in the ClientOptions. class ServiceClientOptions : public Azure::Core::_internal::ClientOptions { public: @@ -443,23 +532,49 @@ class ServiceClientOptions : public Azure::Core::_internal::ClientOptions { class ServiceClient { private: ServiceClientOptions m_clientOptions; - Azure::Core::Tracing::_internal::DiagnosticTracingFactory m_serviceTrace; + Azure::Core::Tracing::_internal::DiagnosticTracingFactory m_tracingFactory; + std::unique_ptr m_pipeline; public: explicit ServiceClient(ServiceClientOptions const& clientOptions = ServiceClientOptions{}) - : m_serviceTrace(clientOptions, "Azure.Core.OpenTelemetry.Test.Service", "1.0.0.beta-2") + : m_tracingFactory(clientOptions, "Azure.Core.OpenTelemetry.Test.Service", "1.0.0.beta-2") { + std::vector> policies; + policies.emplace_back(std::make_unique( + "Azure.Core.OpenTelemetry.Test.Service", "1.0.0.beta-2", clientOptions.Telemetry)); + policies.emplace_back(std::make_unique()); + policies.emplace_back(std::make_unique(RetryOptions{})); + + // Add the request ID policy - this adds the x-ms-request-id attribute to the pipeline. + policies.emplace_back( + std::make_unique(Azure::Core::_internal::InputSanitizer{})); + + // Final policy - functions as the HTTP transport policy. + policies.emplace_back(std::make_unique([&](Request& request) { + // If the request is for port 12345, throw an exception. + if (request.GetUrl().GetPort() == 12345) + { + throw Azure::Core::RequestFailedException("it all goes wrong here."); + } + return std::make_unique(1, 1, HttpStatusCode::Ok, "Something"); + })); + + m_pipeline = std::make_unique(policies); } Azure::Response GetConfigurationString( std::string const& inputString, Azure::Core::Context const& context = Azure::Core::Context{}) { - auto contextAndSpan = m_serviceTrace.CreateSpan("GetConfigurationString", context); + auto contextAndSpan = m_tracingFactory.CreateSpan( + "GetConfigurationString", Azure::Core::Tracing::_internal::SpanKind::Internal, context); // + Azure::Core::Http::Request requestToSend( + HttpMethod::Get, Azure::Core::Url("https://www.microsoft.com/")); + std::unique_ptr response - = SendHttpRequest(false, contextAndSpan.first); + = m_pipeline->Send(requestToSend, contextAndSpan.first); // Reflect that the operation was successful. contextAndSpan.second.SetStatus(Azure::Core::Tracing::_internal::SpanStatus::Ok); @@ -468,47 +583,22 @@ class ServiceClient { // When contextAndSpan.second goes out of scope, it ends the span, which will record it. } - std::unique_ptr ActuallySendHttpRequest( - Azure::Core::Context const& context) - { - auto contextAndSpan - = Azure::Core::Tracing::_internal::DiagnosticTracingFactory::CreateSpanFromContext( - "HTTP GET#2", context); - - return std::make_unique( - 1, 1, Azure::Core::Http::HttpStatusCode::Ok, "OK"); - } - - std::unique_ptr SendHttpRequest( - bool throwException, - Azure::Core::Context const& context) - { - if (throwException) - { - throw Azure::Core::RequestFailedException("it all goes wrong here."); - } - - auto contextAndSpan - = Azure::Core::Tracing::_internal::DiagnosticTracingFactory::CreateSpanFromContext( - "HTTP GET#1", context); - - std::unique_ptr response - = ActuallySendHttpRequest(contextAndSpan.first); - - return std::make_unique( - 1, 1, Azure::Core::Http::HttpStatusCode::Ok, "OK"); - } - Azure::Response ApiWhichThrows( std::string const&, Azure::Core::Context const& context = Azure::Core::Context{}) { - auto contextAndSpan = m_serviceTrace.CreateSpan("ApiWhichThrows", context); + auto contextAndSpan = m_tracingFactory.CreateSpan( + "ApiWhichThrows", Azure::Core::Tracing::_internal::SpanKind::Internal, context); try { - auto rawResponse = SendHttpRequest(false, contextAndSpan.first); - return Azure::Response("", std::move(rawResponse)); + // + Azure::Core::Http::Request requestToSend( + HttpMethod::Get, Azure::Core::Url("https://www.microsoft.com/:12345/index.html")); + + std::unique_ptr response + = m_pipeline->Send(requestToSend, contextAndSpan.first); + return Azure::Response("", std::move(response)); } catch (std::exception const& ex) { @@ -541,15 +631,20 @@ TEST_F(OpenTelemetryServiceTests, ServiceApiImplementation) } // Now let's verify what was logged via OpenTelemetry. auto spans = m_spanData->GetSpans(); - EXPECT_EQ(3ul, spans.size()); + EXPECT_EQ(2ul, spans.size()); VerifySpan(spans[0], R"( { - "name": "HTTP GET#2", - "kind": "internal", + "name": "HTTP GET #0", + "kind": "client", "statusCode": "unset", "attributes": { - "az.namespace": "Azure.Core.OpenTelemetry.Test.Service" + "az.namespace": "Azure.Core.OpenTelemetry.Test.Service", + "http.method": "GET", + "http.url": "https://www.microsoft.com", + "requestId": ".*", + "http.user_agent": "MyApplication azsdk-cpp-Azure.Core.OpenTelemetry.Test.Service/1.0.0.beta-2.*", + "http.status_code": "200" }, "library": { "name": "Azure.Core.OpenTelemetry.Test.Service", @@ -558,20 +653,6 @@ TEST_F(OpenTelemetryServiceTests, ServiceApiImplementation) })"); VerifySpan(spans[1], R"( -{ - "name": "HTTP GET#1", - "kind": "internal", - "statusCode": "unset", - "attributes": { - "az.namespace": "Azure.Core.OpenTelemetry.Test.Service" - }, - "library": { - "name": "Azure.Core.OpenTelemetry.Test.Service", - "version": "1.0.0.beta-2" - } -})"); - - VerifySpan(spans[2], R"( { "name": "GetConfigurationString", "kind": "internal", diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md index ac1c85cb31..d8e42ac48f 100644 --- a/sdk/core/azure-core/CHANGELOG.md +++ b/sdk/core/azure-core/CHANGELOG.md @@ -3,7 +3,8 @@ ## 1.7.0-beta.1 (Unreleased) ### Features Added -Added implementation for Distributed Tracing. + +- Added prototypes and initial service support for Distributed Tracing. ### Breaking Changes diff --git a/sdk/core/azure-core/CMakeLists.txt b/sdk/core/azure-core/CMakeLists.txt index 5bc4145465..90857472f0 100644 --- a/sdk/core/azure-core/CMakeLists.txt +++ b/sdk/core/azure-core/CMakeLists.txt @@ -38,10 +38,10 @@ endif() if(BUILD_TRANSPORT_CURL) SET(CURL_TRANSPORT_ADAPTER_SRC + src/http/curl/curl.cpp src/http/curl/curl_connection_pool_private.hpp src/http/curl/curl_connection_private.hpp src/http/curl/curl_session_private.hpp - src/http/curl/curl.cpp ) SET(CURL_TRANSPORT_ADAPTER_INC inc/azure/core/http/curl_transport.hpp @@ -56,37 +56,39 @@ set( AZURE_CORE_HEADER ${CURL_TRANSPORT_ADAPTER_INC} ${WIN_TRANSPORT_ADAPTER_INC} + inc/azure/core.hpp + inc/azure/core/azure_assert.hpp + inc/azure/core/base64.hpp + inc/azure/core/case_insensitive_containers.hpp + inc/azure/core/context.hpp inc/azure/core/credentials/credentials.hpp inc/azure/core/credentials/token_credential_options.hpp inc/azure/core/cryptography/hash.hpp + inc/azure/core/datetime.hpp inc/azure/core/diagnostics/logger.hpp - inc/azure/core/http/policies/policy.hpp + inc/azure/core/dll_import_export.hpp + inc/azure/core/etag.hpp + inc/azure/core/exception.hpp inc/azure/core/http/http.hpp inc/azure/core/http/http_status_code.hpp + inc/azure/core/http/policies/policy.hpp inc/azure/core/http/raw_response.hpp inc/azure/core/http/transport.hpp + inc/azure/core/internal/client_options.hpp + inc/azure/core/internal/contract.hpp inc/azure/core/internal/cryptography/sha_hash.hpp inc/azure/core/internal/diagnostics/log.hpp + inc/azure/core/internal/environment.hpp + inc/azure/core/internal/extendable_enumeration.hpp inc/azure/core/internal/http/pipeline.hpp inc/azure/core/internal/io/null_body_stream.hpp inc/azure/core/internal/json/json.hpp inc/azure/core/internal/json/json_optional.hpp inc/azure/core/internal/json/json_serializable.hpp - inc/azure/core/internal/client_options.hpp - inc/azure/core/internal/contract.hpp - inc/azure/core/internal/environment.hpp - inc/azure/core/internal/extendable_enumeration.hpp inc/azure/core/internal/strings.hpp inc/azure/core/internal/tracing/service_tracing.hpp + inc/azure/core/internal/input_sanitizer.hpp inc/azure/core/io/body_stream.hpp - inc/azure/core/azure_assert.hpp - inc/azure/core/base64.hpp - inc/azure/core/case_insensitive_containers.hpp - inc/azure/core/context.hpp - inc/azure/core/datetime.hpp - inc/azure/core/dll_import_export.hpp - inc/azure/core/etag.hpp - inc/azure/core/exception.hpp inc/azure/core/match_conditions.hpp inc/azure/core/modified_conditions.hpp inc/azure/core/nullable.hpp @@ -99,41 +101,44 @@ set( inc/azure/core/tracing/tracing.hpp inc/azure/core/url.hpp inc/azure/core/uuid.hpp - inc/azure/core.hpp) + ) set( AZURE_CORE_SOURCE ${CURL_TRANSPORT_ADAPTER_SRC} ${WIN_TRANSPORT_ADAPTER_SRC} + src/azure_assert.cpp + src/base64.cpp + src/context.cpp src/cryptography/md5.cpp src/cryptography/sha_hash.cpp + src/datetime.cpp + src/environment.cpp + src/environment_log_level_listener.cpp + src/etag.cpp + src/exception.cpp src/http/bearer_token_authentication_policy.cpp src/http/http.cpp src/http/log_policy.cpp src/http/policy.cpp src/http/raw_response.cpp src/http/request.cpp + src/http/request_activity_policy.cpp src/http/retry_policy.cpp src/http/telemetry_policy.cpp src/http/transport_policy.cpp src/http/url.cpp src/io/body_stream.cpp src/io/random_access_file_body_stream.cpp - src/private/environment_log_level_listener.hpp - src/private/package_version.hpp - src/azure_assert.cpp - src/base64.cpp - src/context.cpp - src/datetime.cpp - src/environment.cpp - src/environment_log_level_listener.cpp - src/etag.cpp - src/exception.cpp src/logger.cpp src/operation_status.cpp + src/private/environment_log_level_listener.hpp + src/private/package_version.hpp + src/private/input_sanitizer.cpp src/strings.cpp + src/tracing/tracing.cpp src/uuid.cpp - src/tracing/tracing.cpp) +) add_library(azure-core ${AZURE_CORE_HEADER} ${AZURE_CORE_SOURCE}) diff --git a/sdk/core/azure-core/inc/azure/core/context.hpp b/sdk/core/azure-core/inc/azure/core/context.hpp index 67d55fddb7..5a610bb3d9 100644 --- a/sdk/core/azure-core/inc/azure/core/context.hpp +++ b/sdk/core/azure-core/inc/azure/core/context.hpp @@ -12,7 +12,6 @@ #include "azure/core/datetime.hpp" #include "azure/core/dll_import_export.hpp" #include "azure/core/rtti.hpp" -#include "azure/core/tracing/tracing.hpp" #include #include #include @@ -20,6 +19,11 @@ #include #include +// Forward declare TracerProvider to resolve an include file dependency ordering problem. +namespace Azure { namespace Core { namespace Tracing { + class TracerProvider; +}}} // namespace Azure::Core::Tracing + namespace Azure { namespace Core { /** diff --git a/sdk/core/azure-core/inc/azure/core/http/http.hpp b/sdk/core/azure-core/inc/azure/core/http/http.hpp index 5e88b9d996..a8c8247ab3 100644 --- a/sdk/core/azure-core/inc/azure/core/http/http.hpp +++ b/sdk/core/azure-core/inc/azure/core/http/http.hpp @@ -273,6 +273,16 @@ namespace Azure { namespace Core { namespace Http { */ void SetHeader(std::string const& name, std::string const& value); + /** + * @brief Gets a specific HTTP header from an #Azure::Core::Http::Request. + * + * @param name The name for the header to be retrieved. + * @return The desired header, or an empty nullable if it is not found.. + * + * @throw if \p name is an invalid header key. + */ + Azure::Nullable GetHeader(std::string const& name); + /** * @brief Remove an HTTP header. * @@ -285,11 +295,13 @@ namespace Azure { namespace Core { namespace Http { * @brief Get HttpMethod. * */ - HttpMethod GetMethod() const; + HttpMethod const& GetMethod() const; /** * @brief Get HTTP headers. * + * @remark Note that this function return a COPY of the headers for this request. + * */ CaseInsensitiveMap GetHeaders() const; diff --git a/sdk/core/azure-core/inc/azure/core/http/policies/policy.hpp b/sdk/core/azure-core/inc/azure/core/http/policies/policy.hpp index 65c394d40b..2262537a25 100644 --- a/sdk/core/azure-core/inc/azure/core/http/policies/policy.hpp +++ b/sdk/core/azure-core/inc/azure/core/http/policies/policy.hpp @@ -14,9 +14,11 @@ #include "azure/core/dll_import_export.hpp" #include "azure/core/http/http.hpp" #include "azure/core/http/transport.hpp" +#include "azure/core/internal/input_sanitizer.hpp" #include "azure/core/tracing/tracing.hpp" #include "azure/core/uuid.hpp" +#include #include #include #include @@ -382,6 +384,44 @@ namespace Azure { namespace Core { namespace Http { namespace Policies { } }; + /** + * @brief HTTP Request Activity policy. + * + * @details Registers an HTTP request with the distributed tracing infrastructure, adding + * the traceparent header to the request if necessary. + * + * This policy is intended to be inserted into the HTTP pipeline *after* the retry policy. + */ + class RequestActivityPolicy final : public HttpPolicy { + private: + Azure::Core::_internal::InputSanitizer m_inputSanitizer; + + public: + /** + * @brief Constructs HTTP Request Activity policy. + */ + // explicit RequestActivityPolicy() = default; + /** + * @brief Constructs HTTP Request Activity policy. + * + * @param inputSanitizer for sanitizing data before it is logged. + */ + explicit RequestActivityPolicy(Azure::Core::_internal::InputSanitizer const& inputSanitizer) + : m_inputSanitizer(inputSanitizer) + { + } + + std::unique_ptr Clone() const override + { + return std::make_unique(*this); + } + + std::unique_ptr Send( + Request& request, + NextHttpPolicy nextPolicy, + Context const& context) const override; + }; + /** * @brief HTTP telemetry policy. * @@ -475,13 +515,18 @@ namespace Azure { namespace Core { namespace Http { namespace Policies { */ class LogPolicy final : public HttpPolicy { LogOptions m_options; + Azure::Core::_internal::InputSanitizer m_inputSanitizer; public: /** * @brief Constructs HTTP logging policy. * */ - explicit LogPolicy(LogOptions options) : m_options(std::move(options)) {} + explicit LogPolicy(LogOptions options) + : m_options(std::move(options)), + m_inputSanitizer(m_options.AllowedHttpQueryParameters, m_options.AllowedHttpHeaders) + { + } std::unique_ptr Clone() const override { diff --git a/sdk/core/azure-core/inc/azure/core/internal/http/pipeline.hpp b/sdk/core/azure-core/inc/azure/core/internal/http/pipeline.hpp index 3217ffc0c4..f2ee822752 100644 --- a/sdk/core/azure-core/inc/azure/core/internal/http/pipeline.hpp +++ b/sdk/core/azure-core/inc/azure/core/internal/http/pipeline.hpp @@ -14,6 +14,7 @@ #include "azure/core/http/policies/policy.hpp" #include "azure/core/http/transport.hpp" #include "azure/core/internal/client_options.hpp" +#include "azure/core/internal/input_sanitizer.hpp" #include #include @@ -76,16 +77,20 @@ namespace Azure { namespace Core { namespace Http { namespace _internal { std::vector>&& perRetryPolicies, std::vector>&& perCallPolicies) { + Azure::Core::_internal::InputSanitizer inputSanitizer( + clientOptions.Log.AllowedHttpQueryParameters, clientOptions.Log.AllowedHttpHeaders); + auto const& perCallClientPolicies = clientOptions.PerOperationPolicies; auto const& perRetryClientPolicies = clientOptions.PerRetryPolicies; - // Adding 5 for: + // Adding 6 for: // - TelemetryPolicy // - RequestIdPolicy // - RetryPolicy // - LogPolicy + // - RequestActivityPolicy // - TransportPolicy auto pipelineSize = perCallClientPolicies.size() + perRetryClientPolicies.size() - + perRetryPolicies.size() + perCallPolicies.size() + 5; + + perRetryPolicies.size() + perCallPolicies.size() + 6; m_policies.reserve(pipelineSize); @@ -98,6 +103,7 @@ namespace Azure { namespace Core { namespace Http { namespace _internal { // Request Id m_policies.emplace_back( std::make_unique()); + // Telemetry m_policies.emplace_back( std::make_unique( @@ -124,6 +130,11 @@ namespace Azure { namespace Core { namespace Http { namespace _internal { m_policies.emplace_back(policy->Clone()); } + // Add a request activity policy which will generate distributed traces for the pipeline. + m_policies.emplace_back( + std::make_unique( + inputSanitizer)); + // logging - won't update request m_policies.emplace_back( std::make_unique(clientOptions.Log)); diff --git a/sdk/core/azure-core/inc/azure/core/internal/input_sanitizer.hpp b/sdk/core/azure-core/inc/azure/core/internal/input_sanitizer.hpp new file mode 100644 index 0000000000..2322f341a5 --- /dev/null +++ b/sdk/core/azure-core/inc/azure/core/internal/input_sanitizer.hpp @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#pragma once + +#include "azure/core/url.hpp" +#include + +namespace Azure { namespace Core { namespace _internal { + class InputSanitizer final { + /** + * @brief HTTP header names that are allowed to be logged. + */ + Azure::Core::CaseInsensitiveSet m_allowedHttpHeaders; + + /** + * @brief HTTP query parameter names that are allowed to be logged. + */ + std::set m_allowedHttpQueryParameters; + + // Manifest constant indicating a field was redacted. + static const char* m_RedactedPlaceholder; + + public: + InputSanitizer() = default; + InputSanitizer( + std::set const& allowedHttpQueryParameters, + Azure::Core::CaseInsensitiveSet const& allowedHttpHeaders) + : m_allowedHttpHeaders(allowedHttpHeaders), + m_allowedHttpQueryParameters(allowedHttpQueryParameters) + { + } + /** + * @brief Sanitizes the specified URL according to the sanitization rules configured. + * + * @param url Url to sanitize. Specified elements will be redacted from the URL. + * @return sanitized URL. + */ + Azure::Core::Url SanitizeUrl(Url const& url) const; + /** + * @brief Sanitizes the provided HTTP header value according to the sanitization rules + * configured. + * + * @param headerName Name of the header to sanitize. + * @param headerValue Current value of the header to sanitize. + * @return Sanitized header value. + */ + std::string SanitizeHeader(std::string const& headerName, std::string const& headerValue) const; + }; +}}} // namespace Azure::Core::_internal diff --git a/sdk/core/azure-core/inc/azure/core/internal/tracing/service_tracing.hpp b/sdk/core/azure-core/inc/azure/core/internal/tracing/service_tracing.hpp index 9b10b2a355..9f9217c7d7 100644 --- a/sdk/core/azure-core/inc/azure/core/internal/tracing/service_tracing.hpp +++ b/sdk/core/azure-core/inc/azure/core/internal/tracing/service_tracing.hpp @@ -65,6 +65,7 @@ namespace Azure { namespace Core { namespace Tracing { namespace _internal { m_span->SetStatus(status, description); } } + /** * @brief Adds a set of attributes to the span. * @@ -78,6 +79,21 @@ namespace Azure { namespace Core { namespace Tracing { namespace _internal { } } + /** + * @brief Adds a single attributes to the span. + * + * @param attributeName Name of the attribute to be added. + * @param attributeValue Value of the attribute to be added. + */ + virtual void AddAttribute(std::string const& attributeName, std::string const& attributeValue) + override + { + if (m_span) + { + m_span->AddAttribute(attributeName, attributeValue); + } + } + /** * @brief Adds an event to the span. * @@ -123,6 +139,20 @@ namespace Azure { namespace Core { namespace Tracing { namespace _internal { m_span->AddEvent(exception); } } + + /** + * @brief Propogate information from the current span to the HTTP request headers. + * + * @param request HTTP Request to the service. If there is an active tracing span, this will + * add required headers to the HTTP Request. + */ + virtual void PropagateToHttpHeaders(Azure::Core::Http::Request& request) override + { + if (m_span) + { + m_span->PropagateToHttpHeaders(request); + } + } }; /** @@ -178,10 +208,12 @@ namespace Azure { namespace Core { namespace Tracing { namespace _internal { ContextAndSpan CreateSpan( std::string const& spanName, + Azure::Core::Tracing::_internal::SpanKind const& spanKind, Azure::Core::Context const& clientContext); static ContextAndSpan CreateSpanFromContext( std::string const& spanName, + Azure::Core::Tracing::_internal::SpanKind const& spanKind, Azure::Core::Context const& clientContext); std::unique_ptr CreateAttributeSet(); diff --git a/sdk/core/azure-core/inc/azure/core/io/body_stream.hpp b/sdk/core/azure-core/inc/azure/core/io/body_stream.hpp index d805ab203d..f71d631945 100644 --- a/sdk/core/azure-core/inc/azure/core/io/body_stream.hpp +++ b/sdk/core/azure-core/inc/azure/core/io/body_stream.hpp @@ -162,8 +162,8 @@ namespace Azure { namespace Core { namespace IO { namespace _internal { /** - * @brief A concrete implementation of #Azure::Core::IO::BodyStream used for reading data from - * a file from any offset and length within it. + * @brief A concrete implementation of #Azure::Core::IO::BodyStream used for reading data + * from a file from any offset and length within it. */ class RandomAccessFileBodyStream final : public BodyStream { private: @@ -284,8 +284,8 @@ namespace Azure { namespace Core { namespace IO { }; /** - * @brief A concrete implementation of #Azure::Core::IO::BodyStream that wraps another stream and - * reports progress + * @brief A concrete implementation of #Azure::Core::IO::BodyStream that wraps another stream + * and reports progress */ class ProgressBodyStream : public BodyStream { private: diff --git a/sdk/core/azure-core/inc/azure/core/tracing/tracing.hpp b/sdk/core/azure-core/inc/azure/core/tracing/tracing.hpp index 71663cf4fb..9385277b75 100644 --- a/sdk/core/azure-core/inc/azure/core/tracing/tracing.hpp +++ b/sdk/core/azure-core/inc/azure/core/tracing/tracing.hpp @@ -17,6 +17,11 @@ #include #include +// Forward declare Azure::Core::Http::Request to resolve an include file ordering problem. +namespace Azure { namespace Core { namespace Http { + class Request; +}}} // namespace Azure::Core::Http + namespace Azure { namespace Core { namespace Tracing { namespace _internal { @@ -104,10 +109,10 @@ namespace Azure { namespace Core { namespace Tracing { virtual void AddAttribute(std::string const& attributeName, std::string const& value) = 0; /** - * @brief destroys an AttributeSet - virtual destructor to enable base class users to destroy - * derived classes. + * @brief destroys an AttributeSet - virtual destructor to enable base class users to + * destroy derived classes. */ - virtual ~AttributeSet(){}; + virtual ~AttributeSet() = default; }; /** @brief The Type of Span. @@ -184,6 +189,15 @@ namespace Azure { namespace Core { namespace Tracing { */ virtual void AddAttributes(AttributeSet const& attributeToAdd) = 0; + /** + * @brief Adds a single string valued attribute to the span. + * + * @param attributeName Name of the attribute to add. + * @param attributeValue value of the attribute. + */ + virtual void AddAttribute(std::string const& attributeName, std::string const& attributeValue) + = 0; + /** * @brief Adds an event to the span. * @@ -217,6 +231,14 @@ namespace Azure { namespace Core { namespace Tracing { * @param description A description associated with the Status. */ virtual void SetStatus(SpanStatus const& status, std::string const& description = "") = 0; + + /** + * @brief Propogate information from the current span to the HTTP request headers. + * + * @param request HTTP Request to the service. If there is an active tracing span, this will + * add required headers to the HTTP Request. + */ + virtual void PropagateToHttpHeaders(Azure::Core::Http::Request& request) = 0; }; /** diff --git a/sdk/core/azure-core/src/http/log_policy.cpp b/sdk/core/azure-core/src/http/log_policy.cpp index 1b824fcf8c..2fe4cf5884 100644 --- a/sdk/core/azure-core/src/http/log_policy.cpp +++ b/sdk/core/azure-core/src/http/log_policy.cpp @@ -21,8 +21,8 @@ std::string RedactedPlaceholder = "REDACTED"; inline void AppendHeaders( std::ostringstream& log, - Azure::Core::CaseInsensitiveMap const& headers, - Azure::Core::CaseInsensitiveSet const& allowedHaders) + Azure::Core::_internal::InputSanitizer const& inputSanitizer, + Azure::Core::CaseInsensitiveMap const& headers) { for (auto const& header : headers) { @@ -30,90 +30,27 @@ inline void AppendHeaders( if (!header.second.empty()) { - log - << ((allowedHaders.find(header.first) != allowedHaders.end()) ? header.second - : RedactedPlaceholder); + log << inputSanitizer.SanitizeHeader(header.first, header.second); } } } -inline void LogUrlWithoutQuery(std::ostringstream& log, Url const& url) +inline std::string GetRequestLogMessage( + Azure::Core::_internal::InputSanitizer const& inputSanitizer, + Request const& request) { - if (!url.GetScheme().empty()) - { - log << url.GetScheme() << "://"; - } - log << url.GetHost(); - if (url.GetPort() != 0) - { - log << ":" << url.GetPort(); - } - if (!url.GetPath().empty()) - { - log << "/" << url.GetPath(); - } -} - -inline std::string GetRequestLogMessage(LogOptions const& options, Request const& request) -{ - auto const& requestUrl = request.GetUrl(); - std::ostringstream log; log << "HTTP Request : " << request.GetMethod().ToString() << " "; - LogUrlWithoutQuery(log, requestUrl); - - { - auto encodedRequestQueryParams = requestUrl.GetQueryParameters(); - std::remove_const::type>::type - loggedQueryParams; + Azure::Core::Url urlToLog(inputSanitizer.SanitizeUrl(request.GetUrl())); + log << urlToLog.GetAbsoluteUrl(); - if (!encodedRequestQueryParams.empty()) - { - auto const& unencodedAllowedQueryParams = options.AllowedHttpQueryParameters; - if (!unencodedAllowedQueryParams.empty()) - { - std::remove_const::type>::type - encodedAllowedQueryParams; - std::transform( - unencodedAllowedQueryParams.begin(), - unencodedAllowedQueryParams.end(), - std::inserter(encodedAllowedQueryParams, encodedAllowedQueryParams.begin()), - [](std::string const& s) { return Url::Encode(s); }); - - for (auto const& encodedRequestQueryParam : encodedRequestQueryParams) - { - if (encodedRequestQueryParam.second.empty() - || (encodedAllowedQueryParams.find(encodedRequestQueryParam.first) - != encodedAllowedQueryParams.end())) - { - loggedQueryParams.insert(encodedRequestQueryParam); - } - else - { - loggedQueryParams.insert( - std::make_pair(encodedRequestQueryParam.first, RedactedPlaceholder)); - } - } - } - else - { - for (auto const& encodedRequestQueryParam : encodedRequestQueryParams) - { - loggedQueryParams.insert( - std::make_pair(encodedRequestQueryParam.first, RedactedPlaceholder)); - } - } - - log << Azure::Core::_detail::FormatEncodedUrlQueryParameters(loggedQueryParams); - } - } - AppendHeaders(log, request.GetHeaders(), options.AllowedHttpHeaders); + AppendHeaders(log, inputSanitizer, request.GetHeaders()); return log.str(); } inline std::string GetResponseLogMessage( - LogOptions const& options, + Azure::Core::_internal::InputSanitizer const& inputSanitizer, RawResponse const& response, std::chrono::system_clock::duration const& duration) { @@ -124,36 +61,39 @@ inline std::string GetResponseLogMessage( << "ms) : " << static_cast(response.GetStatusCode()) << " " << response.GetReasonPhrase(); - AppendHeaders(log, response.GetHeaders(), options.AllowedHttpHeaders); + AppendHeaders(log, inputSanitizer, response.GetHeaders()); return log.str(); } } // namespace Azure::Core::CaseInsensitiveSet const Azure::Core::Http::Policies::_detail::g_defaultAllowedHttpHeaders - = {"x-ms-request-id", - "x-ms-client-request-id", - "x-ms-return-client-request-id", - "traceparent", - "Accept", - "Cache-Control", - "Connection", - "Content-Length", - "Content-Type", - "Date", - "ETag", - "Expires", - "If-Match", - "If-Modified-Since", - "If-None-Match", - "If-Unmodified-Since", - "Last-Modified", - "Pragma", - "Request-Id", - "Retry-After", - "Server", - "Transfer-Encoding", - "User-Agent"}; + = { + "Accept", + "Cache-Control", + "Connection", + "Content-Length", + "Content-Type", + "Date", + "ETag", + "Expires", + "If-Match", + "If-Modified-Since", + "If-None-Match", + "If-Unmodified-Since", + "Last-Modified", + "Pragma", + "Request-Id", + "Retry-After", + "Server", + "traceparent", + "tracestate", + "Transfer-Encoding", + "User-Agent" + "x-ms-client-request-id", + "x-ms-request-id", + "x-ms-return-client-request-id", +}; std::unique_ptr LogPolicy::Send( Request& request, @@ -165,7 +105,7 @@ std::unique_ptr LogPolicy::Send( if (Log::ShouldWrite(Logger::Level::Verbose)) { - Log::Write(Logger::Level::Informational, GetRequestLogMessage(m_options, request)); + Log::Write(Logger::Level::Informational, GetRequestLogMessage(m_inputSanitizer, request)); } else { @@ -177,7 +117,8 @@ std::unique_ptr LogPolicy::Send( auto const end = std::chrono::system_clock::now(); Log::Write( - Logger::Level::Informational, GetResponseLogMessage(m_options, *response, end - start)); + Logger::Level::Informational, + GetResponseLogMessage(m_inputSanitizer, *response, end - start)); return response; } diff --git a/sdk/core/azure-core/src/http/request.cpp b/sdk/core/azure-core/src/http/request.cpp index 0fa5035076..ba768de9a8 100644 --- a/sdk/core/azure-core/src/http/request.cpp +++ b/sdk/core/azure-core/src/http/request.cpp @@ -22,6 +22,24 @@ static Azure::Core::CaseInsensitiveMap MergeMaps( } } // namespace +Azure::Nullable Request::GetHeader(std::string const& name) +{ + std::vector returnedHeaders; + auto headerNameLowerCase = Azure::Core::_internal::StringExtensions::ToLower(name); + + auto retryHeader = this->m_retryHeaders.find(headerNameLowerCase); + if (retryHeader != this->m_retryHeaders.end()) + { + return retryHeader->second; + } + auto header = this->m_headers.find(headerNameLowerCase); + if (header != this->m_headers.end()) + { + return header->second; + } + return Azure::Nullable{}; +} + void Request::SetHeader(std::string const& name, std::string const& value) { auto headerNameLowerCase = Azure::Core::_internal::StringExtensions::ToLower(name); @@ -50,7 +68,7 @@ void Request::StartTry() } } -HttpMethod Request::GetMethod() const { return this->m_method; } +HttpMethod const& Request::GetMethod() const { return this->m_method; } Azure::Core::CaseInsensitiveMap Request::GetHeaders() const { diff --git a/sdk/core/azure-core/src/http/request_activity_policy.cpp b/sdk/core/azure-core/src/http/request_activity_policy.cpp new file mode 100644 index 0000000000..556830a2e4 --- /dev/null +++ b/sdk/core/azure-core/src/http/request_activity_policy.cpp @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "azure/core/http/policies/policy.hpp" +#include "azure/core/internal/diagnostics/log.hpp" +#include "azure/core/internal/input_sanitizer.hpp" +#include "azure/core/internal/tracing/service_tracing.hpp" + +#include +#include +#include +#include +#include + +using Azure::Core::Context; +using namespace Azure::Core::Http; +using namespace Azure::Core::Http::Policies; +using namespace Azure::Core::Http::Policies::_internal; +using namespace Azure::Core::Tracing::_internal; + +std::unique_ptr RequestActivityPolicy::Send( + Request& request, + NextHttpPolicy nextPolicy, + Context const& context) const +{ + // Create a tracing span over the HTTP request. + std::stringstream ss; + // We know that the retry policy MUST be above us in the hierarchy, so ask it for the current + // retry count. + auto retryCount = RetryPolicy::GetRetryCount(context); + if (retryCount == -1) + { + // We don't have a RetryPolicy in the policy stack - just assume this is request 0. + retryCount = 0; + } + ss << "HTTP " << request.GetMethod().ToString() << " #" << retryCount; + auto contextAndSpan + = Azure::Core::Tracing::_internal::DiagnosticTracingFactory::CreateSpanFromContext( + ss.str(), SpanKind::Client, context); + auto scope = std::move(contextAndSpan.second); + + scope.AddAttribute(TracingAttributes::HttpMethod.ToString(), request.GetMethod().ToString()); + scope.AddAttribute("http.url", m_inputSanitizer.SanitizeUrl(request.GetUrl()).GetAbsoluteUrl()); + { + Azure::Nullable requestId = request.GetHeader("x-ms-client-request-id"); + if (requestId.HasValue()) + { + scope.AddAttribute(TracingAttributes::RequestId.ToString(), requestId.Value()); + } + } + + { + auto userAgent = request.GetHeader("User-Agent"); + if (userAgent.HasValue()) + { + scope.AddAttribute(TracingAttributes::HttpUserAgent.ToString(), userAgent.Value()); + } + } + + // Propagate information from the scope to the HTTP headers. + // + // This will add the "traceparent" header and any other OpenTelemetry related headers. + scope.PropagateToHttpHeaders(request); + + try + { + // Send the request on to the service. + auto response = nextPolicy.Send(request, contextAndSpan.first); + + // And register the headers we received from the service. + scope.AddAttribute( + TracingAttributes::HttpStatusCode.ToString(), + std::to_string(static_cast(response->GetStatusCode()))); + auto const& responseHeaders = response->GetHeaders(); + auto serviceRequestId = responseHeaders.find("x-ms-request-id"); + if (serviceRequestId != responseHeaders.end()) + { + scope.AddAttribute(TracingAttributes::ServiceRequestId.ToString(), serviceRequestId->second); + } + + return response; + } + catch (const TransportException& e) + { + scope.AddEvent(e); + + // Rethrow the exception. + throw; + } +} diff --git a/sdk/core/azure-core/src/private/input_sanitizer.cpp b/sdk/core/azure-core/src/private/input_sanitizer.cpp new file mode 100644 index 0000000000..be2494e059 --- /dev/null +++ b/sdk/core/azure-core/src/private/input_sanitizer.cpp @@ -0,0 +1,89 @@ + +#include "azure/core/internal/input_sanitizer.hpp" +#include "azure/core/url.hpp" +#include +#include + +namespace Azure { namespace Core { namespace _internal { + + const char* InputSanitizer::m_RedactedPlaceholder = "REDACTED"; + + Azure::Core::Url InputSanitizer::SanitizeUrl(Azure::Core::Url const& url) const + { + std::ostringstream ss; + + // Sanitize the non-query part of the URL (remove username and password). + if (!url.GetScheme().empty()) + { + ss << url.GetScheme() << "://"; + } + ss << url.GetHost(); + if (url.GetPort() != 0) + { + ss << ":" << url.GetPort(); + } + if (!url.GetPath().empty()) + { + ss << "/" << url.GetPath(); + } + + { + auto encodedRequestQueryParams = url.GetQueryParameters(); + + std::remove_const::type>::type + loggedQueryParams; + + if (!encodedRequestQueryParams.empty()) + { + auto const& unencodedAllowedQueryParams = m_allowedHttpQueryParameters; + if (!unencodedAllowedQueryParams.empty()) + { + std::remove_const::type>:: + type encodedAllowedQueryParams; + std::transform( + unencodedAllowedQueryParams.begin(), + unencodedAllowedQueryParams.end(), + std::inserter(encodedAllowedQueryParams, encodedAllowedQueryParams.begin()), + [](std::string const& s) { return Url::Encode(s); }); + + for (auto const& encodedRequestQueryParam : encodedRequestQueryParams) + { + if (encodedRequestQueryParam.second.empty() + || (encodedAllowedQueryParams.find(encodedRequestQueryParam.first) + != encodedAllowedQueryParams.end())) + { + loggedQueryParams.insert(encodedRequestQueryParam); + } + else + { + loggedQueryParams.insert( + std::make_pair(encodedRequestQueryParam.first, m_RedactedPlaceholder)); + } + } + } + else + { + for (auto const& encodedRequestQueryParam : encodedRequestQueryParams) + { + loggedQueryParams.insert( + std::make_pair(encodedRequestQueryParam.first, m_RedactedPlaceholder)); + } + } + + ss << Azure::Core::_detail::FormatEncodedUrlQueryParameters(loggedQueryParams); + } + } + return Azure::Core::Url(ss.str()); + } + + std::string InputSanitizer::SanitizeHeader(std::string const& header, std::string const& value) + const + { + if (m_allowedHttpHeaders.find(header) != m_allowedHttpHeaders.end()) + { + return value; + } + return m_RedactedPlaceholder; + } + +}}} // namespace Azure::Core::_internal diff --git a/sdk/core/azure-core/src/tracing/tracing.cpp b/sdk/core/azure-core/src/tracing/tracing.cpp index d36a5301a3..920c51c7d0 100644 --- a/sdk/core/azure-core/src/tracing/tracing.cpp +++ b/sdk/core/azure-core/src/tracing/tracing.cpp @@ -14,9 +14,16 @@ namespace Azure { namespace Core { namespace Tracing { namespace _internal { const SpanStatus SpanStatus::Error("Error"); const TracingAttributes TracingAttributes::AzNamespace("az.namespace"); + const TracingAttributes TracingAttributes::ServiceRequestId("serviceRequestId"); + const TracingAttributes TracingAttributes::HttpUserAgent("http.user_agent"); + const TracingAttributes TracingAttributes::HttpMethod("http.method"); + const TracingAttributes TracingAttributes::HttpUrl("http.url"); + const TracingAttributes TracingAttributes::RequestId("requestId"); + const TracingAttributes TracingAttributes::HttpStatusCode("http.status_code"); DiagnosticTracingFactory::ContextAndSpan DiagnosticTracingFactory::CreateSpan( std::string const& methodName, + Azure::Core::Tracing::_internal::SpanKind const& spanKind, Azure::Core::Context const& context) { CreateSpanOptions createOptions; @@ -47,6 +54,8 @@ namespace Azure { namespace Core { namespace Tracing { namespace _internal { createOptions.Attributes->AddAttribute( TracingAttributes::AzNamespace.ToString(), m_serviceName); + createOptions.Kind = spanKind; + std::shared_ptr newSpan(m_serviceTracer->CreateSpan(methodName, createOptions)); TracingContext tracingContext = newSpan; Azure::Core::Context newContext = contextToUse.WithValue(ContextSpanKey, tracingContext); @@ -61,13 +70,14 @@ namespace Azure { namespace Core { namespace Tracing { namespace _internal { } DiagnosticTracingFactory::ContextAndSpan DiagnosticTracingFactory::CreateSpanFromContext( std::string const& spanName, + Azure::Core::Tracing::_internal::SpanKind const& spanKind, Azure::Core::Context const& context) { DiagnosticTracingFactory* tracingFactory = DiagnosticTracingFactory::DiagnosticFactoryFromContext(context); if (tracingFactory) { - return tracingFactory->CreateSpan(spanName, context); + return tracingFactory->CreateSpan(spanName, spanKind, context); } else { diff --git a/sdk/core/azure-core/test/ut/CMakeLists.txt b/sdk/core/azure-core/test/ut/CMakeLists.txt index f50f787a1a..bd75517ce3 100644 --- a/sdk/core/azure-core/test/ut/CMakeLists.txt +++ b/sdk/core/azure-core/test/ut/CMakeLists.txt @@ -66,6 +66,7 @@ add_executable ( operation_status_test.cpp pipeline_test.cpp policy_test.cpp + request_activity_policy_test.cpp request_id_policy_test.cpp response_t_test.cpp retry_policy_test.cpp diff --git a/sdk/core/azure-core/test/ut/request_activity_policy_test.cpp b/sdk/core/azure-core/test/ut/request_activity_policy_test.cpp new file mode 100644 index 0000000000..6a001a1e4f --- /dev/null +++ b/sdk/core/azure-core/test/ut/request_activity_policy_test.cpp @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "azure/core/http/policies/policy.hpp" +#include "azure/core/internal/http/pipeline.hpp" +#include "azure/core/internal/tracing/service_tracing.hpp" +#include "azure/core/tracing/tracing.hpp" +#include +#include + +using namespace Azure::Core; +using namespace Azure::Core::Http; +using namespace Azure::Core::Http::Policies; +using namespace Azure::Core::Http::Policies::_internal; +using namespace Azure::Core::Tracing::_internal; +using namespace Azure::Core::Tracing; + +namespace { +class NoOpPolicy final : public HttpPolicy { + std::function(Request&)> m_createResponse{}; + +public: + std::unique_ptr Clone() const override { return std::make_unique(*this); } + + std::unique_ptr Send(Request& request, NextHttpPolicy, Azure::Core::Context const&) + const override + { + if (m_createResponse) + { + return m_createResponse(request); + } + else + { + return std::make_unique(1, 1, HttpStatusCode::Ok, "Something"); + } + } + + NoOpPolicy() = default; + NoOpPolicy(std::function(Request&)> createResponse) + : HttpPolicy(), m_createResponse(createResponse){}; +}; + +// Dummy service tracing class. +class TestSpan final : public Azure::Core::Tracing::_internal::Span { + std::vector m_events; + std::map m_stringAttributes; + std::string m_spanName; + +public: + TestSpan(std::string const& spanName) + : Azure::Core::Tracing::_internal::Span(), m_spanName(spanName) + { + } + + // Inherited via Span + virtual void AddAttributes(AttributeSet const&) override {} + virtual void AddAttribute(std::string const& attributeName, std::string const& attributeValue) + override + { + m_stringAttributes.emplace(std::make_pair(attributeName, attributeValue)); + } + virtual void AddEvent(std::string const& eventName, AttributeSet const&) override + { + m_events.push_back(eventName); + } + virtual void AddEvent(std::string const& eventName) override { m_events.push_back(eventName); } + virtual void AddEvent(std::exception const& ex) override { m_events.push_back(ex.what()); } + virtual void SetStatus(SpanStatus const&, std::string const&) override {} + + // Inherited via Span + virtual void End(Azure::Nullable) override {} + + // Inherited via Span + virtual void PropagateToHttpHeaders(Azure::Core::Http::Request&) override {} + + std::string const& GetName() { return m_spanName; } + std::vector const& GetEvents() { return m_events; } + std::map const& GetAttributes() { return m_stringAttributes; } +}; + +class TestAttributeSet : public Azure::Core::Tracing::_internal::AttributeSet { +public: + TestAttributeSet() : Azure::Core::Tracing::_internal::AttributeSet() {} + + // Inherited via AttributeSet + virtual void AddAttribute(std::string const&, bool) override {} + virtual void AddAttribute(std::string const&, int32_t) override {} + virtual void AddAttribute(std::string const&, int64_t) override {} + virtual void AddAttribute(std::string const&, uint64_t) override {} + virtual void AddAttribute(std::string const&, double) override {} + virtual void AddAttribute(std::string const&, const char*) override {} + virtual void AddAttribute(std::string const&, std::string const&) override {} +}; + +class TestTracer final : public Azure::Core::Tracing::_internal::Tracer { + mutable std::vector> m_spans; + +public: + TestTracer(std::string const&, std::string const&) : Azure::Core::Tracing::_internal::Tracer() {} + std::shared_ptr CreateSpan(std::string const& spanName, CreateSpanOptions const&) + const override + { + auto returnSpan(std::make_shared(spanName)); + m_spans.push_back(returnSpan); + return returnSpan; + } + + std::unique_ptr CreateAttributeSet() const override + { + return std::make_unique(); + }; + + std::vector> const& GetSpans() { return m_spans; } +}; + +class TestTracingProvider final : public Azure::Core::Tracing::TracerProvider { + mutable std::list> m_tracers; + +public: + TestTracingProvider() : TracerProvider() {} + ~TestTracingProvider() {} + std::shared_ptr CreateTracer( + std::string const& serviceName, + std::string const& serviceVersion) const override + { + auto returnTracer = std::make_shared(serviceName, serviceVersion); + m_tracers.push_back(returnTracer); + return returnTracer; + }; + + std::list> const& GetTracers() { return m_tracers; } +}; +} // namespace + +TEST(RequestActivityPolicy, Basic) +{ + { + auto testTracer = std::make_shared(); + + Azure::Core::_internal::ClientOptions clientOptions; + clientOptions.Telemetry.TracingProvider = testTracer; + Azure::Core::Tracing::_internal::DiagnosticTracingFactory serviceTrace( + clientOptions, "my-service-cpp", "1.0b2"); + + auto contextAndSpan = serviceTrace.CreateSpan( + "My API", Azure::Core::Tracing::_internal::SpanKind::Internal, {}); + Azure::Core::Context callContext = std::move(contextAndSpan.first); + Request request(HttpMethod::Get, Url("https://www.microsoft.com")); + + { + std::vector> policies; + // Add the request ID policy - this adds the x-ms-request-id attribute to the pipeline. + policies.emplace_back( + std::make_unique(Azure::Core::_internal::InputSanitizer{})); + // Final policy - equivalent to HTTP policy. + policies.emplace_back(std::make_unique()); + + Azure::Core::Http::_internal::HttpPipeline(policies).Send(request, callContext); + } + + EXPECT_EQ(1ul, testTracer->GetTracers().size()); + auto& tracer = testTracer->GetTracers().front(); + EXPECT_EQ(2ul, tracer->GetSpans().size()); + EXPECT_EQ("My API", tracer->GetSpans()[0]->GetName()); + EXPECT_EQ("HTTP GET #0", tracer->GetSpans()[1]->GetName()); + EXPECT_EQ("GET", tracer->GetSpans()[1]->GetAttributes().at("http.method")); + } + + // Now try with the request ID and telemetry policies (simulating a more complete pipeline). + { + auto testTracer = std::make_shared(); + + Azure::Core::_internal::ClientOptions clientOptions; + clientOptions.Telemetry.TracingProvider = testTracer; + Azure::Core::Tracing::_internal::DiagnosticTracingFactory serviceTrace( + clientOptions, "my-service-cpp", "1.0b2"); + auto contextAndSpan = serviceTrace.CreateSpan( + "My API", Azure::Core::Tracing::_internal::SpanKind::Internal, {}); + Azure::Core::Context callContext = std::move(contextAndSpan.first); + Request request(HttpMethod::Get, Url("https://www.microsoft.com")); + + { + std::vector> policies; + // Add the request ID policy - this adds the x-ms-request-id attribute to the pipeline. + policies.emplace_back(std::make_unique()); + policies.emplace_back( + std::make_unique("my-service-cpp", "1.0b2", clientOptions.Telemetry)); + policies.emplace_back(std::make_unique(RetryOptions{})); + policies.emplace_back( + std::make_unique(Azure::Core::_internal::InputSanitizer{})); + // Final policy - equivalent to HTTP policy. + policies.emplace_back(std::make_unique()); + + Azure::Core::Http::_internal::HttpPipeline(policies).Send(request, callContext); + } + + EXPECT_EQ(1ul, testTracer->GetTracers().size()); + auto& tracer = testTracer->GetTracers().front(); + EXPECT_EQ(2ul, tracer->GetSpans().size()); + EXPECT_EQ("My API", tracer->GetSpans()[0]->GetName()); + EXPECT_EQ("HTTP GET #0", tracer->GetSpans()[1]->GetName()); + EXPECT_EQ("GET", tracer->GetSpans()[1]->GetAttributes().at("http.method")); + } +} + +TEST(RequestActivityPolicy, TryRetries) +{ + { + auto testTracer = std::make_shared(); + + Azure::Core::_internal::ClientOptions clientOptions; + clientOptions.Telemetry.TracingProvider = testTracer; + Azure::Core::Tracing::_internal::DiagnosticTracingFactory serviceTrace( + clientOptions, "my-service-cpp", "1.0b2"); + + auto contextAndSpan = serviceTrace.CreateSpan( + "My API", Azure::Core::Tracing::_internal::SpanKind::Internal, {}); + Azure::Core::Context callContext = std::move(contextAndSpan.first); + Request request(HttpMethod::Get, Url("https://www.microsoft.com")); + + { + std::vector> policies; + + policies.emplace_back(std::make_unique()); + policies.emplace_back(std::make_unique(RetryOptions{})); + + // Add the request ID policy - this adds the x-ms-request-id attribute to the pipeline. + policies.emplace_back( + std::make_unique(Azure::Core::_internal::InputSanitizer{})); + // Final policy - equivalent to HTTP policy. + int retryCount = 0; + policies.emplace_back(std::make_unique([&](Request&) { + retryCount += 1; + if (retryCount < 3) + { + // Return a response which should trigger a response. + return std::make_unique( + 1, 1, *RetryOptions().StatusCodes.begin(), "Something"); + } + else + { + // Return success. + return std::make_unique(1, 1, HttpStatusCode::Ok, "Something"); + } + })); + + Azure::Core::Http::_internal::HttpPipeline pipeline(policies); + // Simulate retrying an HTTP operation 3 times on the pipeline: + pipeline.Send(request, callContext); + } + + EXPECT_EQ(1ul, testTracer->GetTracers().size()); + auto& tracer = testTracer->GetTracers().front(); + EXPECT_EQ(4ul, tracer->GetSpans().size()); + EXPECT_EQ("My API", tracer->GetSpans()[0]->GetName()); + EXPECT_EQ("HTTP GET #0", tracer->GetSpans()[1]->GetName()); + EXPECT_EQ("HTTP GET #1", tracer->GetSpans()[2]->GetName()); + EXPECT_EQ("HTTP GET #2", tracer->GetSpans()[3]->GetName()); + EXPECT_EQ("GET", tracer->GetSpans()[1]->GetAttributes().at("http.method")); + EXPECT_EQ("408", tracer->GetSpans()[1]->GetAttributes().at("http.status_code")); + EXPECT_EQ("408", tracer->GetSpans()[2]->GetAttributes().at("http.status_code")); + EXPECT_EQ("200", tracer->GetSpans()[3]->GetAttributes().at("http.status_code")); + } +} diff --git a/sdk/core/azure-core/test/ut/service_tracing_test.cpp b/sdk/core/azure-core/test/ut/service_tracing_test.cpp index 68e205134d..7675a62363 100644 --- a/sdk/core/azure-core/test/ut/service_tracing_test.cpp +++ b/sdk/core/azure-core/test/ut/service_tracing_test.cpp @@ -48,11 +48,12 @@ TEST(DiagnosticTracingFactory, SimpleServiceSpanTests) Azure::Core::Tracing::_internal::DiagnosticTracingFactory serviceTrace( clientOptions, "my-service-cpp", "1.0b2"); - auto contextAndSpan = serviceTrace.CreateSpan("My API", {}); + auto contextAndSpan = serviceTrace.CreateSpan( + "My API", Azure::Core::Tracing::_internal::SpanKind::Internal, {}); EXPECT_FALSE(contextAndSpan.first.IsCancelled()); } } - +namespace { // Dummy service tracing class. class TestSpan final : public Azure::Core::Tracing::_internal::Span { public: @@ -60,6 +61,7 @@ class TestSpan final : public Azure::Core::Tracing::_internal::Span { // Inherited via Span virtual void AddAttributes(AttributeSet const&) override {} + virtual void AddAttribute(std::string const&, std::string const&) override {} virtual void AddEvent(std::string const&, AttributeSet const&) override {} virtual void AddEvent(std::string const&) override {} virtual void AddEvent(std::exception const&) override {} @@ -67,6 +69,9 @@ class TestSpan final : public Azure::Core::Tracing::_internal::Span { // Inherited via Span virtual void End(Azure::Nullable) override {} + + // Inherited via Span + virtual void PropagateToHttpHeaders(Azure::Core::Http::Request&) override {} }; class TestAttributeSet : public Azure::Core::Tracing::_internal::AttributeSet { @@ -107,7 +112,7 @@ class TestTracingProvider final : public Azure::Core::Tracing::TracerProvider { return std::make_shared(serviceName, serviceVersion); }; }; - +} // namespace TEST(DiagnosticTracingFactory, BasicServiceSpanTests) { { @@ -115,14 +120,16 @@ TEST(DiagnosticTracingFactory, BasicServiceSpanTests) Azure::Core::Tracing::_internal::DiagnosticTracingFactory serviceTrace( clientOptions, "my-service-cpp", "1.0b2"); - auto contextAndSpan = serviceTrace.CreateSpan("My API", {}); - auto span = std::move(contextAndSpan.second); + auto contextAndSpan = serviceTrace.CreateSpan( + "My API", Azure::Core::Tracing::_internal::SpanKind::Internal, {}); + ServiceSpan span = std::move(contextAndSpan.second); span.End(); span.AddEvent("New Event"); span.AddEvent(std::runtime_error("Exception")); span.SetStatus(SpanStatus::Error); } + { Azure::Core::_internal::ClientOptions clientOptions; auto testTracer = std::make_shared(); @@ -130,8 +137,9 @@ TEST(DiagnosticTracingFactory, BasicServiceSpanTests) Azure::Core::Tracing::_internal::DiagnosticTracingFactory serviceTrace( clientOptions, "my-service-cpp", "1.0b2"); - auto contextAndSpan = serviceTrace.CreateSpan("My API", {}); - auto span = std::move(contextAndSpan.second); + auto contextAndSpan = serviceTrace.CreateSpan( + "My API", Azure::Core::Tracing::_internal::SpanKind::Internal, {}); + ServiceSpan span = std::move(contextAndSpan.second); span.End(); span.AddEvent("New Event");