diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,28 @@
+Copyright (c) 2015, Alex Bates
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+* Neither the name of hslogger-reader nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/hslogger-reader.cabal b/hslogger-reader.cabal
new file mode 100644
--- /dev/null
+++ b/hslogger-reader.cabal
@@ -0,0 +1,71 @@
+name:                hslogger-reader
+version:             1.0.0
+synopsis:            Parsing hslogger-produced logs.
+description:         
+
+  hslogger-reader provides a function to generate a parser for
+  <http://hackage.haskell.org/package/hslogger hslogger>-produced logs
+  from a hslogger format string (see "System.Log.Formatter"). The
+  accompanying /profiling/ executable demonstrates parsing and computing
+  with a log file in constant memory.
+
+  Also bundled is the /filter-logs/ executable, a command-line tool to
+  filter a hslogger-syle log file.
+
+license:             BSD3
+license-file:        LICENSE
+author:              Alex Bates
+maintainer:          ard.bates@gmail.com
+build-type:          Simple
+homepage:            http://github.com/prophet-on-that/hslogger-reader
+bug-reports:         http://github.com/prophet-on-that/hslogger-reader/issues
+cabal-version:       >=1.10
+category: Interfaces, Parsing
+
+library 
+  exposed-modules:
+      System.Log.Reader
+  hs-source-dirs:      
+    src
+  default-language:    
+    Haskell2010
+  build-depends:       
+      base >= 4.7 && < 5
+    , attoparsec == 0.12.*
+    , text == 1.2.*
+    , hslogger == 1.2.*
+    , time == 1.4.*
+    , old-locale 
+  ghc-options: -W
+
+executable profiling
+  hs-source-dirs: src-exe/profiling
+  main-is: 
+      Main.hs
+  default-language: Haskell2010
+  build-depends:
+      base
+    , hslogger-reader
+    , text
+    , attoparsec
+  ghc-options: -W
+
+executable filter-logs
+  hs-source-dirs: src-exe/filter-logs
+  main-is: 
+      Main.hs
+  other-modules:
+      Arguments
+  default-language: Haskell2010
+  build-depends:
+      base
+    , hslogger-reader
+    , text
+    , attoparsec
+    , hslogger
+    , time
+    , text-icu == 0.7.*
+    , optparse-applicative == 0.11.*
+    , old-locale
+  ghc-options: -W
+
diff --git a/src-exe/filter-logs/Arguments.hs b/src-exe/filter-logs/Arguments.hs
new file mode 100644
--- /dev/null
+++ b/src-exe/filter-logs/Arguments.hs
@@ -0,0 +1,107 @@
+module Arguments
+  ( Arguments (..)
+  , opts
+  ) where
+
+import Options.Applicative
+import Data.Time
+import System.Log.Logger
+import qualified Data.Text as T
+import System.Locale (defaultTimeLocale)
+import Data.Maybe
+
+data Arguments = Arguments
+  { lowerTime :: Maybe UTCTime
+  , upperTime :: Maybe UTCTime
+  , lowerPrio :: Priority
+  , upperPrio :: Priority
+  , insensitive :: Bool
+  , pattern :: Maybe T.Text
+  , format :: T.Text
+  , pid :: Maybe Int
+  , tid :: Maybe Int
+  , logFile :: FilePath
+  }
+
+parseArgs :: Parser Arguments
+parseArgs
+  = Arguments
+      <$> ( optional . fmap parseUTCTime . strOption $
+              ( short 'l'
+             <> long "lower-time"
+             <> metavar "UTCTIME"
+             <> help "Ignore messages logged before this time."
+              )
+          )
+      <*> ( optional . fmap parseUTCTime . strOption $
+              ( short 'u'
+             <> long "upper-time"
+             <> metavar "UTCTIME"
+             <> help "Ignore messages logged after this time."
+              )
+           )
+      <*> ( option auto $
+              ( short 'm'
+             <> long "min-priority"
+             <> value DEBUG
+             <> showDefault
+             <> metavar "PRIO"
+             <> help "Assert a minimum message priority."
+              )
+          )
+      <*> ( option auto $
+              ( short 'n'
+             <> long "max-priority"
+             <> value EMERGENCY
+             <> showDefault
+             <> metavar "PRIO"
+             <> help "Assert a maximum message priority."
+              )
+          )
+      <*> ( switch $
+              ( short 'i'
+             <> long "insensitive"
+             <> help "Ignore case when matching message pattern."
+              )
+          )
+      <*> ( optional . fmap T.pack . strOption $
+              ( short 'p'
+             <> long "pattern"
+             <> metavar "REGEXP"
+             <> help "Assert a pattern the logger name must satisfy."
+              )
+          )
+      <*> ( fmap T.pack . strOption $
+              ( short 'f'
+             <> long "format"
+             <> value "[$utcTime $loggername $prio] $msg"
+             <> showDefault
+             <> metavar "FORMAT"
+             <> help "Specify hslogger-style format of log files."
+              )
+          )
+      <*> ( optional . option auto $
+              ( long "process-id"
+             <> metavar "PID"
+             <> help "Assert logging process."
+              )
+          )
+      <*> ( optional . option auto $
+              ( long "thread-id"
+             <> metavar "TID"
+             <> help "Assert logging thread."
+              )
+          )
+      <*> ( strArgument $ metavar "FILE"
+          )
+
+opts
+  = info (helper <*> parseArgs)
+      ( header "Filter hslogger-produced log files."
+     <> fullDesc
+     <> footer "Limitations: logs must use hslogger's default time format `yyyy-mm-ddThh:mm:ssZ' and logger names must not include whitespace."
+      )
+
+parseUTCTime :: String -> UTCTime
+parseUTCTime
+  = fromMaybe (error "Cannot parse date (expected format `yyyy-mm-ddThh:mm:ssZ'") . parseTime defaultTimeLocale "%FT%TZ"
diff --git a/src-exe/filter-logs/Main.hs b/src-exe/filter-logs/Main.hs
new file mode 100644
--- /dev/null
+++ b/src-exe/filter-logs/Main.hs
@@ -0,0 +1,103 @@
+-- | Filter hslogger-produced logs and print to standard
+-- output. Requires logs to use default hslogger time format and for
+-- logger names to not include whitespace.
+
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+
+module Main where
+
+import Arguments
+
+import Options.Applicative (execParser)
+import Prelude hiding (takeWhile)
+import System.Log.Reader
+import System.Log.Logger
+import qualified Data.Text.Lazy as L
+import qualified Data.Text.Lazy.IO as L
+import Data.Attoparsec.Text.Lazy
+import Data.Maybe
+import Control.Applicative
+import Data.Time
+import Data.Text.ICU hiding (pattern)
+import Control.Exception
+import Control.Monad
+import Data.Typeable
+import Data.Monoid
+
+main = do
+  arguments <- execParser opts
+
+  regex'' <- case pattern arguments of
+    Nothing ->
+      return Nothing
+    Just pattern' -> do
+      let
+        regexOptions
+          = if insensitive arguments
+              then
+                [CaseInsensitive]
+              else
+                []
+      either (throwIO . RegexError) (return . return) $ regex' regexOptions pattern'
+  messageParser <- either (throwIO . ParserError) return $ logMessageParser (format arguments) loggerNameParser
+
+  lts <- fmap L.lines . L.readFile . logFile $ arguments
+  forM lts $ \lt -> do
+    lm <- either (const $ throwIO (MessageParseError lt)) return . eitherResult . parse messageParser $ lt
+    let
+      pred
+        = filterLogMessage
+            (lowerPrio arguments)
+            (upperPrio arguments)
+            (lowerTime arguments)
+            (upperTime arguments)
+            (pid arguments)
+            (tid arguments)
+            regex''
+            
+    when (pred lm) $ L.putStrLn lt
+  where
+    loggerNameParser
+      = takeTill isHorizontalSpace
+
+filterLogMessage
+  :: Priority -- ^ Lower priority
+  -> Priority -- ^ Upper priority
+  -> Maybe UTCTime -- ^ Lower bound on date
+  -> Maybe UTCTime -- ^ Upper bound on date
+  -> Maybe Int -- ^ Process ID
+  -> Maybe Int -- ^ Thread ID
+  -> Maybe Regex
+  -> LogMessage
+  -> Bool
+filterLogMessage lowerPrio upperPrio lTime uTime pid tid reg lm
+  = and . catMaybes $
+      [ (>= lowerPrio) <$> priority lm
+      , (<= upperPrio) <$> priority lm
+      , liftA2 (<=) lTime $ zonedTimeToUTC <$> timestamp lm
+      , liftA2 (>=) uTime $ zonedTimeToUTC <$> timestamp lm
+      , liftA2 (==) pid $ processId lm
+      , liftA2 (==) tid $ threadId lm
+      , do
+          reg' <- reg
+          name <- loggerName lm
+          return . fromMaybe False $ find reg' name >> return True
+      ]
+
+data Errors
+  = MessageParseError L.Text
+  | RegexError ParseError
+  | ParserError String
+  deriving (Typeable)
+
+instance Show Errors where
+  show (MessageParseError lt)
+    = L.unpack $ "Format of following message does not match expected: " <> lt
+  show (RegexError err)
+    = "Failure parsing regular expression: " <> show err
+  show (ParserError str)
+    = "Failure parsing log format spec: " <> str
+    
+instance Exception Errors
+
diff --git a/src-exe/profiling/Main.hs b/src-exe/profiling/Main.hs
new file mode 100644
--- /dev/null
+++ b/src-exe/profiling/Main.hs
@@ -0,0 +1,55 @@
+-- | Compute number of lines and maximum message length for given log
+-- file and format.
+
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE BangPatterns #-}
+
+module Main where
+
+import Prelude hiding (takeWhile)
+import System.Log.Reader
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as L
+import qualified Data.Text.Lazy.IO as L
+import Data.Attoparsec.Text.Lazy
+import Data.Foldable
+import Data.Maybe
+import System.Exit
+import System.Environment
+
+main = do
+  (fileName, format) <- handleArgs
+  lts <- fmap L.lines $ L.readFile fileName
+  let
+    result = do
+      parser <- logMessageParser format loggerNameParser
+      let
+        f (!lineCount, !maxMsgLength) lt = do
+          lm <- eitherResult . parse parser $ lt
+          let
+            newMaxLength
+              = fromMaybe maxMsgLength $ do
+                  msg <- message lm
+                  return $ max maxMsgLength (T.length msg)
+          return (lineCount + 1, newMaxLength)
+      foldlM f (0, 0) lts
+  case result of
+    Left err ->
+      putStrLn err
+    Right (lineCount, maxMsgLength) ->  
+      putStrLn $ "Lines: " ++ show lineCount ++ "\tMaximum message length: " ++ show maxMsgLength
+  where
+    loggerNameParser
+      = takeTill isHorizontalSpace
+
+    handleArgs :: IO (FilePath, T.Text)
+    handleArgs = do
+      args <- getArgs
+      case args of
+        [fileName, format] ->
+          return (fileName, T.pack format)
+        _ -> do
+          name <- getProgName
+          putStrLn $ "Usage: " ++ name ++ " format_string path/to/log/file"
+          exitFailure
+         
diff --git a/src/System/Log/Reader.hs b/src/System/Log/Reader.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Log/Reader.hs
@@ -0,0 +1,170 @@
+-- | Generate a parser for logs produced by the
+-- <http://hackage.haskell.org/package/hslogger hslogger> package,
+-- supporting arbitrary formatting strings (see
+-- "System.Log.Formatter"). Currently, this package does provide
+-- support for custom-defined formatters.
+--
+-- 'logMessageParser' will generate a parser for a 'LogMessage' given
+-- a format string (such @ "[$utcTime $loggername $prio] $msg" @) and
+-- a parser for your logger names. This can then be used to read logs
+-- line-by-line, potentially in constant memory, from disk. See the
+-- accompanying executable for an example of this.
+
+{-# LANGUAGE OverloadedStrings #-}
+
+module System.Log.Reader
+  ( LogMessage (..)
+  , FormatString
+  , logMessageParser
+  , tfLogMessageParser
+    -- * Extras
+  , zonedTimeParser
+  ) where
+
+import Prelude hiding (takeWhile)
+import Data.Attoparsec.Text.Lazy 
+import qualified Data.Text as T
+import Control.Applicative
+import Data.Foldable
+import System.Log
+import Data.Time
+import Data.Char (isAlpha)
+import Data.Monoid
+import System.Locale (defaultTimeLocale)
+
+type FormatString = T.Text
+
+data LogMessage = LogMessage
+  { message :: Maybe T.Text
+  , loggerName :: Maybe T.Text
+  , priority :: Maybe Priority
+  , threadId :: Maybe Int
+  , processId :: Maybe Int
+  , timestamp :: Maybe ZonedTime
+  } deriving (Show)
+
+data Instruction
+  = Noise T.Text
+  | Message
+  | LoggerName
+  | Priority
+  | ThreadId
+  | ProcessId
+  | Time
+  | TimeUTC
+  deriving (Show)
+
+formatStringParser :: Parser [Instruction]
+formatStringParser
+  = handleEOI <|> do
+      instr <- handleNoise <|> do
+        char '$'
+        instr <- takeWhile isAlpha
+        case instr of
+          "msg" -> return Message
+          "loggername" -> return LoggerName
+          "prio" -> return Priority
+          "tid" -> return ThreadId
+          "pid" -> return ProcessId
+          "time" -> return Time
+          "utcTime" -> return TimeUTC
+          str -> fail $ "Formatter `" ++ T.unpack str ++ "' is unsupported, use one of $msg, $loggername, $prio, $tid, $pid, $time, $utcTime"
+      (instr :) <$> formatStringParser
+  where
+    handleEOI :: Parser [Instruction]
+    handleEOI = do
+      endOfInput
+      return []
+
+    handleNoise = do
+      noise <- takeWhile1 (/= '$')
+      return $ Noise noise
+
+buildParser
+  :: Parser T.Text -- ^ LoggerName parser
+  -> Parser ZonedTime
+  -> [Instruction]
+  -> Parser LogMessage
+buildParser loggerNameParser zonedTimeParser
+  = foldlM helper $ LogMessage Nothing Nothing Nothing Nothing Nothing Nothing 
+  where
+    helper :: LogMessage -> Instruction -> Parser LogMessage
+    helper lm (Noise noise) = do
+      _ <- string noise
+      return lm
+    helper lm Message = do
+      msg <- takeTill isEndOfLine
+      return $ lm { message = Just msg }
+    helper lm LoggerName = do
+      name <- loggerNameParser
+      return $ lm { loggerName = Just name }
+    helper lm Priority = do
+      prio <- parsePriority
+      return $ lm { priority = Just prio }
+      where
+        parsePriority :: Parser Priority
+        parsePriority
+          = ("DEBUG" >> return DEBUG) <|>
+            ("INFO" >> return INFO) <|>
+            ("NOTICE" >> return NOTICE) <|> 
+            ("WARNING" >> return WARNING) <|> 
+            ("ERROR" >> return ERROR) <|> 
+            ("CRITICAL" >> return CRITICAL) <|> 
+            ("ALERT" >> return ALERT) <|> 
+            ("EMERGENCY" >> return EMERGENCY) <|>
+            (fail "Priority not recognised")
+    helper lm ThreadId = do
+      tid <- decimal
+      return $ lm { threadId = Just tid }
+    helper lm ProcessId = do
+      pid <- decimal
+      return $ lm { processId = Just pid }
+    helper lm Time = do
+      time' <- zonedTimeParser
+      return $ lm { timestamp = Just time' }
+    helper lm TimeUTC = do
+      time' <- zonedTimeParser
+      return $ lm { timestamp = Just time' }
+
+-- | Build a parser for a 'LogMessage' from a format string, as
+-- described by the hslogger package.
+logMessageParser
+  :: FormatString
+  -> Parser T.Text -- ^ LoggerName parser
+  -> Either String (Parser LogMessage)
+logMessageParser format loggerNameParser
+  = tfLogMessageParser format loggerNameParser zonedTimeParser
+
+-- | As 'logMessageParser', but provide a custom time format for
+-- parsing @ "$time" @ and @ "$utcTime" @ formatters. Compatible with
+-- hslogger's /tfLogFormatter/ function.
+tfLogMessageParser
+  :: FormatString
+  -> Parser T.Text -- ^ LoggerName parser
+  -> Parser ZonedTime -- ^ Time parser
+  -> Either String (Parser LogMessage)
+tfLogMessageParser format loggerNameParser zonedTimeParser = do
+  instrs <- parseOnly (formatStringParser <* endOfInput) format
+  return $ buildParser loggerNameParser zonedTimeParser instrs
+
+-- | Parse time format string @ "%F %X %Z" @ with 'defaultTimeLocale'.
+zonedTimeParser :: Parser ZonedTime
+zonedTimeParser = do
+  date <- takeWhile (inClass "-0-9")
+  skipSpace
+  time <- takeWhile (inClass ":0-9")
+  skipSpace
+  tzName <- takeWhile isAlpha
+  let
+    str
+      = date `space` time `space` tzName
+        
+  case parseTime defaultTimeLocale "%F %X %Z" (T.unpack str) of
+    Nothing ->
+      fail "zonedTimeParse: failed to parse ZonedTime"
+    Just zt ->
+      return zt
+  where
+    space a b
+      = a <> " " <> b
+    
