packages feed

http-tower-hs-0.2.0.0: test/Network/HTTP/Tower/Middleware/TracingDockerSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE NamedFieldPuns #-}

module Network.HTTP.Tower.Middleware.TracingDockerSpec (spec) where

import Control.Concurrent (threadDelay)
import Control.Exception (try, SomeException)
import Data.Aeson (Value(..), (.:), decode, withObject)
import Data.Aeson.Types (parseMaybe)
import Data.Maybe (fromMaybe)
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Vector as V
import qualified Network.HTTP.Client as HTTP
import qualified Network.HTTP.Client.Internal as HTTP
import qualified Network.HTTP.Client.TLS as TLS
import qualified Network.HTTP.Types as HTTP
import System.Environment (setEnv, unsetEnv)
import System.Process (readProcess)
import Test.Hspec

import qualified TestContainers as TC
import TestContainers.Hspec (withContainers)

import OpenTelemetry.Trace.Core
  ( makeTracer
  , InstrumentationLibrary(..)
  , TracerOptions(..)
  , shutdownTracerProvider
  )
import OpenTelemetry.Attributes (emptyAttributes)
import OpenTelemetry.Trace (initializeGlobalTracerProvider)

import Network.HTTP.Tower.Client (HttpResponse)
import Tower.Service
import Network.HTTP.Tower.Middleware.Tracing

-- | Jaeger container setup via testcontainers.
data JaegerPorts = JaegerPorts
  { otlpPort   :: Int
  , jaegerPort :: Int
  }

setupJaeger :: TC.TestContainer JaegerPorts
setupJaeger = do
  container <- TC.run $ TC.containerRequest (TC.fromTag "jaegertracing/all-in-one:latest")
    TC.& TC.setExpose [4318, 16686]
    TC.& TC.setWaitingFor (TC.waitUntilTimeout 60 (TC.waitUntilMappedPortReachable 16686))
  pure JaegerPorts
    { otlpPort   = TC.containerPort container 4318
    , jaegerPort = TC.containerPort container 16686
    }

dockerAvailable :: IO Bool
dockerAvailable = do
  result <- try (readProcess "docker" ["info"] "") :: IO (Either SomeException String)
  pure $ case result of
    Right _ -> True
    Left _  -> False

queryJaegerTraces :: HTTP.Manager -> Int -> String -> IO (Maybe Value)
queryJaegerTraces mgr port service = do
  req <- HTTP.parseRequest $
    "http://localhost:" <> show port <> "/api/traces?service=" <> service <> "&limit=10"
  resp <- HTTP.httpLbs req mgr
  pure (decode (HTTP.responseBody resp))

-- | Poll Jaeger until traces appear or max attempts exhausted.
-- Returns the number of traces found.
pollJaegerTraces :: HTTP.Manager -> Int -> String -> Int -> IO Int
pollJaegerTraces mgr port service maxAttempts = go 0
  where
    go attempt
      | attempt >= maxAttempts = pure 0
      | otherwise = do
          threadDelay 2_000_000  -- 2 seconds between polls
          result <- queryJaegerTraces mgr port service
          let count = case result of
                Nothing -> 0
                Just val -> fromMaybe 0 $ parseMaybe (withObject "resp" $ \o -> do
                    arr <- o .: "data"
                    pure (V.length (arr :: V.Vector Value))) val
          if count > 0
            then pure count
            else go (attempt + 1)

spec :: Spec
spec = describe "Tracing Docker integration (Jaeger via testcontainers)" $ beforeAll dockerAvailable $ do

  it "exports spans to Jaeger via OTLP" $ \isAvailable -> do
    if not isAvailable
      then pendingWith "Docker not available, skipping Jaeger integration test"
      else withContainers setupJaeger $ \JaegerPorts{otlpPort, jaegerPort} -> do
        -- Configure OTel SDK via environment variables
        setEnv "OTEL_EXPORTER_OTLP_ENDPOINT" ("http://localhost:" <> show otlpPort)
        setEnv "OTEL_EXPORTER_OTLP_PROTOCOL" "http/protobuf"
        setEnv "OTEL_SERVICE_NAME" "http-tower-hs-tc-test"

        -- Initialize TracerProvider from env
        tp <- initializeGlobalTracerProvider

        let tracer = makeTracer tp
              (InstrumentationLibrary
                { libraryName = "http-tower-hs-tc-test"
                , libraryVersion = "0.1.0.0"
                , librarySchemaUrl = ""
                , libraryAttributes = emptyAttributes
                })
              (TracerOptions Nothing)

        -- Run a request through the tracing middleware
        let svc = Service $ \_ -> pure (Right fakeResponse)
            traced = withTracingTracer tracer svc
        req <- HTTP.parseRequest "http://example.com/testcontainers-test"
        _ <- runService traced req

        -- Shut down to flush all spans
        shutdownTracerProvider tp

        -- Clean up env vars
        unsetEnv "OTEL_EXPORTER_OTLP_ENDPOINT"
        unsetEnv "OTEL_EXPORTER_OTLP_PROTOCOL"
        unsetEnv "OTEL_SERVICE_NAME"

        -- Poll Jaeger until traces appear (up to 10 attempts, 2s apart = 20s max)
        mgr <- HTTP.newManager TLS.tlsManagerSettings
        traceCount <- pollJaegerTraces mgr jaegerPort "http-tower-hs-tc-test" 10
        traceCount `shouldSatisfy` (>= 1)

fakeResponse :: HttpResponse
fakeResponse = HTTP.Response
  { HTTP.responseStatus = HTTP.status200
  , HTTP.responseVersion = HTTP.http11
  , HTTP.responseHeaders = []
  , HTTP.responseBody = ""
  , HTTP.responseCookieJar = HTTP.createCookieJar []
  , HTTP.responseClose' = HTTP.ResponseClose (pure ())
  , HTTP.responseOriginalRequest = error "not used"
  , HTTP.responseEarlyHints = []
  }