{-# OPTIONS_GHC -Wno-name-shadowing #-}{-# LANGUAGE FlexibleContexts #-}{-# LANGUAGE LambdaCase #-}{-# LANGUAGE OverloadedStrings #-}{-# LANGUAGE RankNTypes #-}{-# LANGUAGE RecordWildCards #-}{-# LANGUAGE ScopedTypeVariables #-}moduleMyServicewhere-- generische imports aus den dependencies/base, nicht in der preludeimportCodec.MIME.TypeimportConfiguration.DotenvasDotenvimportControl.Concurrent(forkIO,threadDelay)importControl.Concurrent.AsyncimportControl.Concurrent.STMimportControl.MonadimportControl.Monad.CatchimportControl.Monad.ExceptimportConversionimportConversion.Text()importData.Binary.BuilderimportData.String(IsString(..))importData.TimeimportData.Time.ClockimportData.Time.FormatimportData.DefaultimportNetwork.HostNameimportNetwork.HTTP.ClientasHTTPhiding(withConnection)importNetwork.HTTP.Types(Status,statusCode)importNetwork.Mom.Stompl.Client.QueueimportNetwork.Wai(Middleware)importNetwork.Wai.LoggerimportNetwork.Wai.Middleware.CorsimportNetwork.Wai.Middleware.RequestLogger(OutputFormat(..),logStdout,mkRequestLogger,outputFormat)importServant.Client(mkClientEnv,parseBaseUrl)importSystem.DirectoryimportSystem.EnvyimportSystem.IOimportSystem.Log.FastLoggerimportText.PrettyPrint.GenericPretty-- generische imports, aber qualified, weil es sonst zu name-clashes kommtimportqualifiedData.ByteStringasBS-- import qualified Data.ByteString.Char8 as BS8importqualifiedData.ByteString.LazyasLBSimportqualifiedNetwork.HTTP.Client.TLSasUseDefaultHTTPSSettings(tlsManagerSettings)importqualifiedNetwork.Mom.Stompl.Client.QueueasAMQimportqualifiedNetwork.WaiasWAI-- Handler für den MyServiceBackend-Typen und Imports aus den LibrariesimportMyService.HandlerasH-- handler der H.myApiEndpointV1Post implementiertimportMyService.Types-- weitere Type (s. nächste box)importMyServiceGen.APIasMS-- aus der generierten librarymyServicemain::IO()myServicemain=do-- .env-Datei ins Prozess-Environment laden, falls noch nicht von außen gesetztvoid$loadFile$Dotenv.Config[".env"][]False-- Config holen (defaults + overrides aus dem Environment)sc@ServerConfig{..}<-decodeWithDefaultsdefConfig-- Backend-Setup-- legt sowas wie Proxy-Server fest und wo man wie dran kommt. Benötigt für das Sprechen mit anderen MicroservicesletdefaultHTTPSSettings=UseDefaultHTTPSSettings.tlsManagerSettings{managerResponseTimeout=responseTimeoutMicro$1000*1000*myserviceMaxTimeout}createBackendurlproxy=domanager<-newManager.managerSetProxyproxy$defaultHTTPSSettingsurl'<-parseBaseUrlurlreturn(mkClientEnvmanagerurl')internalProxy=casemyserviceInternalProxyUrlof""->noProxyurl->useProxy$HTTP.Proxy(fromStringurl)myserviceInternalProxyPort-- externalProxy = case myserviceExternalProxyUrl of-- "" -> noProxy-- url -> useProxy $ HTTP.Proxy (fromString url) myserviceExternalProxyPort-- Definieren & Erzeugen der Funktionen um die anderen Services anzusprechen.calls<-(,)<$>createBackendmyserviceAUriinternalProxy<*>createBackendmyserviceBUriinternalProxy-- Logging-SetuphSetBufferingstdoutLineBufferinghSetBufferingstderrLineBuffering-- Infos holen, brauchen wir spätermyName<-getHostNametoday<-formatTimedefaultTimeLocale"%F".utctDay<$>getCurrentTime-- activeMQ-Transaktional-Queue zum schreiben nachher vorbereitenamqPost<-newTQueueIO-- 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(ifmyserviceDebugthen"/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(\(LogFilesabcd)->mapM_hClose[a,b,c,d])$\logfiles->do-- logschreibe-funktionen aliasen; log ist hier abstrakt, iolog spezialisiert auf io.letlog=printLogFileslogfiles::MonadIOm=>[LogItem]->m()iolog=printLogFilesIOlogfiles::[LogItem]->IO()-- 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.server=MyServiceBackend{myApiEndpointV1Post=H.myApiEndpointV1PostsccallsamqPostlog}config=MS.Config$"http://"++myserviceHost++":"++showmyservicePort++"/"iolog.pure.Info$"Using Server configuration:"iolog.pure.Info$prettysc{myserviceActivemqPassword="******"-- Do NOT log the password ;),myserviceMongoPassword="******"}-- alle Services starten (Hintergrund-Aktionen wie z.b. einen MongoDB-Dumper, einen Talk-Server oder wie hier die ActiveMQvoid$forkIO$keepActiveMQConnectedsciologamqPost-- logging-Framework erzeugenloggingMW<-loggingMiddleware-- server startenifmyserviceDebugthenrunMyServiceMiddlewareServerconfig(cors(\_->Just(simpleCorsResourcePolicy{corsRequestHeaders=["Content-Type"]})).loggingMW.logStdout)serverelserunMyServiceMiddlewareServerconfig(cors(\_->Just(simpleCorsResourcePolicy{corsRequestHeaders=["Content-Type"]})))server-- Sollte bald in die Library hs-stomp ausgelagert werden-- ist ein Beispiel für einen ActiveMQ-DumperkeepActiveMQConnected::ServerConfig->([LogItem]->IO())->TQueueBS.ByteString->IO()keepActiveMQConnectedsc@ServerConfig{..}printLogvar=dores<-handle(\(e::SomeException)->doprintLog.pure.Error$"Exception in AMQ-Thread: "<>showereturn$Right())$AMQ.try$do-- catches all AMQ-Exception that we can handle. All others bubble up.printLog.pure.Info$"AMQ: connecting..."withConnectionmyserviceActivemqHostmyserviceActivemqPort[OAuthmyserviceActivemqUsernamemyserviceActivemqPassword,OTmo(30*1000){- 30 sec timeout -}][]$\c->doletoconv=returnprintLog.pure.Info$"AMQ: connected"withWriterc"Chaos-Logger for Kibana""chaos.logs"[][]oconv$\writer->doprintLog.pure.Info$"AMQ: queue created"letpostfun=writeQwriter(Type(Application"json")[])[]void$race(forever$atomically(readTQueuevar)>>=postfun)(threadDelay(600*1000*1000))-- wait 10 Minutes-- close writer-- close connection-- get outside of all try/handle/...-constructions befor recursing.caseresofLeftex->doprintLog.pure.Error$"AMQ: "<>showexkeepActiveMQConnectedscprintLogvarRight_->keepActiveMQConnectedscprintLogvar-- 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::IOMiddlewareloggingMiddleware=liftIO$mkRequestLogger$def{outputFormat=CustomOutputFormatWithDetailsout}whereout::ZonedDate->WAI.Request->Status->MaybeInteger->NominalDiffTime->[BS.ByteString]->Builder->LogStrout_rstatus__payload_|statusCodestatus<300=""|statusCodestatus>399&&statusCodestatus<500="Error code "<>toLogStr(statusCodestatus)<>" sent. Request-Payload was: "<>mconcat(toLogStr<$>payload)<>"\n"|otherwise=toLogStr(showr)<>"\n"