diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,24 @@
+# Revision history for plow-log
+
+## 0.1.6.0 -- 2022-11-23
+* Prep for OSS release
+
+## 0.1.5.0 -- 2022-10-31
+* Add instance Semigroup (LogMessage String).
+* Add Aeson instances for LogMessage.
+
+## 0.1.4.0 -- 2022-08-19
+* Add Plow.Logging.Message.
+
+## 0.1.3.0 -- 2022-1-10
+* Added Semigroup and Monoid instances for Tracer
+
+## 0.1.2.0 -- 2021-7-23
+* Added simpleStdErrLogger and a new withEitherTracer function for combining two loggers
+
+## 0.1.1.0 -- 2021-3-18
+* Cleanup in Logging and added separate Plow.Throwing
+
+## 0.1.0.0 -- 2021-3-17
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Plow Technologies
+
+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/plow-log.cabal b/plow-log.cabal
new file mode 100644
--- /dev/null
+++ b/plow-log.cabal
@@ -0,0 +1,35 @@
+cabal-version:       >=1.10
+name:                plow-log
+version:             0.1.6.0
+synopsis:            Contravariant logging library
+description:         Contravariant logging library agnostic to the logging backend
+category:            Logging
+homepage:            https://github.com/plow-technologies/plow-log.git#readme
+bug-reports:         https://github.com/plow-technologies/plow-log.git/issues
+copyright:           Plow-Technologies LLC
+license:             MIT
+license-file:        LICENSE
+author:              Sam Balco
+maintainer:          samuel.balco@plowtech.net
+build-type:          Simple
+extra-source-files:  CHANGELOG.md
+
+source-repository head
+  type: git
+  location: https://github.com/plow-technologies/plow-log.git
+
+library
+  exposed-modules:
+      Plow.Logging
+    , Plow.Logging.Message
+    , Plow.Throwing
+  other-modules:
+      Plow.Logging.EnumerableConstructors
+  hs-source-dirs:
+      src
+  ghc-options: -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates
+  build-depends:
+      base >= 4.13 && < 4.18
+      , aeson >= 2.1.1 && < 2.2
+      , text >= 1.2.5 && < 2.1
+  default-language: Haskell2010
diff --git a/src/Plow/Logging.hs b/src/Plow/Logging.hs
new file mode 100644
--- /dev/null
+++ b/src/Plow/Logging.hs
@@ -0,0 +1,121 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE RankNTypes #-}
+
+module Plow.Logging
+  ( Tracer (..),
+    traceWith,
+    HasEnumerableConstructors,
+    invalidSilencedConstructors,
+    warnInvalidSilencedConstructorsWith,
+    withSilencedTracer,
+    withAllowedTracer,
+    withMaybeTracer,
+    withEitherTracer,
+    filterTracer,
+    simpleStdOutTracer,
+    simpleStdErrTracer,
+    voidTracer,
+    IOTracer (..),
+  )
+where
+
+import Control.Monad (when)
+import Control.Monad.IO.Class (MonadIO (..))
+import Data.Functor.Contravariant (Contravariant (..))
+import Data.List (intercalate, intersect)
+import Data.Proxy (Proxy)
+import Data.String (IsString (..))
+import Plow.Logging.EnumerableConstructors
+import System.IO (hPutStrLn, stderr)
+
+newtype Tracer m a = Tracer (a -> m ())
+
+instance Monad m => Semigroup (Tracer m a) where
+  (Tracer f) <> (Tracer g) = Tracer $ \a -> f a >> g a
+
+instance Monad m => Monoid (Tracer m a) where
+  mempty = Tracer $ \_ -> pure ()
+
+class TraceWith x m where
+  traceWith :: x a -> a -> m ()
+
+instance TraceWith (Tracer m) m where
+  traceWith (Tracer t) = t
+
+instance Contravariant (Tracer m) where
+  contramap f (Tracer t) = Tracer (t . f)
+
+invalidSilencedConstructors :: HasEnumerableConstructors a => Proxy a -> [String] -> [String]
+invalidSilencedConstructors p silencedConstructors =
+  let cs = allConstructors p
+   in filter (\c -> not $ c `elem` cs) silencedConstructors
+
+-- | Given a "string-like" tracer, outputs a warning message if the supplied 'silencedConstructors' do not
+-- match the output of 'allConstructors' for the given tracer type 'a'
+warnInvalidSilencedConstructorsWith :: (Applicative m, HasEnumerableConstructors a, IsString s) => Proxy a -> [String] -> Tracer m s -> m ()
+warnInvalidSilencedConstructorsWith p silencedConstructors t = case invalidSilencedConstructors p silencedConstructors of
+  [] -> pure ()
+  invalid -> traceWith t $ fromString $ "Detected invalid silenced logging options: " <> intercalate ", " invalid
+
+filterTracer :: Applicative m => (a -> Bool) -> Tracer m a -> Tracer m a
+filterTracer test (Tracer f) = Tracer $ \m -> when (test m) (f m)
+
+-- | Modifies a given tracer so that any message with a constructor name appearing in 'silencedConstructors'
+-- will be discarded. In order to work as expected, the type 'a' is required to have an automatically
+-- derived instance of 'HasEnumerableConstructors'.
+-- @
+--    data Foo = Florb Int | Fleeb String | Bar Bool deriving (Generic, HasEnumerableConstructors)
+-- @
+-- then calling
+-- >  traceWith (withSilencedTracer ["Bar"] t) $ Bar False
+-- is equivalent to
+-- > pure ()
+-- and
+-- >  traceWith (withSilencedTracer ["Bar"] t) $ Florb 3
+-- will be definitionally equal to
+-- >  traceWith t $ Florb 3
+withSilencedTracer :: (Applicative m, HasEnumerableConstructors a) => [String] -> Tracer m a -> Tracer m a
+withSilencedTracer silencedConstructors = filterTracer (\m -> null $ listConstructors m `intersect` silencedConstructors)
+
+-- | The opposite of 'withSilencedTracer'. This tracer only loggs messages which match a constructor in
+-- 'allowedConstructors'.
+withAllowedTracer :: (Applicative m, HasEnumerableConstructors a) => [String] -> Tracer m a -> Tracer m a
+withAllowedTracer allowedConstructors = filterTracer (\m -> not $ null $ listConstructors m `intersect` allowedConstructors)
+
+-- | Turns a tracer for some 'a' into a tracer for 'Maybe a', which traces 'Just x' using the original tracer
+-- | and ignores 'Nothing' (i.e. 'pure ()')
+withMaybeTracer :: Applicative m => Tracer m a -> Tracer m (Maybe a)
+withMaybeTracer (Tracer t) = Tracer (maybe (pure ()) t)
+
+-- | Takes two tracers for values 'a'  and 'b' to a tracer for 'Either a b', which selects the appropriate tracer for each value
+withEitherTracer :: Applicative m => Tracer m a -> Tracer m b -> Tracer m (Either a b)
+withEitherTracer (Tracer ta) (Tracer tb) = Tracer $ \case
+  Left a -> ta a
+  Right b -> tb b
+
+simpleStdOutTracer :: MonadIO m => Tracer m String
+simpleStdOutTracer = Tracer $ liftIO . putStrLn
+
+simpleStdErrTracer :: MonadIO m => Tracer m String
+simpleStdErrTracer = Tracer $ liftIO . hPutStrLn stderr
+
+-- | Tracer that discards/ignores all messages. Useful in test suites, if we don't care about logging output
+voidTracer :: Applicative m => Tracer m t
+voidTracer = Tracer $ const $ pure ()
+
+-- | To avoid having to write those pesky 'liftIO's when changing monads, e.g. when
+-- passing the logger into a servant server, we can hide the monad entirely behind an
+-- existential, requiring a 'MonadIO' instance.
+-- We wrap in newtype instead of just a type synonym, to avoid having to have
+-- RankNTypes turned on everywhere.
+newtype IOTracer a = IOTracer (forall m. MonadIO m => Tracer m a)
+
+instance Contravariant IOTracer where
+  contramap f (IOTracer t) = IOTracer $ contramap f t
+
+instance MonadIO m => TraceWith IOTracer m where
+  traceWith (IOTracer (Tracer t)) = t
diff --git a/src/Plow/Logging/EnumerableConstructors.hs b/src/Plow/Logging/EnumerableConstructors.hs
new file mode 100644
--- /dev/null
+++ b/src/Plow/Logging/EnumerableConstructors.hs
@@ -0,0 +1,80 @@
+{-# LANGUAGE DefaultSignatures #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+
+module Plow.Logging.EnumerableConstructors (HasEnumerableConstructors (..)) where
+
+import Data.Proxy
+import GHC.Generics
+
+class HasEnumerableConstructors1 f where
+  listConstructors1 :: f p -> [String]
+  allConstructors1 :: Proxy f -> [String]
+
+instance HasEnumerableConstructors1 V1 where
+  listConstructors1 _ = []
+  allConstructors1 _ = []
+
+instance HasEnumerableConstructors1 U1 where
+  listConstructors1 _ = []
+  allConstructors1 _ = []
+
+instance (HasEnumerableConstructors a) => HasEnumerableConstructors1 (K1 i a) where
+  listConstructors1 (K1 x) = listConstructors x
+  allConstructors1 _ = allConstructors (Proxy :: Proxy a)
+
+instance HasEnumerableConstructors1 f => HasEnumerableConstructors1 (D1 c f) where
+  listConstructors1 (M1 x) = listConstructors1 x
+  allConstructors1 _ = allConstructors1 (Proxy :: Proxy f)
+
+instance (Constructor c, HasEnumerableConstructors1 f) => HasEnumerableConstructors1 (C1 c f) where
+  listConstructors1 x@(M1 y) = [conName x] ++ listConstructors1 y
+  allConstructors1 _ = [conName (undefined :: C1 c f g)] ++ allConstructors1 (Proxy :: Proxy f)
+
+instance HasEnumerableConstructors1 f => HasEnumerableConstructors1 (S1 c f) where
+  listConstructors1 (M1 x) = listConstructors1 x
+  allConstructors1 _ = allConstructors1 (Proxy :: Proxy f)
+
+instance (HasEnumerableConstructors1 a, HasEnumerableConstructors1 b) => HasEnumerableConstructors1 (a :+: b) where
+  listConstructors1 (L1 x) = listConstructors1 x
+  listConstructors1 (R1 x) = listConstructors1 x
+
+  allConstructors1 _ = (allConstructors1 (Proxy :: Proxy a) ++ (allConstructors1 (Proxy :: Proxy b)))
+
+instance (HasEnumerableConstructors1 a, HasEnumerableConstructors1 b) => HasEnumerableConstructors1 (a :*: b) where
+  listConstructors1 (a :*: b) = listConstructors1 a ++ listConstructors1 b
+  allConstructors1 _ = (allConstructors1 (Proxy :: Proxy a) ++ (allConstructors1 (Proxy :: Proxy b)))
+
+instance HasEnumerableConstructors1 f => HasEnumerableConstructors1 (Rec1 f) where
+  listConstructors1 (Rec1 a) = listConstructors1 a
+  allConstructors1 _ = allConstructors1 (Proxy :: Proxy f)
+
+class HasEnumerableConstructors a where
+  listConstructors :: a -> [String]
+  allConstructors :: Proxy a -> [String]
+  default listConstructors :: (Generic a, HasEnumerableConstructors1 (Rep a)) => a -> [String]
+  listConstructors = listConstructors1 . from
+
+  default allConstructors :: (Generic a, HasEnumerableConstructors1 (Rep a)) => Proxy a -> [String]
+  allConstructors _ = allConstructors1 (Proxy :: Proxy (Rep a))
+
+instance {-# OVERLAPPABLE #-} HasEnumerableConstructors a where
+  listConstructors _ = []
+  allConstructors _ = []
+
+instance HasEnumerableConstructors a => HasEnumerableConstructors [a] where
+  listConstructors = concatMap listConstructors
+  allConstructors _ = allConstructors (Proxy :: Proxy a)
+
+instance HasEnumerableConstructors a => HasEnumerableConstructors (Maybe a) where
+  listConstructors = maybe [] listConstructors
+  allConstructors _ = allConstructors (Proxy :: Proxy a)
+
+instance (HasEnumerableConstructors a, HasEnumerableConstructors b) => HasEnumerableConstructors (Either a b) where
+  listConstructors = either listConstructors listConstructors
+  allConstructors _ = allConstructors (Proxy :: Proxy a) ++ allConstructors (Proxy :: Proxy b)
diff --git a/src/Plow/Logging/Message.hs b/src/Plow/Logging/Message.hs
new file mode 100644
--- /dev/null
+++ b/src/Plow/Logging/Message.hs
@@ -0,0 +1,53 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Plow.Logging.Message
+  ( LogLevel (..),
+    LogMessage (..),
+  )
+where
+
+import Data.Aeson (FromJSON (parseJSON), ToJSON, withText)
+import Data.Text (Text)
+import qualified Data.Text as Text
+import GHC.Generics (Generic)
+import Plow.Logging (HasEnumerableConstructors)
+
+-- | Generic log levels
+data LogLevel = LevelDebug | LevelInfo | LevelWarn | LevelError | LevelOther Text
+  deriving (Eq, Ord, Generic, HasEnumerableConstructors, ToJSON)
+
+instance Show LogLevel where
+  show LevelDebug = "DEBUG"
+  show LevelInfo = "INFO"
+  show LevelWarn = "WARN"
+  show LevelError = "ERROR"
+  show (LevelOther l) = Text.unpack l
+
+instance FromJSON LogLevel where
+  parseJSON = withText "LogLevel" $ \case
+    "DEBUG" -> pure LevelDebug
+    "INFO" -> pure LevelInfo
+    "WARN" -> pure LevelWarn
+    "ERROR" -> pure LevelError
+    other -> pure $ LevelOther other
+
+-- | A log message that allows for a log level to be specified or for the log
+-- to be ignored.
+data LogMessage a
+  = Stdout LogLevel a
+  | Ignore
+  deriving (Show, Generic, HasEnumerableConstructors)
+
+-- | Concatenates two log messages with a newline, keeping the log level of that has the higher severity
+instance (Semigroup String) => Semigroup (LogMessage String) where
+  a <> b = case (a, b) of
+    (Stdout logLevelA a', Stdout logLevelB b') -> Stdout (max logLevelA logLevelB) (a' <> "\n" <> b')
+    (Stdout logLevelA a', Ignore) -> Stdout logLevelA a'
+    (Ignore, Stdout logLevelB b') -> Stdout logLevelB b'
+    (Ignore, Ignore) -> Ignore
diff --git a/src/Plow/Throwing.hs b/src/Plow/Throwing.hs
new file mode 100644
--- /dev/null
+++ b/src/Plow/Throwing.hs
@@ -0,0 +1,17 @@
+{-# LANGUAGE RankNTypes #-}
+
+module Plow.Throwing where
+
+import Data.Functor.Contravariant (Contravariant (..))
+import Plow.Logging
+
+newtype Thrower m a = Thrower (forall b. a -> m b)
+
+instance Contravariant (Thrower m) where
+  contramap f (Thrower t) = Thrower (t . f)
+
+throwWith :: Thrower m a -> a -> m b
+throwWith (Thrower t) a = t a
+
+withTracer :: Monad m => Tracer m a -> Thrower m a -> Thrower m a
+withTracer (Tracer tr) (Thrower th) = Thrower $ \m -> tr m >> th m
