{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module OpenTelemetry.Instrumentation.Yesod ( -- * Middleware functionality openTelemetryYesodMiddleware, RouteRenderer (..), mkRouteToRenderer, mkRouteToPattern, -- * Utilities rheSiteL, handlerEnvL, ) where import qualified Data.HashMap.Strict as H import Data.List (intercalate) import Data.Maybe (catMaybes) import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as T import Language.Haskell.TH.Syntax import Lens.Micro import Network.Wai (requestHeaders) import qualified OpenTelemetry.Context as Context import OpenTelemetry.Context.ThreadLocal import OpenTelemetry.Contrib.SpanTraversals import OpenTelemetry.Instrumentation.Wai (requestContext) import OpenTelemetry.Trace.Core hiding (inSpan, inSpan', inSpan'') import OpenTelemetry.Trace.Monad import UnliftIO.Exception import Yesod.Core import Yesod.Core.Types import Yesod.Routes.TH.Types handlerEnvL :: Lens' (HandlerData child site) (RunHandlerEnv child site) handlerEnvL = lens handlerEnv (\h e -> h {handlerEnv = e}) {-# INLINE handlerEnvL #-} rheSiteL :: Lens' (RunHandlerEnv child site) site rheSiteL = lens rheSite (\rhe new -> rhe {rheSite = new}) {-# INLINE rheSiteL #-} instance MonadTracer (HandlerFor site) where getTracer = do tp <- getGlobalTracerProvider OpenTelemetry.Trace.Core.getTracer tp "hs-opentelemetry-instrumentation-yesod" tracerOptions {- | Template Haskell to generate a function named routeToRendererFunction. For a route like HomeR, this function returns "HomeR". For routes with parents, this function returns e.g. "FooR.BarR.BazR". -} mkRouteToRenderer :: Name -> [ResourceTree a] -> Q [Dec] mkRouteToRenderer appName ress = do let fnName = mkName "routeToRenderer" t1 `arrow` t2 = ArrowT `AppT` t1 `AppT` t2 clauses <- mapM (goTree id []) ress pure [ SigD fnName ((ConT ''Route `AppT` ConT appName) `arrow` ConT ''Text) , FunD fnName $ concat clauses ] goTree :: (Pat -> Pat) -> [String] -> ResourceTree a -> Q [Clause] goTree front names (ResourceLeaf res) = pure <$> goRes front names res #if MIN_VERSION_template_haskell(2, 18, 0) goTree front names (ResourceParent name _check pieces trees) = concat <$> mapM (goTree front' newNames) trees where ignored = (replicate toIgnore WildP ++) . pure toIgnore = length $ filter isDynamic pieces isDynamic Dynamic {} = True isDynamic Static {} = False front' = front . ConP (mkName name) [] . ignored newNames = names <> [name] #else goTree front names (ResourceParent name _check pieces trees) = concat <$> mapM (goTree front' newNames) trees where ignored = (replicate toIgnore WildP ++) . pure toIgnore = length $ filter isDynamic pieces isDynamic Dynamic {} = True isDynamic Static {} = False front' = front . ConP (mkName name) . ignored newNames = names <> [name] #endif goRes :: (Pat -> Pat) -> [String] -> Resource a -> Q Clause goRes front names Resource {..} = pure $ Clause [front $ RecP (mkName resourceName) []] (NormalB $ toText $ intercalate "." (names <> [resourceName])) [] where toText s = VarE 'T.pack `AppE` LitE (StringL s) mkRouteToPattern :: Name -> [ResourceTree String] -> Q [Dec] #if MIN_VERSION_template_haskell(2, 18, 0) mkRouteToPattern appName ress = do let fnName = mkName "routeToPattern" t1 `arrow` t2 = ArrowT `AppT` t1 `AppT` t2 clauses <- mapM mkClause $ flatten ress pure [ SigD fnName ((ConT ''Route `AppT` ConT appName) `arrow` ConT ''Text) , FunD fnName clauses ] where toText s = VarE 'T.pack `AppE` LitE (StringL s) isDynamic Dynamic {} = True isDynamic Static {} = False parentPieceWrapper (parentName, pieces) nestedPat = ConP (mkName parentName) [] $ mconcat [ replicate (length $ filter isDynamic pieces) WildP , [nestedPat] ] mkClause fr@FlatResource{..} = do let clausePattern = foldr parentPieceWrapper (RecP (mkName frName) []) frParentPieces pure $ Clause [clausePattern] (NormalB $ toText $ renderPattern fr) [] #else mkRouteToPattern appName ress = do let fnName = mkName "routeToPattern" t1 `arrow` t2 = ArrowT `AppT` t1 `AppT` t2 clauses <- mapM mkClause $ flatten ress pure [ SigD fnName ((ConT ''Route `AppT` ConT appName) `arrow` ConT ''Text) , FunD fnName clauses ] where toText s = VarE 'T.pack `AppE` LitE (StringL s) isDynamic Dynamic {} = True isDynamic Static {} = False parentPieceWrapper (parentName, pieces) nestedPat = ConP (mkName parentName) $ mconcat [ replicate (length $ filter isDynamic pieces) WildP , [nestedPat] ] mkClause fr@FlatResource{..} = do let clausePattern = foldr parentPieceWrapper (RecP (mkName frName) []) frParentPieces pure $ Clause [clausePattern] (NormalB $ toText $ renderPattern fr) [] #endif renderPattern :: FlatResource String -> String renderPattern FlatResource {..} = concat $ concat [ if frCheck then [] else ["!"] , case formattedParentPieces <> concatMap routePortionSection frPieces of [] -> ["/"] pieces -> pieces , case frDispatch of Methods {..} -> concat [ case methodsMulti of Nothing -> [] Just t -> ["/+", t] ] Subsite {} -> [] ] where routePortionSection :: Piece String -> [String] routePortionSection (Static t) = ["/", t] routePortionSection (Dynamic t) = ["/#{", t, "}"] formattedParentPieces :: [String] formattedParentPieces = do (_parentName, pieces) <- frParentPieces piece <- pieces routePortionSection piece data RouteRenderer site = RouteRenderer { nameRender :: Route site -> T.Text , pathRender :: Route site -> T.Text } -- TODO figure out a way to get better code locations for these spans. -- | This middleware works best when used with `OpenTelemetry.Instrumentation.Wai` middleware. openTelemetryYesodMiddleware :: (ToTypedContent res) => RouteRenderer site -> HandlerFor site res -> HandlerFor site res openTelemetryYesodMiddleware rr m = do req <- waiRequest mr <- getCurrentRoute let mspan = requestContext req >>= Context.lookupSpan sharedAttributes = H.fromList $ ("http.framework", toAttribute ("yesod" :: Text)) : catMaybes [ do r <- mr pure ("http.route", toAttribute $ pathRender rr r) , do r <- mr pure ("http.handler", toAttribute $ nameRender rr r) , do ff <- lookup "X-Forwarded-For" $ requestHeaders req pure ("http.client_ip", toAttribute $ T.decodeUtf8 ff) ] args = defaultSpanArguments { kind = maybe Server (const Internal) mspan , attributes = sharedAttributes } case mspan of Nothing -> do eResult <- inSpan' (maybe "notFound" (\r -> nameRender rr r) mr) args $ \_s -> do catch (Right <$> m) $ \e -> do -- We want to mark the span as an error if it's an InternalError, -- the other HCError values are 4xx status codes which don't -- really count as a server error in OpenTelemetry spec parlance. case e of HCError (InternalError _) -> throwIO e _ -> pure () pure (Left (e :: HandlerContents)) case eResult of Left hc -> throwIO hc Right normal -> pure normal Just waiSpan -> do addAttributes waiSpan sharedAttributes m