packages feed

langchain-hs-core-0.0.6.0: test/Test/Langchain/Observability.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.Langchain.Observability (tests) where

import Control.Concurrent.STM (atomically, modifyTVar')
import Control.Monad.Except (runExceptT)
import qualified Data.Map.Strict as Map
import Data.Maybe (isJust)
import qualified Data.Text as T
import Test.Tasty
import Test.Tasty.HUnit

import Langchain.Observability

tests :: TestTree
tests =
  testGroup
    "Langchain.Observability"
    [ testGroup
        "Structured Logging"
        [ testCase "InMemoryLogger records events and respects minLevel" $ do
            logger <- newInMemoryLogger InfoLevel
            let logHandler =
                  Logger
                    { minLevel = InfoLevel
                    , writeLog = \ev -> do
                        atomically $ modifyTVar' (inMemoryVar logger) (\ls -> ls ++ [ev])
                    }
            logDebug logHandler "Agent" "This debug log should be ignored"
            logInfo logHandler "Agent" "Starting agent turn"
            logWarn logHandler "Agent" "Model rate limit approaching"
            logError logHandler "Agent" "Tool execution failed"

            logs <- getInMemoryLogs logger
            length logs @?= 3
            map logLevel logs @?= [InfoLevel, WarnLevel, ErrorLevel]
        , testCase "logEvent attaches custom metadata" $ do
            logger <- newInMemoryLogger DebugLevel
            let logHandler =
                  Logger
                    { minLevel = DebugLevel
                    , writeLog = \ev -> do
                        atomically $ modifyTVar' (inMemoryVar logger) (\ls -> ls ++ [ev])
                    }
            let customMeta = Map.fromList [("runId", "run-123"), ("attempt", "1")]
            logEvent logHandler DebugLevel "LLM" "Prompt sent" customMeta
            logs <- getInMemoryLogs logger
            case logs of
              [ev] -> do
                logComponent ev @?= "LLM"
                logMetadata ev @?= customMeta
              _ -> assertFailure "Expected exactly 1 log"
        ]
    , testGroup
        "OpenTelemetry Tracing"
        [ testCase "withSpan wraps computation, records duration and Ok status" $ do
            tracer <- newOTelTracer (Just "trace-unit-1")
            res <-
              runExceptT $
                withSpan tracer "test-operation" Nothing InternalSpan Map.empty (pure ("success" :: String))
            res @?= Right "success"

            spans <- getSpans tracer
            case spans of
              [sp] -> do
                spanName sp @?= "test-operation"
                spanTraceId sp @?= "trace-unit-1"
                spanStatus sp @?= StatusOk
                isJust (spanEndTime sp) @? "Expected span end time"
                isJust (spanDurationMicros sp) @? "Expected duration"
              _ -> assertFailure "Expected exactly 1 span"
        , testCase "exportSpansJson exports valid JSON formatted trace" $ do
            tracer <- newOTelTracer (Just "trace-unit-2")
            _ <- runExceptT $ withSpan tracer "child-span" Nothing ClientSpan Map.empty (pure (42 :: Int))
            jsonOutput <- exportSpansJson tracer
            "child-span" `T.isInfixOf` jsonOutput @? "Expected JSON to contain span name"
            "trace-unit-2" `T.isInfixOf` jsonOutput @? "Expected JSON to contain trace ID"
        ]
    ]