hs-opentelemetry-instrumentation-amazonka (empty) → 1.0.0.0
raw patch · 7 files changed
+387/−0 lines, 7 filesdep +amazonkadep +amazonka-coredep +basesetup-changed
Dependencies added: amazonka, amazonka-core, base, bytestring, case-insensitive, hs-opentelemetry-api, hs-opentelemetry-exporter-in-memory, hs-opentelemetry-instrumentation-amazonka, hs-opentelemetry-sdk, hs-opentelemetry-semantic-conventions, hspec, http-client, http-types, text, unordered-containers
Files
- ChangeLog.md +14/−0
- LICENSE +30/−0
- README.md +44/−0
- Setup.hs +4/−0
- hs-opentelemetry-instrumentation-amazonka.cabal +69/−0
- src/OpenTelemetry/Instrumentation/Amazonka.hs +224/−0
- test/Spec.hs +2/−0
+ ChangeLog.md view
@@ -0,0 +1,14 @@+# Changelog for hs-opentelemetry-instrumentation-amazonka++## 1.0.0.0 - 2026-05-29++- Promoted to 1.0.0.0 for the hs-opentelemetry 1.0 release.++## 0.1.0.0++* Initial release+* Hooks-based automatic tracing for all Amazonka `send` calls+* `instrumentEnv` to add tracing hooks to an Amazonka `Env`+* AWS SDK semantic convention attributes (rpc.system, rpc.service, rpc.method, aws.request_id)+* HTTP response attributes (http.response.status_code)+* Error recording on failed AWS calls
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Ian Duncan (c) 2021-2026++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Ian Duncan nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,44 @@+# hs-opentelemetry-instrumentation-amazonka++[](https://hackage.haskell.org/package/hs-opentelemetry-instrumentation-amazonka)++OpenTelemetry instrumentation for the [Amazonka](https://hackage.haskell.org/package/amazonka) AWS SDK.++## GHC Compatibility++This package requires `amazonka >= 2.0`. As of April 2026, the published+`amazonka-2.0` on Hackage does not compile with GHC 9.10+ due to+`DuplicateRecordFields` changes in generated service packages (`amazonka-sts`,+`amazonka-sso`). You may need to use amazonka from its GitHub `main` branch+or wait for the next Hackage release.++## Usage++```haskell+import Amazonka+import OpenTelemetry.Instrumentation.Amazonka (instrumentEnv)+import OpenTelemetry.Trace (getTracerProvider, makeTracer, tracerOptions)++main :: IO ()+main = do+ tp <- getTracerProvider+ let tracer = makeTracer tp "my-app" tracerOptions+ env <- newEnv discover+ let tracedEnv = instrumentEnv tracer env+ -- All send calls through tracedEnv will create OTel spans+ runResourceT $ send tracedEnv someRequest+```++## Attributes++Per the [OTel AWS SDK semantic conventions](https://opentelemetry.io/docs/specs/semconv/cloud-providers/aws-sdk/):++| Attribute | Description |+|-----------|-------------|+| `rpc.system` | Always `"aws-api"` |+| `rpc.service` | AWS service abbreviation (e.g., `"S3"`, `"DynamoDB"`) |+| `rpc.method` | AWS operation name (e.g., `"GetObject"`, `"PutItem"`) |+| `aws.request_id` | From response headers when available |+| `http.response.status_code` | HTTP status code |+| `server.address` | AWS endpoint host |+| `cloud.region` | AWS region |
+ Setup.hs view
@@ -0,0 +1,4 @@+import Distribution.Simple+++main = defaultMain
+ hs-opentelemetry-instrumentation-amazonka.cabal view
@@ -0,0 +1,69 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.38.3.+--+-- see: https://github.com/sol/hpack++name: hs-opentelemetry-instrumentation-amazonka+version: 1.0.0.0+synopsis: OpenTelemetry instrumentation for the Amazonka AWS SDK+description: Automatic tracing for Amazonka AWS SDK calls via the hooks API. Creates spans per AWS API call with semantic convention attributes (rpc.system, rpc.service, rpc.method, aws.request_id, etc.).+category: OpenTelemetry, Telemetry, Monitoring, Observability, AWS+homepage: https://github.com/iand675/hs-opentelemetry#readme+bug-reports: https://github.com/iand675/hs-opentelemetry/issues+author: Ian Duncan, Jade Lovelace+maintainer: ian@iankduncan.com+copyright: 2024 Ian Duncan, Mercury Technologies+license: BSD3+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.md+ ChangeLog.md++source-repository head+ type: git+ location: https://github.com/iand675/hs-opentelemetry++library+ exposed-modules:+ OpenTelemetry.Instrumentation.Amazonka+ other-modules:+ Paths_hs_opentelemetry_instrumentation_amazonka+ hs-source-dirs:+ src+ ghc-options: -Wall+ build-depends:+ amazonka ==2.0.*+ , amazonka-core ==2.0.*+ , base >=4.7 && <5+ , bytestring+ , case-insensitive+ , hs-opentelemetry-api ==1.0.*+ , hs-opentelemetry-semantic-conventions >=1.40 && <2+ , http-client+ , http-types+ , text+ , unordered-containers+ default-language: Haskell2010++test-suite hs-opentelemetry-instrumentation-amazonka-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Paths_hs_opentelemetry_instrumentation_amazonka+ hs-source-dirs:+ test+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ amazonka+ , amazonka-core+ , base >=4.7 && <5+ , hs-opentelemetry-api ==1.0.*+ , hs-opentelemetry-exporter-in-memory ==1.0.*+ , hs-opentelemetry-instrumentation-amazonka ==1.0.*+ , hs-opentelemetry-sdk ==1.0.*+ , hspec+ , text+ , unordered-containers+ default-language: Haskell2010
+ src/OpenTelemetry/Instrumentation/Amazonka.hs view
@@ -0,0 +1,224 @@+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- |+Module : OpenTelemetry.Instrumentation.Amazonka+Copyright : (c) Ian Duncan, 2024+License : BSD-3+Description : OpenTelemetry instrumentation for the Amazonka AWS SDK+Maintainer : Ian Duncan+Stability : experimental+Portability : non-portable (GHC extensions)++Provides automatic tracing for all AWS API calls made through the Amazonka+SDK. Installs hooks on an Amazonka 'Env' that create OTel spans per+@send@\/@sendEither@ call, following the+<https://opentelemetry.io/docs/specs/semconv/cloud-providers/aws-sdk/ OTel AWS SDK semantic conventions>.++@+import Amazonka+import OpenTelemetry.Instrumentation.Amazonka ('instrumentEnv')++main :: IO ()+main = 'OpenTelemetry.Trace.withTracerProvider' $ \\tp -> do+ let tracer = 'OpenTelemetry.Trace.Core.makeTracer' tp "my-app" 'OpenTelemetry.Trace.Core.tracerOptions'+ env <- newEnv discover+ let tracedEnv = 'instrumentEnv' tracer env+ runResourceT $ send tracedEnv someRequest+@++Each @send@ call produces a span named @\"Service.Operation\"@ (e.g.,+@\"S3.GetObject\"@, @\"DynamoDB.PutItem\"@) with standard RPC and AWS+attributes.++@since 0.1.0.0+-}+module OpenTelemetry.Instrumentation.Amazonka (+ instrumentEnv,+ instrumentHooks,+) where++import Amazonka.Data.Text (toText)+import Amazonka.Env (Env, Env' (..))+import Amazonka.Env.Hooks (Finality (..), Hook, Hook_, Hooks (..))+import qualified Amazonka.Types as AWS+import Control.Applicative ((<|>))+import Control.Exception (onException)+import qualified Data.HashMap.Strict as HM+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import Data.Typeable (Proxy (..), Typeable, tyConName, typeRep, typeRepTyCon)+import qualified Network.HTTP.Client as HTTP+import Network.HTTP.Types (Header)+import qualified Network.HTTP.Types.Status as HTTP+import OpenTelemetry.Attributes (toAttribute)+import OpenTelemetry.Attributes.Key (unkey)+import OpenTelemetry.Context (insertSpan, lookupSpan)+import OpenTelemetry.Context.ThreadLocal (getAndAdjustContext, getContext)+import qualified OpenTelemetry.SemanticConventions as SC+import OpenTelemetry.Trace.Core (+ Span,+ SpanArguments (..),+ SpanKind (Client),+ SpanStatus (Error),+ Tracer,+ addAttributes,+ createSpanWithoutCallStack,+ defaultSpanArguments,+ endSpan,+ setStatus,+ )+import Prelude hiding (error)+++{- | Add OpenTelemetry tracing hooks to an Amazonka 'Env'.++All subsequent @send@ and @sendEither@ calls through the returned 'Env'+will automatically create spans with AWS SDK semantic convention attributes.++The tracing hooks compose with any existing hooks on the 'Env' — they do+not replace them.++@since 0.1.0.0+-}+instrumentEnv :: Tracer -> Env -> Env+instrumentEnv tracer env =+ env {hooks = instrumentHooks tracer (hooks env)}+++{- | Install tracing hooks on an Amazonka 'Hooks' value.++Lower-level than 'instrumentEnv' — use this if you need fine-grained+control over hook composition.++@since 0.1.0.0+-}+instrumentHooks :: Tracer -> Hooks -> Hooks+instrumentHooks tracer baseHooks =+ baseHooks+ { configuredRequest = tracingConfiguredRequest tracer (configuredRequest baseHooks)+ , clientResponse = tracingClientResponse (clientResponse baseHooks)+ , response = tracingResponse (response baseHooks)+ , error = tracingError (error baseHooks)+ }+++tracingConfiguredRequest+ :: forall a+ . (AWS.AWSRequest a, Typeable a)+ => Tracer+ -> Hook (AWS.Request a)+ -> Hook (AWS.Request a)+tracingConfiguredRequest tracer baseHook env req = do+ let svc = (AWS.service req :: AWS.Service)+ svcAbbrev = toText svc.abbrev+ opName = T.pack $ tyConName $ typeRepTyCon $ typeRep (Proxy @a)+ spanName = svcAbbrev <> "." <> opName+ region = AWS.fromRegion (Amazonka.Env.region env)+ endpoint_ = AWS.endpoint (AWS.service req) (Amazonka.Env.region env)+ host = TE.decodeUtf8 (AWS.host endpoint_)++ ctx <- getContext+ span <-+ createSpanWithoutCallStack tracer ctx spanName $+ defaultSpanArguments+ { kind = Client+ , attributes =+ HM.fromList+ [ (unkey SC.rpc_system, toAttribute ("aws-api" :: T.Text))+ , (unkey SC.rpc_service, toAttribute svcAbbrev)+ , (unkey SC.rpc_method, toAttribute opName)+ , (unkey SC.cloud_region, toAttribute region)+ , (unkey SC.server_address, toAttribute host)+ ]+ }++ _ <- getAndAdjustContext (insertSpan span)+ baseHook env req `onException` endSpan span Nothing+++tracingClientResponse+ :: forall a+ . (AWS.AWSRequest a, Typeable a)+ => Hook_ (AWS.Request a, AWS.ClientResponse ())+ -> Hook_ (AWS.Request a, AWS.ClientResponse ())+tracingClientResponse baseHook env arg@(_req, resp) = do+ ctx <- getContext+ case lookupSpan ctx of+ Just span -> do+ let sc = HTTP.statusCode (HTTP.responseStatus resp)+ mReqId = requestIdFromHeaders (HTTP.responseHeaders resp)+ attrs =+ HM.fromList $+ (unkey SC.http_response_statusCode, toAttribute sc)+ : case mReqId of+ Just reqId -> [(unkey SC.aws_requestId, toAttribute reqId)]+ Nothing -> []+ addAttributes span attrs+ Nothing -> pure ()+ baseHook env arg+++tracingResponse+ :: forall a+ . (AWS.AWSRequest a, Typeable a)+ => Hook_ (AWS.Request a, AWS.ClientResponse (AWS.AWSResponse a))+ -> Hook_ (AWS.Request a, AWS.ClientResponse (AWS.AWSResponse a))+tracingResponse baseHook env arg = do+ result <- baseHook env arg+ ctx <- getContext+ case lookupSpan ctx of+ Just span -> endSpan span Nothing+ Nothing -> pure ()+ pure result+++tracingError+ :: forall a+ . (AWS.AWSRequest a, Typeable a)+ => Hook_ (Finality, AWS.Request a, AWS.Error)+ -> Hook_ (Finality, AWS.Request a, AWS.Error)+tracingError baseHook env arg@(finality, _req, err) = do+ case finality of+ Final -> do+ ctx <- getContext+ case lookupSpan ctx of+ Just span -> do+ let errDesc = describeError err+ attrs =+ HM.fromList $+ (unkey SC.error_type, toAttribute errDesc)+ : case extractServiceRequestId err of+ Just reqId -> [(unkey SC.aws_requestId, toAttribute reqId)]+ Nothing -> []+ addAttributes span attrs+ setStatus span (Error errDesc)+ endSpan span Nothing+ Nothing -> pure ()+ NotFinal -> pure ()+ baseHook env arg+++requestIdFromHeaders :: [Header] -> Maybe T.Text+requestIdFromHeaders headers =+ fmap TE.decodeUtf8 (lookup "x-amzn-requestid" headers)+ <|> fmap TE.decodeUtf8 (lookup "x-amzn-request-id" headers)+ <|> fmap TE.decodeUtf8 (lookup "x-amz-request-id" headers)+++extractServiceRequestId :: AWS.Error -> Maybe T.Text+extractServiceRequestId (AWS.ServiceError svcErr) =+ AWS.fromRequestId <$> AWS.requestId svcErr+extractServiceRequestId _ = Nothing+++describeError :: AWS.Error -> T.Text+describeError (AWS.TransportError _) = "transport_error"+describeError (AWS.SerializeError serr) =+ T.pack (serr :: AWS.SerializeError).message+describeError (AWS.ServiceError svcErr) =+ let AWS.ErrorCode code = AWS.code svcErr+ in code
+ test/Spec.hs view
@@ -0,0 +1,2 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}+