diff --git a/IHP/Log.hs b/IHP/Log.hs
new file mode 100644
--- /dev/null
+++ b/IHP/Log.hs
@@ -0,0 +1,120 @@
+{-|
+Module: IHP.Log
+Description:  Functions to write logs at all log levels.
+
+Import this module qualified! All code examples
+assume you have imported the module as follows:
+
+> import qualified IHP.Log as Log
+
+-}
+module IHP.Log
+( module IHP.Log.Types
+, debug
+, info
+, warn
+, error
+, fatal
+, unknown
+, makeRequestLogger
+, defaultRequestLogger
+) where
+
+import Prelude hiding (error, log)
+import Control.Monad (when)
+import IHP.Log.Types
+import Network.Wai (Middleware)
+import Network.Wai.Middleware.RequestLogger (mkRequestLogger, RequestLoggerSettings, destination)
+import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
+import Data.Default (Default (def))
+import qualified System.Log.FastLogger as FastLogger
+
+-- | Format a log and send it to the logger.
+-- Internal use only -- application code should call the
+-- function corresponding to the desired log level.
+log :: (?context :: context, LoggingProvider context, FastLogger.ToLogStr string) => LogLevel -> string -> IO ()
+log level text = do
+    writeLog level ?context.logger text
+
+-- | Log a debug level message.
+--
+-- > action CreateUserAction { .. } = do
+-- >     Log.debug "entered CreateUserAction"
+-- >     ...
+debug :: (?context :: context, LoggingProvider context, FastLogger.ToLogStr string) => string -> IO ()
+debug = log Debug
+
+-- | Log an info level message.
+--
+-- > action UsersAction = do
+-- >     users <- query @User |> fetch
+-- >     Log.info $ show (lengh users) <> " users fetched."
+-- >     ...
+info :: (?context :: context, LoggingProvider context, FastLogger.ToLogStr string) => string -> IO ()
+info = log Info
+
+-- | Log a warning level message.
+--
+-- > action UsersAction = do
+-- >     users <- query @User |> fetch
+-- >     whenEmpty users $ Log.warn "No users found. Something might be wrong!"
+-- >     ...
+warn :: (?context :: context, LoggingProvider context, FastLogger.ToLogStr string) => string -> IO ()
+warn = log Warn
+
+-- |Log a warning level message.
+--
+-- @
+--    action CreatePostAction = do
+--        let post = newRecord @Post
+--        post
+--            |> buildPost
+--            |> ifValid \case
+--                Left post -> do
+--                    Log.error "Invalid post."
+--                    render NewView { .. }
+--                Right post -> do
+--                    post <- post |> createRecord
+--                    setSuccessMessage "Post created"
+--                    redirectTo PostsAction
+-- @
+error :: (?context :: context, LoggingProvider context, FastLogger.ToLogStr string) => string -> IO ()
+error = log Error
+
+-- | Log a fatal level message.
+-- Note this does not exit the program for you -- it only logs to the "Fatal" log level.
+--
+-- > Log.fatal "Unrecoverable application error!"
+fatal :: (?context :: context, LoggingProvider context, FastLogger.ToLogStr string) => string -> IO ()
+fatal = log Fatal
+
+-- | Log an "unknown" level message.
+-- This is the highest log level and will always be output by the logger.
+--
+-- > Log.unknown "This will be sent to the logger no matter what!"
+unknown :: (?context :: context, LoggingProvider context, FastLogger.ToLogStr string) => string -> IO ()
+unknown = log Unknown
+
+-- | Write a log if the given log level is greater than or equal to the logger's log level.
+writeLog :: (FastLogger.ToLogStr string) => LogLevel -> Logger -> string -> IO ()
+writeLog level logger text = do
+    let write = logger.write
+    let formatter = logger.formatter
+    when (level >= logger.level) $
+        write (\time -> formatter time level (toLogStr text))
+
+-- | Wraps 'RequestLogger' from wai-extra to log to an IHP logger.
+-- See 'Network.Wai.Middleware.RequestLogger'.
+makeRequestLogger :: RequestLoggerSettings -> Logger -> IO Middleware
+makeRequestLogger settings logger =
+    mkRequestLogger settings {
+        destination = RequestLogger.Callback (\logStr ->
+            let ?context = logger in
+                info (fromLogStr logStr)
+            )
+        }
+
+-- | Create a request logger with default settings wrapped in an IHP logger.
+-- See 'Network.Wai.Middleware.RequestLogger'.
+defaultRequestLogger :: Logger -> IO Middleware
+defaultRequestLogger = makeRequestLogger def
diff --git a/IHP/Log/Types.hs b/IHP/Log/Types.hs
new file mode 100644
--- /dev/null
+++ b/IHP/Log/Types.hs
@@ -0,0 +1,261 @@
+{-# LANGUAGE ConstraintKinds #-}
+{-|
+Module: IHP.Log.Types
+Description:  Types for the IHP logging system
+-}
+
+module IHP.Log.Types
+( Bytes(..)
+, LogStr
+, BufSize
+, TimeFormat
+, RotateSettings(..)
+, toLogStr
+, fromLogStr
+, defaultBufSize
+, simpleTimeFormat
+, simpleTimeFormat'
+, Logger(..)
+, LogLevel(..)
+, LogDestination(..)
+, LoggingProvider(..)
+, LoggerSettings(..)
+, LogFormatter
+, FormattedTime
+, newLogger
+, defaultLogger
+, defaultDestination
+, defaultFormatter
+, withLevelFormatter
+, withTimeFormatter
+, withTimeAndLevelFormatter
+, OsPath
+) where
+
+import Prelude
+import Data.ByteString (ByteString)
+import Data.Text as Text
+import Data.Default (Default (def))
+import System.Log.FastLogger (
+    LogStr,
+    LogType'(..),
+    BufSize,
+    FileLogSpec(..),
+    TimedFileLogSpec(..),
+    TimeFormat,
+    toLogStr,
+    fromLogStr,
+    defaultBufSize,
+    newTimeCache,
+    simpleTimeFormat,
+    simpleTimeFormat',
+    newTimedFastLogger,
+    ToLogStr (..)
+    )
+
+import qualified System.Log.FastLogger as FastLogger (FormattedTime)
+import GHC.Records
+import System.OsPath (OsPath, encodeUtf, decodeUtf)
+
+
+-- some functions brought over from IHP.Prelude
+-- can't import due to circular dependency with IHP.ModelSupport which relies on this module
+
+tshow :: Show a => a -> Text
+tshow value = Text.pack (Prelude.show value)
+
+show :: Show a => a -> Text
+show = tshow
+
+-- | Interal logger type that encapsulates information needed to perform
+-- logging operations. Users can also access this though the 'LoggingProvider'
+-- class in controller and model actions to perform logic based on the set log level.
+data Logger = Logger {
+    write     :: !((FastLogger.FormattedTime -> LogStr) -> IO ()),
+    level     :: !LogLevel,
+    formatter :: !LogFormatter,
+    timeCache :: !(IO FastLogger.FormattedTime),
+    cleanup   :: !(IO ())
+}
+
+data LogLevel
+    -- | For general messages to help with debugging during development.
+    -- Default log level in development.
+    -- Also the log level used for SQL queries.
+    -- See 'IHP.Log.debug' for example usage.
+    = Debug
+    -- | For info messages that help montior application usage.
+    -- Default log level for production.
+    -- See 'IHP.Log.info' for example usage.
+    | Info
+    -- | For warning messages when something might be wrong.
+    -- See 'IHP.Log.warn' for example usage.
+    | Warn
+    -- | For application errors that can be recovered from.
+    -- See 'IHP.Log.error' for example usage.
+    | Error
+    -- | For application errors that are fatal
+    -- See 'IHP.Log.fatal' for example usage.
+    | Fatal
+    -- | For miscallenaous log messages. Highest log level - will always be logged
+    -- See 'IHP.Log.unknown' for example usage.
+    | Unknown
+    deriving (Enum, Eq, Ord, Show)
+
+instance ToLogStr LogLevel where
+    toLogStr Debug = "DEBUG"
+    toLogStr Info = "INFO"
+    toLogStr Warn = "WARN"
+    toLogStr Error = "ERROR"
+    toLogStr Fatal = "FATAL"
+    toLogStr Unknown = "UNKNOWN"
+
+-- | The timestamp in the formatted defined by the logger's timeFormat string.
+type FormattedTime = ByteString
+
+-- | Called every time a message is sent to the logger.
+-- Since this is just a function type, it's trivial to define custom formatters:
+--
+-- @
+--     withTimeAndLevelFormatterUpcaseAndHappy :: LogFormatter
+--     withTimeAndLevelFormatterUpcaseAndHappy time level msg =
+--        "[" <> toUpper (show level) <> "]"
+--          <> "[" <> time <> "] "
+--          <> toUpper msg <> " :) \n"
+-- @
+type LogFormatter = FormattedTime -> LogLevel -> LogStr -> LogStr
+
+-- | A number of bytes, used in 'RotateSettings'
+newtype Bytes = Bytes Integer
+
+data RotateSettings
+    -- | Log messages to a file which is never rotated.
+    --
+    -- @
+    -- newLogger def {
+    --    destination = File "Log/production.log" NoRotate defaultBufSize
+    --    }
+    -- @
+    = NoRotate
+    -- | Log messages to a file and rotate the file after it reaches the given size in bytes.
+    -- Third argument is the max number of rotated log files to keep around before overwriting the oldest one.
+    --
+    -- Example: log to a file rotated once it is 4MB, and keep 7 files before overwriting the first file.
+    --
+    -- @
+    --    newLogger def {
+    --      destination = File "Log/production.log" (SizeRotate (Bytes (4 * 1024 * 1024)) 7) defaultBufSize
+    --      }
+    -- @
+    | SizeRotate !Bytes !Int
+    -- | Log messages to a file rotated on a timed basis.
+    -- Expects a time format string as well as a function which compares two formatted time strings
+    -- which is used to determine if the file should be rotated.
+    -- Last argument is a function which is called on a log file once its rotated.
+    --
+    -- Example: rotate a file daily and compress the log file once rotated.
+    --
+    -- @
+    --   let
+    --       filePath = "Log/production.log"
+    --       formatString = "%FT%H%M%S"
+    --       timeCompare = (==) on C8.takeWhile (/=T))
+    --       compressFile fp = void . forkIO $
+    --           callProcess "tar" [ "--remove-files", "-caf", fp <> ".gz", fp ]
+    --   in
+    --     newLogger def {
+    --        destination = File
+    --          filePath
+    --          (TimedRotate formatString timeCompare compressFile)
+    --          defaultBufSize
+    --        }
+    -- @
+    | TimedRotate !TimeFormat (FastLogger.FormattedTime -> FastLogger.FormattedTime -> Bool) (OsPath -> IO ())
+
+-- | Where logged messages will be delivered to.
+data LogDestination
+    = None
+    -- | Log messages to standard output.
+    | Stdout !BufSize
+    -- | Log messages to standard error.
+    | Stderr !BufSize
+    -- | Log message to a file. Rotate the log file with the behavior given by 'RotateSettings'.
+    | File !OsPath !RotateSettings !BufSize
+    -- | Send logged messages to a callback. Flush action called after every log.
+    | Callback !(LogStr -> IO ()) !(IO ())
+
+data LoggerSettings = LoggerSettings {
+    level       :: LogLevel,
+    formatter   :: LogFormatter,
+    destination :: LogDestination,
+    timeFormat  :: TimeFormat
+}
+
+instance Default LoggerSettings where
+    def = LoggerSettings {
+        level = Debug,
+        formatter = defaultFormatter,
+        destination = defaultDestination,
+        timeFormat = simpleTimeFormat'
+    }
+
+-- | Logger default destination is to standard out.
+defaultDestination :: LogDestination
+defaultDestination = Stdout defaultBufSize
+
+-- | Used to get the logger for a given environment.
+-- | Call in any instance of 'LoggingProvider' get the the environment's current logger.
+-- Useful in controller and model actions, which both have logging contexts.
+type LoggingProvider context = HasField "logger" context Logger
+
+instance HasField "logger" Logger Logger where
+    getField logger = logger
+
+-- | Create a new 'FastLogger' and wrap it in an IHP 'Logger'.
+-- Use with the default logger settings and record update syntax for nice configuration:
+--
+-- > newLogger def { level = Error }
+newLogger :: LoggerSettings -> IO Logger
+newLogger LoggerSettings { .. } = do
+    timeCache <- newTimeCache timeFormat
+    (write, cleanup) <- makeFastLogger timeCache destination
+    pure Logger { .. }
+    where
+        makeFastLogger timeCache = \case
+                None                    -> newTimedFastLogger timeCache LogNone
+                Stdout buf              -> newTimedFastLogger timeCache (LogStdout buf)
+                Stderr buf              -> newTimedFastLogger timeCache (LogStderr buf)
+                File path settings buf  -> do
+                    logType <- makeFileLogger path settings buf
+                    newTimedFastLogger timeCache logType
+                Callback callback flush -> newTimedFastLogger timeCache (LogCallback callback flush)
+
+        makeFileLogger path NoRotate buf = do
+            fp <- decodeUtf path
+            pure (LogFileNoRotate fp buf)
+        makeFileLogger path (SizeRotate (Bytes size) count) buf = do
+            fp <- decodeUtf path
+            pure (LogFile (FileLogSpec fp size count) buf)
+        makeFileLogger path (TimedRotate fmt cmp post) buf = do
+            fp <- decodeUtf path
+            pure (LogFileTimedRotate (TimedFileLogSpec fp fmt cmp (\fpStr -> post =<< encodeUtf fpStr)) buf)
+
+-- | Formats logs as-is to stdout.
+defaultLogger :: IO Logger
+defaultLogger = newLogger def
+
+-- | Formats the log as-is with a newline added.
+defaultFormatter :: LogFormatter
+defaultFormatter _ _ msg = msg <> "\n"
+
+-- | Prepends the timestamp to the log message and adds a new line.
+withTimeFormatter :: LogFormatter
+withTimeFormatter  time _ msg = "[" <> toLogStr time <> "] " <> msg <> "\n"
+
+-- | Prepends the log level to the log message and adds a new line.
+withLevelFormatter :: LogFormatter
+withLevelFormatter time level msg = "[" <> (toLogStr level) <> "] " <> msg <> "\n"
+
+-- | Prepends the log level and timestamp to the log message and adds a new line.
+withTimeAndLevelFormatter :: LogFormatter
+withTimeAndLevelFormatter time level msg = "[" <> (toLogStr level) <> "] [" <> toLogStr time <> "] " <> msg <> "\n"
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2020 digitally induced GmbH
+
+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.
diff --git a/ihp-log.cabal b/ihp-log.cabal
new file mode 100644
--- /dev/null
+++ b/ihp-log.cabal
@@ -0,0 +1,42 @@
+cabal-version:       2.2
+name:                ihp-log
+version:             1.0.0
+synopsis:            Lightweight logging for IHP applications
+description:         Provides structured logging with configurable levels, formatters, and destinations.
+                     Can be used standalone or with the full IHP framework.
+license:             MIT
+license-file:        LICENSE
+author:              digitally induced GmbH
+maintainer:          hello@digitallyinduced.com
+homepage:            https://ihp.digitallyinduced.com/
+bug-reports:         https://github.com/digitallyinduced/ihp/issues
+copyright:           (c) digitally induced GmbH
+category:            Web, Logging, IHP
+
+common shared-properties
+    default-language: GHC2021
+    build-depends:
+          base >= 4.17.0 && < 4.22
+        , filepath >= 1.5
+        , text
+        , bytestring
+        , data-default
+        , fast-logger
+        , wai
+        , wai-extra
+    default-extensions:
+        OverloadedStrings
+        , ImplicitParams
+        , DataKinds
+        , RecordWildCards
+        , OverloadedRecordDot
+        , DuplicateRecordFields
+        , LambdaCase
+    ghc-options: -Werror=incomplete-patterns -Werror=unused-imports -Werror=missing-fields
+
+library
+    import: shared-properties
+    hs-source-dirs: .
+    exposed-modules:
+        IHP.Log
+        , IHP.Log.Types
