nri-postgresql-0.1.0.0: src/Postgres/Settings.hs
module Postgres.Settings
( Settings
( Settings,
pgConnection,
pgPool,
pgQueryTimeout
),
ConnectionSettings
( ConnectionSettings,
pgDatabase,
pgUser,
pgHost,
pgPassword,
pgPort
),
PoolSettings
( PoolSettings,
pgPoolStripes,
pgPoolMaxIdleTime,
pgPoolSize
),
decoder,
decoderWithPrefix,
PgDatabase (PgDatabase, unPgDatabase),
PgUser (PgUser, unPgUser),
PgHost (PgHost, unPgHost),
PgPassword (PgPassword, unPgPassword),
PgPort (PgPort, unPgPort),
PgPoolStripes (PgPoolStripes, unPgPoolStripes),
PgPoolMaxIdleTime (PgPoolMaxIdleTime, unPgPoolMaxIdleTime),
PgPoolSize (PgPoolSize, unPgPoolSize),
defaultSettings,
toPGDatabase,
)
where
import qualified Data.ByteString.Char8
import qualified Data.Text.Encoding
import qualified Data.Time
import Database.PostgreSQL.Typed
( PGDatabase,
defaultPGDatabase,
pgDBAddr,
pgDBName,
pgDBParams,
pgDBPass,
pgDBUser,
)
import qualified Environment
import qualified Log
import Network.Socket (SockAddr (SockAddrUnix))
import qualified Postgres.Time as Time
import System.FilePath ((</>))
import Prelude (Either (Left, Right), pure, realToFrac, round, show)
-- | Postgres connection details. You can use 'decoder' to create one of these.
data Settings = Settings
{ pgConnection :: ConnectionSettings,
pgPool :: PoolSettings,
pgQueryTimeout :: Time.Interval
}
deriving (Eq, Show)
defaultSettings :: Settings
defaultSettings =
Settings
{ pgConnection =
ConnectionSettings
{ pgDatabase = PgDatabase "postgres",
pgUser = PgUser "postgres",
pgHost = PgHost "localhost",
pgPassword = PgPassword (Log.mkSecret ""),
pgPort = PgPort 5432
},
pgPool =
PoolSettings
{ pgPoolSize =
-- Connections in the pool are allocated on demand, so we won't
-- create all these connections unless the application can make use
-- of them.
PgPoolSize 500,
pgPoolMaxIdleTime = PgPoolMaxIdleTime (toNominalDiffTime 3600),
pgPoolStripes = PgPoolStripes 1
},
pgQueryTimeout = Time.fromSeconds 5
}
data ConnectionSettings = ConnectionSettings
{ pgDatabase :: PgDatabase,
pgUser :: PgUser,
pgHost :: PgHost,
pgPassword :: PgPassword,
pgPort :: PgPort
}
deriving (Eq, Show)
data PoolSettings = PoolSettings
{ pgPoolSize :: PgPoolSize,
pgPoolMaxIdleTime :: PgPoolMaxIdleTime,
pgPoolStripes :: PgPoolStripes
}
deriving (Eq, Show)
-- | Create a 'Settings' value by reading settings from environment values.
--
-- [@environment variable@] PGHOST
-- [@default value@] localhost
--
-- [@environment variable@] PGPORT
-- [@default value@] 5432
--
-- [@environment variable@] PGDATABASE
-- [@default value@] postgresql
--
-- [@environment variable@] PGUSER
-- [@default value@] postgresql
--
-- [@environment variable@] PGPASSWORD
-- [@default value@]
--
-- [@environment variable@] PG_POOL_SIZE
-- [@default value@] 500
--
-- [@environment variable@] PG_POOL_STRIPES
-- [@default value@] 1
--
-- [@environment variable@] PG_POOL_MAX_IDLE_TIME
-- [@default value@] 3600
--
-- [@environment variable@] PG_QUERY_TIMEOUT_SECONDS
-- [@default value@] 5
decoder :: Environment.Decoder Settings
decoder = decoderWithPrefix ""
decoderWithPrefix :: Text -> Environment.Decoder Settings
decoderWithPrefix prefix =
pure Settings
|> andMap (connectionDecoder prefix)
|> andMap poolDecoder
|> andMap queryTimeoutDecoder
connectionDecoder :: Text -> Environment.Decoder ConnectionSettings
connectionDecoder prefix =
pure ConnectionSettings
|> andMap (pgDatabaseDecoder prefix)
|> andMap (pgUserDecoder prefix)
|> andMap (pgHostDecoder prefix)
|> andMap (pgPasswordDecoder prefix)
|> andMap (pgPortDecoder prefix)
poolDecoder :: Environment.Decoder PoolSettings
poolDecoder =
pure PoolSettings
|> andMap pgPoolSizeDecoder
|> andMap pgPoolMaxIdleTimeDecoder
|> andMap pgPoolStripesDecoder
newtype PgPort = PgPort {unPgPort :: Int}
deriving (Eq, Show)
pgPortDecoder :: Text -> Environment.Decoder PgPort
pgPortDecoder prefix =
Environment.variable
Environment.Variable
{ Environment.name = prefix ++ "PGPORT",
Environment.description = "The port postgres is running on.",
Environment.defaultValue =
defaultSettings |> pgConnection |> pgPort |> unPgPort |> show |> Text.fromList
}
(Environment.int |> map PgPort)
newtype PgPassword = PgPassword {unPgPassword :: Log.Secret Text}
deriving (Eq, Show)
pgPasswordDecoder :: Text -> Environment.Decoder PgPassword
pgPasswordDecoder prefix =
Environment.variable
Environment.Variable
{ Environment.name = prefix ++ "PGPASSWORD",
Environment.description = "The postgres user password.",
Environment.defaultValue =
defaultSettings |> pgConnection |> pgPassword |> unPgPassword |> Log.unSecret
}
(Environment.secret Environment.text |> map PgPassword)
newtype PgHost = PgHost {unPgHost :: Text}
deriving (Eq, Show)
pgHostDecoder :: Text -> Environment.Decoder PgHost
pgHostDecoder prefix =
Environment.variable
Environment.Variable
{ Environment.name = prefix ++ "PGHOST",
Environment.description = "The hostname of the postgres server to connect to.",
Environment.defaultValue =
defaultSettings |> pgConnection |> pgHost |> unPgHost
}
(Environment.text |> map PgHost)
newtype PgUser = PgUser {unPgUser :: Text}
deriving (Eq, Show)
pgUserDecoder :: Text -> Environment.Decoder PgUser
pgUserDecoder prefix =
Environment.variable
Environment.Variable
{ Environment.name = prefix ++ "PGUSER",
Environment.description = "The postgres user to connect with.",
Environment.defaultValue =
defaultSettings |> pgConnection |> pgUser |> unPgUser
}
(Environment.text |> map PgUser)
newtype PgDatabase = PgDatabase {unPgDatabase :: Text}
deriving (Eq, Show)
pgDatabaseDecoder :: Text -> Environment.Decoder PgDatabase
pgDatabaseDecoder prefix =
Environment.variable
Environment.Variable
{ Environment.name = prefix ++ "PGDATABASE",
Environment.description = "The postgres database to connect to.",
Environment.defaultValue =
defaultSettings |> pgConnection |> pgDatabase |> unPgDatabase
}
(map PgDatabase Environment.text)
newtype PgPoolStripes = PgPoolStripes {unPgPoolStripes :: Int}
deriving (Eq, Show)
pgPoolStripesDecoder :: Environment.Decoder PgPoolStripes
pgPoolStripesDecoder =
Environment.variable
Environment.Variable
{ Environment.name = "PG_POOL_STRIPES",
Environment.description = "The amount of sub-connection pools to create. Best refer to the resource-pool package for more info on this one. 1 is a good value for most applications.",
Environment.defaultValue =
defaultSettings |> pgPool |> pgPoolStripes |> unPgPoolStripes |> show |> Text.fromList
}
(Environment.int |> map PgPoolStripes)
newtype PgPoolMaxIdleTime = PgPoolMaxIdleTime {unPgPoolMaxIdleTime :: Data.Time.NominalDiffTime}
deriving (Eq, Show)
pgPoolMaxIdleTimeDecoder :: Environment.Decoder PgPoolMaxIdleTime
pgPoolMaxIdleTimeDecoder =
Environment.variable
Environment.Variable
{ Environment.name = "PG_POOL_MAX_IDLE_TIME",
Environment.description = "The maximum time a database connection will be able remain idle until it is closed.",
Environment.defaultValue =
defaultSettings |> pgPool |> pgPoolMaxIdleTime |> unPgPoolMaxIdleTime |> fromNominalDiffTime |> show |> Text.fromList
}
(Environment.int |> map (PgPoolMaxIdleTime << toNominalDiffTime))
toNominalDiffTime :: Int -> Data.Time.NominalDiffTime
toNominalDiffTime = realToFrac
fromNominalDiffTime :: Data.Time.NominalDiffTime -> Int
fromNominalDiffTime = Prelude.round
newtype PgPoolSize = PgPoolSize {unPgPoolSize :: Int}
deriving (Eq, Show)
pgPoolSizeDecoder :: Environment.Decoder PgPoolSize
pgPoolSizeDecoder =
Environment.variable
Environment.Variable
{ Environment.name = "PG_POOL_SIZE",
Environment.description = "The size of the postgres connection pool. This is the maximum amount of parallel database connections the app will be able to use.",
Environment.defaultValue =
defaultSettings |> pgPool |> pgPoolSize |> unPgPoolSize |> show |> Text.fromList
}
(Environment.int |> map PgPoolSize)
toPGDatabase :: Settings -> PGDatabase
toPGDatabase
Settings
{ pgConnection =
ConnectionSettings
{ pgDatabase,
pgUser,
pgHost,
pgPassword,
pgPort
},
pgQueryTimeout
} =
defaultPGDatabase
{ pgDBName = Data.Text.Encoding.encodeUtf8 (unPgDatabase pgDatabase),
pgDBUser = Data.Text.Encoding.encodeUtf8 (unPgUser pgUser),
pgDBPass = Data.Text.Encoding.encodeUtf8 <| Log.unSecret (unPgPassword pgPassword),
pgDBParams =
if Time.milliseconds pgQueryTimeout > 0
then
[ -- We configure Postgres to automatically kill queries when they run
-- too long. That should offer some protection against queries
-- locking up the database.
-- https://www.postgresql.org/docs/9.4/runtime-config-client.html
("statement_timeout", pgQueryTimeout |> Time.milliseconds |> floor |> show |> Data.ByteString.Char8.pack)
]
else [],
pgDBAddr =
-- The rule that PostgreSQL/libpq applies to `host`:
--
-- If this begins with a slash, it specifies Unix-domain
-- communication rather than TCP/IP communication; the value is the
-- name of the directory in which the socket file is stored
--
-- https://www.postgresql.org/docs/9.6/libpq-connect.html#LIBPQ-CONNECT-HOST
if Text.startsWith "/" host
then
Text.toList host </> ".s.PGSQL." ++ show port
|> SockAddrUnix
|> Right
else Left (Text.toList host, show port)
}
where
host = unPgHost pgHost
port = unPgPort pgPort
queryTimeoutDecoder :: Environment.Decoder Time.Interval
queryTimeoutDecoder =
Environment.variable
Environment.Variable
{ Environment.name = "PG_QUERY_TIMEOUT_SECONDS",
Environment.description = "The maximum time a query can run before it is cancelled.",
Environment.defaultValue = defaultSettings |> pgQueryTimeout |> Time.seconds |> show |> Text.fromList
}
(Environment.float |> map Time.fromSeconds)