packages feed

langchain-hs-0.0.5.0: test/Test/Langchain/ObservabilitySpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.Langchain.ObservabilitySpec (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 "Retriever" "Slow response from vector store"
            logError logHandler "Model" "Rate limit reached"

            logs <- getInMemoryLogs logger
            case logs of
              (firstLog : _) -> do
                length logs @?= 3
                logLevel firstLog @?= InfoLevel
                logMessage firstLog @?= "Starting agent turn"
              [] -> assertFailure "Expected logs to be non-empty"
        , 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 meta = Map.fromList [("model", "qwen2.5:7b"), ("tokens", "128")]
            logEvent logHandler InfoLevel "Provider" "Model invocation complete" meta

            logs <- getInMemoryLogs logger
            case logs of
              [firstLog] -> logMetadata firstLog @?= meta
              _ -> assertFailure ("Expected exactly 1 log, got " ++ show (length logs))
        ]
    , testGroup
        "OpenTelemetry Tracing"
        [ testCase "withSpan wraps computation, records duration and Ok status" $ do
            tracer <- newOTelTracer (Just "trace-100")
            res <- runExceptT $ withSpan tracer "llm_invoke" Nothing ClientSpan (Map.singleton "provider" "ollama") $ do
              pure ("success response" :: T.Text)
            res @?= Right "success response"

            spans <- getSpans tracer
            case spans of
              [sp] -> do
                spanName sp @?= "llm_invoke"
                spanTraceId sp @?= "trace-100"
                spanStatus sp @?= StatusOk
                assertBool "Duration recorded" (isJust (spanDurationMicros sp))
              _ -> assertFailure ("Expected exactly 1 span, got " ++ show (length spans))
        , testCase "exportSpansJson exports valid JSON formatted trace" $ do
            tracer <- newOTelTracer (Just "trace-export")
            _ <- startSpan tracer "step1" Nothing InternalSpan Map.empty
            jsonText <- exportSpansJson tracer
            assertBool "Contains span name" ("step1" `T.isInfixOf` jsonText)
            assertBool "Contains trace-export" ("trace-export" `T.isInfixOf` jsonText)
        ]
    ]