Webapp-Example: Main.hs
Haskell
Code
Wie man das verwendet, siehe Webapp-Example.
{-# OPTIONS_GHC -Wno-name-shadowing #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
module MyService where
-- generische imports aus den dependencies/base, nicht in der prelude
import Codec.MIME.Type
import Configuration.Dotenv as Dotenv
import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Monad
import Control.Monad.Catch
import Control.Monad.Except
import Conversion
import Conversion.Text ()
import Data.Binary.Builder
import Data.String (IsString (..))
import Data.Time
import Data.Time.Clock
import Data.Time.Format
import Data.Default
import Network.HostName
import Network.HTTP.Client as HTTP hiding
(withConnection)import Network.HTTP.Types (Status, statusCode)
import Network.Mom.Stompl.Client.Queue
import Network.Wai (Middleware)
import Network.Wai.Logger
import Network.Wai.Middleware.Cors
import Network.Wai.Middleware.RequestLogger (OutputFormat (..),
logStdout,
mkRequestLogger,
outputFormat)import Servant.Client (mkClientEnv,
parseBaseUrl)import System.Directory
import System.Envy
import System.IO
import System.Log.FastLogger
import Text.PrettyPrint.GenericPretty
-- generische imports, aber qualified, weil es sonst zu name-clashes kommt
import qualified Data.ByteString as BS
-- import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as LBS
import qualified Network.HTTP.Client.TLS as UseDefaultHTTPSSettings (tlsManagerSettings)
import qualified Network.Mom.Stompl.Client.Queue as AMQ
import qualified Network.Wai as WAI
-- Handler für den MyServiceBackend-Typen und Imports aus den Libraries
import MyService.Handler as H -- handler der H.myApiEndpointV1Post implementiert
import MyService.Types -- weitere Type (s. nächste box)
import MyServiceGen.API as MS -- aus der generierten library
myServicemain :: IO ()
= do
myServicemain -- .env-Datei ins Prozess-Environment laden, falls noch nicht von außen gesetzt
$ loadFile $ Dotenv.Config [".env"] [] False
void -- Config holen (defaults + overrides aus dem Environment)
@ServerConfig{..} <- decodeWithDefaults defConfig
sc-- Backend-Setup
-- legt sowas wie Proxy-Server fest und wo man wie dran kommt. Benötigt für das Sprechen mit anderen Microservices
let defaultHTTPSSettings = UseDefaultHTTPSSettings.tlsManagerSettings { managerResponseTimeout = responseTimeoutMicro $ 1000 * 1000 * myserviceMaxTimeout }
= do
createBackend url proxy <- newManager . managerSetProxy proxy
manager $ defaultHTTPSSettings
<- parseBaseUrl url
url' return (mkClientEnv manager url')
= case myserviceInternalProxyUrl of
internalProxy "" -> noProxy
-> useProxy $ HTTP.Proxy (fromString url) myserviceInternalProxyPort
url -- externalProxy = case myserviceExternalProxyUrl of
-- "" -> noProxy
-- url -> useProxy $ HTTP.Proxy (fromString url) myserviceExternalProxyPort
-- Definieren & Erzeugen der Funktionen um die anderen Services anzusprechen.
<- (,)
calls <$> createBackend myserviceAUri internalProxy
<*> createBackend myserviceBUri internalProxy
-- Logging-Setup
LineBuffering
hSetBuffering stdout LineBuffering
hSetBuffering stderr
-- Infos holen, brauchen wir später
<- getHostName
myName <- formatTime defaultTimeLocale "%F" . utctDay <$> getCurrentTime
today
-- activeMQ-Transaktional-Queue zum schreiben nachher vorbereiten
<- newTQueueIO
amqPost
-- bracket a b c == erst a machen, ergebnis an c als variablen übergeben. Schmeisst c ne exception/wird gekillt/..., werden die variablen an b übergeben.
bracket-- logfiles öffnen
LogFiles <$> openFile ("/logs/myservice-"<>myName<>"-"<>today<>".info") AppendMode
(<*> openFile (if myserviceDebug then "/logs/myservice-"<>myName<>"-"<>today<>".debug" else "/dev/null") AppendMode
<*> openFile ("/logs/myservice-"<>myName<>"-"<>today<>".error") AppendMode
<*> openFile ("/logs/myservice-"<>myName<>"-"<>today<>".timings") AppendMode
)-- und bei exception/beendigung schlißen.h
LogFiles a b c d) -> mapM_ hClose [a,b,c,d])
(\($ \logfiles -> do
-- logschreibe-funktionen aliasen; log ist hier abstrakt, iolog spezialisiert auf io.
let log = printLogFiles logfiles :: MonadIO m => [LogItem] -> m ()
= printLogFilesIO logfiles :: [LogItem] -> IO ()
iolog
-- H.myApiEndpointV1Post ist ein Handler (alle Handler werden mit alias H importiert) und in einer eigenen Datei
-- Per Default bekommen Handler sowas wie die server-config, die Funktionen um mit anderen Services zu reden, die AMQ-Queue um ins Kibana zu loggen und eine Datei-Logging-Funktion
-- Man kann aber noch viel mehr machen - z.b. gecachte Daten übergeben, eine Talk-Instanz, etc. pp.
= MyServiceBackend{ myApiEndpointV1Post = H.myApiEndpointV1Post sc calls amqPost log
server
}= MS.Config $ "http://" ++ myserviceHost ++ ":" ++ show myservicePort ++ "/"
config . pure . Info $ "Using Server configuration:"
iolog . pure . Info $ pretty sc { myserviceActivemqPassword = "******" -- Do NOT log the password ;)
iolog = "******"
, myserviceMongoPassword
}-- alle Services starten (Hintergrund-Aktionen wie z.b. einen MongoDB-Dumper, einen Talk-Server oder wie hier die ActiveMQ
$ forkIO $ keepActiveMQConnected sc iolog amqPost
void -- logging-Framework erzeugen
<- loggingMiddleware
loggingMW -- server starten
if myserviceDebug
then runMyServiceMiddlewareServer config (cors (\_ -> Just (simpleCorsResourcePolicy {corsRequestHeaders = ["Content-Type"]})) . loggingMW . logStdout) server
else runMyServiceMiddlewareServer config (cors (\_ -> Just (simpleCorsResourcePolicy {corsRequestHeaders = ["Content-Type"]}))) server
-- Sollte bald in die Library hs-stomp ausgelagert werden
-- ist ein Beispiel für einen ActiveMQ-Dumper
keepActiveMQConnected :: ServerConfig -> ([LogItem] -> IO ()) -> TQueue BS.ByteString -> IO ()
@ServerConfig{..} printLog var = do
keepActiveMQConnected sc<- handle (\(e :: SomeException) -> do
res . pure . Error $ "Exception in AMQ-Thread: "<>show e
printLog return $ Right ()
$ AMQ.try $ do -- catches all AMQ-Exception that we can handle. All others bubble up.
) . pure . Info $ "AMQ: connecting..."
printLog OAuth myserviceActivemqUsername myserviceActivemqPassword
withConnection myserviceActivemqHost myserviceActivemqPort [ OTmo (30*1000) {- 30 sec timeout -}
,
]$ \c -> do
[] let oconv = return
. pure . Info $ "AMQ: connected"
printLog "Chaos-Logger for Kibana" "chaos.logs" [] [] oconv $ \writer -> do
withWriter c . pure . Info $ "AMQ: queue created"
printLog let postfun = writeQ writer (Type (Application "json") []) []
$ race
void $ atomically (readTQueue var) >>= postfun)
(forever 600*1000*1000)) -- wait 10 Minutes
(threadDelay (-- close writer
-- close connection
-- get outside of all try/handle/...-constructions befor recursing.
case res of
Left ex -> do
. pure . Error $ "AMQ: "<>show ex
printLog
keepActiveMQConnected sc printLog varRight _ -> keepActiveMQConnected sc printLog var
-- Beispiel für eine Custom-Logging-Middleware.
-- Hier werden z.B. alle 4xx-Status-Codes inkl. Payload ins stdout-Log geschrieben.
-- Nützlich, wenn die Kollegen ihre Requests nicht ordentlich schreiben können und der Server das Format zurecht mit einem BadRequest ablehnt ;)
loggingMiddleware :: IO Middleware
= liftIO $ mkRequestLogger $ def { outputFormat = CustomOutputFormatWithDetails out }
loggingMiddleware where
out :: ZonedDate -> WAI.Request -> Status -> Maybe Integer -> NominalDiffTime -> [BS.ByteString] -> Builder -> LogStr
out _ r status _ _ payload _| statusCode status < 300 = ""
| statusCode status > 399 && statusCode status < 500 = "Error code "<>toLogStr (statusCode status) <>" sent. Request-Payload was: "<> mconcat (toLogStr <$> payload) <> "\n"
| otherwise = toLogStr (show r) <> "\n"