packages feed

core-webserver-warp (empty) → 0.1.1.0

raw patch · 3 files changed

+309/−0 lines, 3 filesdep +asyncdep +basedep +bytestring

Dependencies added: async, base, bytestring, core-data, core-program, core-telemetry, core-text, http-types, http2, mtl, safe-exceptions, vault, wai, warp

Files

+ LICENSE view
@@ -0,0 +1,19 @@+Copyright © 2018-2021 Athae Eredh Siniath and Others++Permission is hereby granted, free of charge, to any person obtaining a+copy of this software and associated documentation files (the "Software"),+to deal in the Software without restriction, including without limitation+the rights to use, copy, modify, merge, publish, distribute, sublicense,+and/or sell copies of the Software, and to permit persons to whom the+Software is furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER+DEALINGS IN THE SOFTWARE.
+ core-webserver-warp.cabal view
@@ -0,0 +1,54 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.34.4.+--+-- see: https://github.com/sol/hpack++name:           core-webserver-warp+version:        0.1.1.0+synopsis:       Interoperability with Wai/Warp+description:    This is part of a library to help build command-line programs, both tools and+                longer-running daemons.+                .+                This package in particular adds wrappers around the __wai__ (Web Application+                Interface) and __warp__ (Web Server to facilitate integrating this commonly+                used webserver combination with the Program monad from __core-program__.+category:       System+stability:      experimental+homepage:       https://github.com/aesiniath/unbeliever#readme+bug-reports:    https://github.com/aesiniath/unbeliever/issues+author:         Andrew Cowie <istathar@gmail.com>+maintainer:     Andrew Cowie <istathar@gmail.com>+copyright:      © 2021-2022 Athae Eredh Siniath and Others+license:        MIT+license-file:   LICENSE+build-type:     Simple+tested-with:+    GHC == 8.10.7++source-repository head+  type: git+  location: https://github.com/aesiniath/unbeliever++library+  exposed-modules:+      Core.Webserver.Warp+  hs-source-dirs:+      lib+  ghc-options: -Wall -Wwarn -fwarn-tabs+  build-depends:+      async+    , base >=4.11 && <5+    , bytestring+    , core-data+    , core-program+    , core-telemetry >=0.1.8+    , core-text+    , http-types+    , http2+    , mtl+    , safe-exceptions+    , vault+    , wai+    , warp+  default-language: Haskell2010
+ lib/Core/Webserver/Warp.hs view
@@ -0,0 +1,236 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# OPTIONS_HADDOCK prune #-}++{- |+Many programs present their interface in the form of a webservice, be it+internet-facing, for internal use, or even just as a machine-local daemon. The+Haskell language has numerous frameworks for building webservices and a number+of high-quality HTTP server implementations. This module provides support for+the Web Application Interface from the __wai__ package and the __warp__+webserver.++Given an 'Network.Wai.Application' type (the definition of your web service) and a+'Network.Wai.Middleware' (which is just @Application -> Application@), run a the+"Core.Program"'s 'Core.Program.Execute.Program' monad. Metrics values (aka web+server logs) will be sent as key/value pairs via "Core.Telemetry".++= Usage++First set up your program and initialize the telemetry subsystem.++@+import "Core.Program"+import "Core.Telemetry"+import "Core.Webservice.Warp"++main :: 'IO' ()+main = do+    context <- 'Core.Program.Execute.configure' \"1.0\" 'Core.Program.Execute.None' ('Core.Program.Arguments.simpleConfig' [])+    context' <- 'initializeTelemetry' ['Core.Telemetry.Console.consoleExporter', 'Core.Telemetry.Structured.structuredExporter', 'Core.Telemetry.Honeycomb.honeycombExporter'] context+    'Core.Program.Execute.executeWith' context' \$ do+        'Core.Program.Logging.info' \"Starting...\"+        'launchWebserver' 8080 application+@++You can then describe your webservice 'Application', for example++@+application :: 'Application'+application = request sendResponse =+    sendResponse ('Network.WAI.responseLBS' 'Network.HTTP.Types.status200' [] \"Hello World\")+@++performs the heroic duty of replying to you with the given string. In+practice, if you're using something like __servant__ to define the shape of+your webservice its 'Servant.serve' function will give you the 'Application'+you're trying to run.++Logging output is sent to the telemtry channel. If you run your program with+the console exporter, and hit something like+<http://localhost:8080/hello?question=answer> will see something like this:++@+\$ __hello-service --telemetry=console__+03:16:01Z (00.002) Starting...+03:16:04Z (00.259)                                             <-- this is the request duration, 259 ms+/hello:                                                        <-- the base of the context path aka \"endpoint\"+  request.method = \"GET\"+  request.path = "/hello?question=answer"                      <-- the full context path with query string+  response.status_code = "200"+@++This is useful for debugging during development but for production you are+recommended to use the structured logging output or to send the traces to an+observability service; this will be the root span of a trace.+-}+module Core.Webserver.Warp (+    Port,+    launchWebserver,+    requestContextKey,+    contextFromRequest,+) where++--+-- We follow the convention used elsewhere in this collection of libraries of+-- using a qualified name for the imports of significant libraries. It's a bit+-- cumbersome, but makes it easier to disambiguate what's going on when+-- comparing to almost identical code in sibling modules covering other+-- webserver frameworks.+--++import qualified Control.Exception.Safe as Safe (catch)+import Core.Program.Context+import Core.Program.Logging+import Core.System.Base+import Core.Telemetry.Observability+import Core.Text.Rope+import qualified Data.ByteString.Lazy as L+import qualified Data.Vault.Lazy as Vault+import Network.HTTP.Types (+    hContentType,+    status400,+    status413,+    status431,+    status500,+    statusCode,+ )+import Network.HTTP2.Frame (+    ErrorCodeId (UnknownErrorCode),+    HTTP2Error (ConnectionError),+ )+import Network.Wai+import Network.Wai.Handler.Warp (InvalidRequest, Port)+import qualified Network.Wai.Handler.Warp as Warp++{- |+Given a WAI 'Application', run a Warp webserver on the specified port from+within the 'Program' monad.+-}+launchWebserver :: Port -> Application -> Program τ ()+launchWebserver port application = do+    context <- getContext+    let settings =+            Warp.setOnException+                (onExceptionHandler context)+                . Warp.setPort port+                $ Warp.defaultSettings+    liftIO $ do+        Warp.runSettings+            settings+            ( loggingMiddleware+                context+                application+            )++requestContextKey :: forall t. Vault.Key (Context t)+requestContextKey = unsafePerformIO Vault.newKey+{-# NOINLINE requestContextKey #-}++contextFromRequest :: forall t. Request -> Maybe (Context t)+contextFromRequest request = Vault.lookup requestContextKey (vault request)++-- which is IO+loggingMiddleware :: Context τ -> Application -> Application+loggingMiddleware (context0 :: Context τ) application request sendResponse = do+    let path = intoRope (rawPathInfo request)++    subProgram context0 $ do+        beginTrace $ do+            encloseSpan path $ do+                context1 <- getContext++                -- we could call `telemetry` here with these values, but since+                -- we call into nested actions which could clear the state+                -- without starting a new span, we duplicate adding them below+                -- to ensure they get passed through.++                let query = intoRope (rawQueryString request)+                    path' = path <> query+                    method = intoRope (requestMethod request)++                liftIO $ do+                    -- The below wires the context in the request's `vault`. As the type of+                    -- `Context` is polymorphic to support user data, we have to use a type+                    -- application to make sure that consumers can later fetch the appropriate+                    -- `Context t`.+                    let vault' = Vault.insert (requestContextKey @τ) context1 (vault request)+                        request' = request{vault = vault'}+                    Safe.catch+                        ( application request' $ \response -> do+                            -- accumulate the details for logging+                            let status = intoRope (show (statusCode (responseStatus response)))++                            subProgram context1 $ do+                                telemetry+                                    [ metric "request.method" method+                                    , metric "request.path" path'+                                    , metric "response.status_code" status+                                    ]++                            -- actually handle the request+                            sendResponse response+                        )+                        ( \(e :: SomeException) -> do+                            -- set the magic `error` field with the exception text.+                            let text = intoRope (displayException e)+                            subProgram context1 $ do+                                warn "Trapped internal exception"+                                debug "e" text+                                telemetry+                                    [ metric "request.method" method+                                    , metric "request.path" path'+                                    , metric "error" text+                                    ]++                            sendResponse (onExceptionResponse e)+                        )++onExceptionResponse :: SomeException -> Response+onExceptionResponse e+    | Just (_ :: InvalidRequest) <-+        fromException e =+        responseLBS+            status400+            [(hContentType, "text/plain; charset=utf-8")]+            (fromRope ("Bad Request\n" <> intoRope (displayException e)))+    | Just (ConnectionError (UnknownErrorCode 413) t) <-+        fromException e =+        responseLBS+            status413+            [(hContentType, "text/plain; charset=utf-8")]+            (L.fromStrict t)+    | Just (ConnectionError (UnknownErrorCode 431) t) <-+        fromException e =+        responseLBS+            status431+            [(hContentType, "text/plain; charset=utf-8")]+            (L.fromStrict t)+    | otherwise =+        responseLBS+            status500+            [(hContentType, "text/plain; charset=utf-8")]+            "Internal Server Error"++--+-- Ideally this would be a catch-all and not be hit; our application wrapper+-- should have caught this beforehand. However, it turns out "Bad Request"+-- type protocol problems that don't even result in a coherent request -+-- which, sadly, are somewhat to be expected on the wild and wooly internet.+-- So we note them briefly and move on.+--+-- Much more interesting are exceptions which occur within the request path,+-- which means that we can annotate the current span with an `error` field and+-- send it down the telemetry channel.+--+onExceptionHandler :: Context τ -> Maybe Request -> SomeException -> IO ()+onExceptionHandler context possibleRequest e = do+    subProgram context $ do+        critical "Exception escaped webserver"+        debugS "e" e+        case possibleRequest of+            Nothing -> pure ()+            Just request ->+                let line = intoRope (requestMethod request) <> " " <> intoRope (rawPathInfo request) <> intoRope (rawQueryString request)+                 in debug "request" line