packages feed

hunt-server-0.3.0.1: src/Hunt/Server.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE CPP #-}
-- http://ghc.haskell.org/trac/ghc/blog/LetGeneralisationInGhc7
-- {-# LANGUAGE NoMonoLocalBinds  #-}

-- ----------------------------------------------------------------------------
{- |
  The Hunt server.

  Routes:

    [@POST \/eval@]                          Evaluates 'Command's.

    [@GET  \/search\/:query\/@]              Search (unlimited # of results).

    [@GET  \/search\/:query\/:offset\/:mx@]  Search with pagination.

    [@GET  \/weight\/:query\/@]              Search and return weights of documents

    [@GET  \/completion\/:query\/:mx@]       Word completions with maximum.

    [@POST \/document\/insert@]              Insert 'ApiDocument's.

    [@POST \/document\/update@]              Update 'ApiDocument's.

    [@POST \/document\/delete@]              Delete documents by URI.

    [@GET  \/binary\/save\/:filename@]       Store the index.

    [@GET  \/binary\/load\/:filename@]       Load an index.

    [@GET  \/status\/gc@]                    Garbage collection statistics.

    [@GET  \/status\/doctable@]              JSON dump of the document table (/experimental/).

    [@GET  \/status\/index@]                 JSON dump of the index (/experimental/).
-}
-- ----------------------------------------------------------------------------

module Hunt.Server
  ( -- * Starting the Server
    start
    -- * Configuration
  , HuntServerConfiguration (..)
  )
where

import           Control.Monad.Error
import           Data.String                          (fromString)

import           Data.Text                            (Text)

import qualified Network.Wai.Handler.Warp             as W
import           Network.Wai.Middleware.RequestLogger

import           Hunt.Interpreter
import           Hunt.Interpreter.Command             (StatusCmd (..))

import           Hunt.ClientInterface

import           Hunt.Server.Common
import           Hunt.Server.Schrotty                 hiding (Options)
import qualified Hunt.Server.Schrotty                 as Schrotty
import qualified Hunt.Server.Template                 as Tmpl

import           System.IO                            (stdout)

import           System.Log.Formatter
import           System.Log.Handler
import           System.Log.Handler.Simple
import           System.Log.Logger                    hiding (debugM, errorM,
                                                       warningM)
import qualified System.Log.Logger                    as Log

#ifdef SUPPORT_STATSD
import           System.Remote.Monitoring.Statsd      (defaultStatsdOptions, forkStatsd, StatsdOptions(..))
import           System.Metrics                       (registerGcMetrics, newStore)
#endif
-- ------------------------------------------------------------
-- Logging

-- | Name of the module for logging purposes.
modName :: String
modName = "Hunt.Server"

-- | Log a message at 'DEBUG' priority.
debugM :: String -> IO ()
debugM = Log.debugM modName
{-
-- | Log a message at 'WARNING' priority.
warningM :: String -> IO ()
warningM = Log.warningM modName

-- | Log a message at 'ERROR' priority.
errorM :: String -> IO ()
errorM = Log.errorM modName
-}

-- | Convenience function to add a log formatter.
withFormatter :: (Monad m, LogHandler r) => m r -> LogFormatter r -> m r
withFormatter h f = liftM (flip setFormatter f) h

-- | Initializes the loggers (stdout, file).
--   Sets the stdout logger to the given priority
--   and sets the path and 'DEBUG' priority for the file logger.
initLoggers :: HuntServerConfiguration -> IO ()
initLoggers config = do
  -- formatter
  let defFormatter = simpleLogFormatter "[$time : $loggername : $prio] $msg"

  -- root does not have a priority
  updateGlobalLogger rootLoggerName clearLevel

  -- stdout root logger
  handlerBare <- streamHandler stdout (logPriority config) `withFormatter` defFormatter
  updateGlobalLogger rootLoggerName (setHandlers [handlerBare])

  -- file logger always at 'DEBUG' level
  handlerFile <- fileHandler (logFile config) DEBUG `withFormatter` defFormatter
  updateGlobalLogger rootLoggerName (addHandler handlerFile)

-- ------------------------------------------------------------

-- | Start the server.
start :: HuntServerConfiguration -> IO ()
start config = do
  -- initialize loggers
  initLoggers config

  debugM "Application start"

#ifdef SUPPORT_STATSD
  when (not $ null $ statsDHost config) $ do
    statsDStore <- newStore
    registerGcMetrics statsDStore
    let statsDOpts = defaultStatsdOptions {host = (T.pack $ statsDHost config), port = (statsDPort config), prefix = "Hunt-Server"}
    _ <- forkStatsd statsDOpts statsDStore
    debugM $ "Connected to statsD: " ++ (statsDHost config) ++ ":" ++ (show $ statsDPort config)
#endif  

  -- init interpreter
  env <- initHunt :: IO DefHuntEnv

  case readIndexOnStartup config of
    Just filename -> do
      res <- liftIO $ runCmd env $ cmdLoadIndex filename
      case res of
        Right _  -> debugM $ "Index loaded: " ++ filename
        Left err -> fail $ show err
    Nothing -> return ()

  let options = Schrotty.Options
        { verbose  = 1
        , settings = W.setHost (fromString $ huntServerHost config)
                   $ W.setPort (huntServerPort config)
                   $ W.defaultSettings
        }
  -- start schrotty
  schrottyOpts options $ do

    -- request / response logging
    middleware logStdoutDev

    -- ------------------------------------------------------------
    -- XXX: maybe move to schrotty?

    let interpret = runCmd env

    let eval cmd = do
        res <- liftIO $ interpret cmd
        case res of
          Left res' -> do
            raise $ InterpreterError res'
          Right res' ->
            case res' of
              ResOK               -> json $ JsonSuccess ("ok" :: Text)
              ResSearch docs      -> json $ JsonSuccess docs
              ResCompletion wrds  -> json $ JsonSuccess wrds
              ResSuggestion wrds  -> json $ JsonSuccess wrds
              ResGeneric val      -> json $ JsonSuccess val

    let evalQuery mkCmd q = case parseQuery q of
          Right qry -> eval $ mkCmd qry
          Left  err -> raise $ Json 700 err

    let batch cmd = cmdSequence . map cmd

    -- ------------------------------------------------------------

    get "/"           $ redirect "/search"
    get "/search"     $ html . Tmpl.index $ (0::Int)
    get "/quickstart" $ html Tmpl.quickstart

    -- ------------------------------------------------------------

    -- interpreter
    post "/eval" $ do
      cmd <- jsonData
      eval cmd

    -- ------------------------------------------------------------

    -- simple query with unlimited # of hits
    get "/search/:query/" $ do
      query    <- param "query"
      evalQuery cmdSearch query

    -- simple query with unlimited # of hits
    get "/select/:query/" $ do
      query    <- param "query"
      evalQuery cmdSelect query

    -- paged query
    get "/search/:query/:offset/:mx" $ do
      query    <- param "query"
      offset   <- param "offset"
      mx       <- param "mx"
      evalQuery ( setResultOffset offset
                  . setMaxResults mx
                  . cmdSearch
                ) query

    -- simple query for reading the weight of documents
    get "/weight/:query/" $ do
      query    <- param "query"
      evalQuery ( setWeightIncluded
                  . setSelectedFields []
                  . cmdSearch
                ) query

    -- completion
    get "/completion/:query/:mx" $ do
      query <- param "query"
      mx    <- param "mx"
      evalQuery ( setMaxResults mx
                  . cmdCompletion
                ) query

    -- simple query with unlimited # of hits
    get "/select/:query/" $ do
      query    <- param "query"
      evalQuery (\q -> cmdSelect q) query

    -- insert a document (fails if a document (the uri) already exists)
    post "/document/insert" $ do
      jss <- jsonData
      eval $ batch cmdInsertDoc jss

    -- update a document (fails if a document (the uri) does not exist)
    post "/document/update" $ do
      jss <- jsonData
      eval $ batch cmdUpdateDoc jss

    -- delete a set of documents by URI
    post "/document/delete" $ do
      jss <- jsonData
      eval $ batch cmdDeleteDoc jss

    -- write the indexer to disk
    get "/binary/save/:filename" $ do
      filename  <- param "filename"
      eval $ cmdStoreIndex filename

    -- load indexer from disk
    get "/binary/load/:filename" $ do
      filename  <- param "filename"
      eval $ cmdLoadIndex filename

    -- status commands
    get "/status/gc" $ do
      eval $ cmdStatus StatusGC            -- garbage collector status

    get "/status/doctable" $ do
      eval $ cmdStatus StatusDocTable      -- status of document table

    get "/status/index" $ do
      eval $ cmdStatus StatusIndex         -- status of search index

    get "/status/context/:cx" $ do
      query <- param "cx"
      eval $ cmdStatus (StatusContext query)

    notFound $ raise NotFound

-- ------------------------------------------------------------