diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for eventuo11y
+
+## 0.1.0.0 -- 2022-09-28
+
+Initial release
diff --git a/Example.hs b/Example.hs
new file mode 100644
--- /dev/null
+++ b/Example.hs
@@ -0,0 +1,147 @@
+-- Search for comments to find commentary of interest
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import Control.Exception
+import Control.Monad
+import Data.Aeson
+import Data.ByteString.Internal
+import Data.Void
+import Foreign.C.Error
+import Foreign.C.Types
+import Foreign.ForeignPtr.Safe
+import Foreign.Ptr
+import GHC.Generics
+import Observe.Event
+import Observe.Event.Render.IO.JSON
+import Observe.Event.Render.JSON
+import System.FilePath
+import System.IO.Temp
+import System.Posix.Files
+import System.Posix.IO
+import System.Posix.Types
+
+-- Pretend this is in a separate module File where
+
+-- We take an EventBackend, polymorphic in r, supporting our domain-specific selector type
+writeToFile :: EventBackend IO r FileSelector -> FilePath -> ByteString -> IO ()
+writeToFile backend path (BS fptr sz) = do
+  -- We start an event, selected by OpenFile
+  fd <- withEvent backend OpenFile $ \ev -> do
+    -- We add a Filename field to our current active event
+    addField ev $ Filename path
+
+    fd <- openFd path WriteOnly (Just regularFileMode) defaultFileFlags
+    when (fd == -1) $ do
+      errno <- getErrno
+      -- Throw an exception which we can render as JSON
+      throw $ BadOpen path errno
+
+    addField ev $ FileFd fd
+    pure fd
+  withForeignPtr fptr $ \ptr -> do
+    let bcSz = fromIntegral sz
+        go :: ByteCount -> IO ()
+        go offset = do
+          newOffset <- withEvent backend WriteFile $ \ev -> do
+            let ct = bcSz - offset
+            addField ev $ BytesAsked ct
+            written <- fdWriteBuf fd (plusPtr ptr (fromIntegral offset)) ct
+            addField ev $ BytesActual written
+            pure $ offset + written
+          when (newOffset < bcSz) $
+            go newOffset
+    go 0
+  closeFd fd
+  pure ()
+
+-- Define our selector type. A GADT, parameterized by field type
+data FileSelector f where
+  OpenFile :: FileSelector OpenField
+  WriteFile :: FileSelector WriteField
+
+-- Define a renderer for our selector type
+renderFileSelector :: RenderSelectorJSON FileSelector
+renderFileSelector OpenFile = ("open-file", renderOpenField) -- Expressed in terms of renderers for our field tyeps
+renderFileSelector WriteFile = ("write-file", renderWriteField)
+
+-- Define a field type
+data OpenField
+  = Filename !FilePath
+  | FileFd !Fd
+
+-- Define a renderer for a field type
+renderOpenField :: RenderFieldJSON OpenField
+renderOpenField (Filename path) = ("file-name", toJSON path)
+renderOpenField (FileFd fd) = ("file-fd", toJSON fd)
+
+data WriteField
+  = BytesAsked !ByteCount
+  | BytesActual !ByteCount
+
+renderWriteField :: RenderFieldJSON WriteField
+renderWriteField (BytesAsked ct) = ("asked", toJSON ct)
+renderWriteField (BytesActual ct) = ("actual", toJSON ct)
+
+-- Define a new exception that can be used with simpleJsonStderrBackend
+data BadOpen = BadOpen
+  { path :: !FilePath,
+    errno :: !Errno
+  }
+  deriving (Show, ToJSON, Generic)
+
+-- Our exception is beneath SomeJSONException in the hierarchy
+instance Exception BadOpen where
+  toException = jsonExceptionToException
+  fromException = jsonExceptionFromException
+
+-- end module File
+
+-- Note a different selector type than writeToFile
+instrumentedMain :: EventBackend IO r MainSelector -> IO ()
+instrumentedMain backend = do
+  withEvent backend UsingTempDir $ \ev -> do
+    withSystemTempDirectory "example" $ \dir -> do
+      addField ev dir
+      let -- Create a new EventBackend where all parentless events are made children of our current event
+          subBackend = subEventBackend ev
+          -- Narrow subBackend to create events from FileSelectors instead of MainSelectors
+          narrowerBackend = narrowEventBackend InjectFileSelector subBackend
+      -- Pass our narrower backend to writeToFile
+      writeToFile narrowerBackend (dir </> "example.txt") "example"
+
+main :: IO ()
+main =
+  -- Initialize a backend to write JSON to stderr and use it.
+  simpleJsonStderrBackend renderMainSelector >>= instrumentedMain
+
+-- Instrumentation definitions
+data MainSelector f where
+  UsingTempDir :: MainSelector FilePath
+  InjectFileSelector :: FileSelector a -> MainSelector a -- A typical pattern together with narrowEventBackend to call functions with a more specialized selector type
+
+renderMainSelector :: RenderSelectorJSON MainSelector
+renderMainSelector UsingTempDir = ("using-tmp-dir", \f -> ("path", toJSON f))
+renderMainSelector (InjectFileSelector filesel) = renderFileSelector filesel -- Note that we reuse the selector renderer from our File "module"
+
+deriving instance Show Errno
+
+deriving instance ToJSON Errno
+
+deriving instance Generic Errno
+
+deriving instance ToJSON CInt
+
+deriving instance Generic CInt
+
+deriving instance ToJSON Fd
+
+deriving instance Generic Fd
+
+deriving instance ToJSON ByteCount
+
+deriving instance Generic ByteCount
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright 2022 Shea Levy.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this project except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/eventuo11y.cabal b/eventuo11y.cabal
new file mode 100644
--- /dev/null
+++ b/eventuo11y.cabal
@@ -0,0 +1,71 @@
+cabal-version:      3.0
+name:               eventuo11y
+version:            0.1.0.0
+synopsis:           An event-oriented observability library
+description:
+  Instrument your Haskell codebase with wide, semantically meaningful events.
+  This library is designed with separating the following concerns in mind:
+
+  [@Writing instrumentation@] When instrumenting code, I want to think in terms of my
+  application domain and report any information I might need to infer internal
+  application-level state and understand the behavior of my program/library.
+
+  [@Consuming instrumentation@] When consuming instrumentation, I want to think in
+  terms of the API fo the specific backend I'm supporting (writing to @stderr@,
+  serving a @Prometheus@ page, posting to @OpenTelemetry@) and what is needed to
+  render to that API.
+
+  [@Initializing instrumentation in an application@] When I'm ready to tie it all
+  together, I want to identify the specific backends I want to post to and provide
+  the bridge code to render the domain-specific instrumentation as needed for those
+  backends. I also want to handle concerns like sampling or client-side aggregation
+  of domain-specific instrumentation to keep usage manageable.
+
+  See "Observe.Event" for detailed documentation on instrumenting your code.
+
+  See "Observe.Event.Implementation" for documentation on writing an
+  @EventBackend@.
+
+  See [Example.hs](https://github.com/shlevy/eventuo11y/tree/v0.1.0.0/Example.hs) for an example.
+
+  See [eventuo11y-batteries](https://hackage.haskell.org/package/eventuo11y-batteries) for miscellaneous
+  framework-specific helpers.
+
+bug-reports:        https://github.com/shlevy/eventuo11y/issues
+license:            Apache-2.0
+license-file:       LICENSE
+author:             Shea Levy
+maintainer:         shea@shealevy.com
+copyright:          Copyright 2022 Shea Levy.
+category:           Observability
+extra-source-files:
+  CHANGELOG.md
+  Example.hs
+
+tested-with:        GHC ==8.10.7 || ==9.0.2 || ==9.2.2
+
+source-repository head
+  type:     git
+  location: https://github.com/shlevy/eventuo11y
+
+library
+  exposed-modules:
+    Observe.Event
+    Observe.Event.Dynamic
+    Observe.Event.Implementation
+    Observe.Event.Render.IO.JSON
+    Observe.Event.Render.JSON
+
+  build-depends:
+    , aeson          ^>=2.0.3.0
+    , base           >=4.14      && <4.17
+    , bytestring     >=0.10.12.0 && <0.12
+    , exceptions     ^>=0.10.4
+    , resourcet      ^>=1.2.4.3
+    , text           ^>=1.2.5.0
+    , time           >=1.9.3     && <1.12
+    , unliftio-core  ^>=0.2.0.1
+    , uuid           ^>=1.3.15
+
+  hs-source-dirs:   src
+  default-language: Haskell2010
diff --git a/src/Observe/Event.hs b/src/Observe/Event.hs
new file mode 100644
--- /dev/null
+++ b/src/Observe/Event.hs
@@ -0,0 +1,462 @@
+{-# LANGUAGE ApplicativeDo #-}
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE RecordWildCards #-}
+
+-- |
+-- Description : Core interface for instrumentation with eventuo11y
+-- Copyright   : Copyright 2022 Shea Levy.
+-- License     : Apache-2.0
+-- Maintainer  : shea@shealevy.com
+--
+-- This is the primary module needed to instrument code with eventuo11y.
+--
+-- Instrumentors should first define selector types and field types
+-- appropriate to the unit of code they're instrumenting:
+--
+-- Selectors are values which designate the general category of event
+-- being created, parameterized by the type of fields that can be added to it.
+-- For example, a web service's selector type may have a @ServicingRequest@
+-- constructor, whose field type includes a @ResponseCode@ constructor which
+-- records the HTTP status code. Selectors are intended to be of a domain-specific
+-- type per unit of functionality within an instrumented codebase, implemented as a GADT
+-- (but see t'Observe.Event.Dynamic.DynamicEventSelector' for a generic option).
+--
+-- Fields make up the basic data captured in an event. They should be added
+-- to an 'Event' as the code progresses through various phases of work, and can
+-- be both milestone markers ("we got this far in the process") or more detailed
+-- instrumentation ("we've processed N records"). They are intended to be of a
+-- domain-specific type per unit of functionality within an instrumented codebase
+-- (but see t'Observe.Event.Dynamic.DynamicField' for a generic option).
+--
+-- Instrumentation then centers around 'Event's, populated using the
+-- <#g:eventmanip event manipulation functions>. 'Event's are initialized
+-- with 'EventBackend's, typically via the
+-- <#g:resourcesafe resource-safe event allocation functions>.
+--
+-- Depending on which 'EventBackend's may end up consuming the 'Event's,
+-- instrumentors will also need to define renderers for their selectors
+-- and fields. For example, they may need to implement values of types
+--  t'Observe.Event.Render.JSON.RenderSelectorJSON' and
+--  t'Observe.Event.Render.JSON.RenderFieldJSON' to use JSON rendering 'EventBackend's.
+module Observe.Event
+  ( Event,
+
+    -- * Event manipulation #eventmanip#
+    reference,
+    addField,
+    addParent,
+    addProximate,
+
+    -- * Resource-safe event allocation #resourcesafe#
+    withEvent,
+    withSubEvent,
+
+    -- ** Acquire/MonadResource variants
+    acquireEvent,
+    acquireSubEvent,
+
+    -- * 'EventBackend's
+    EventBackend,
+    subEventBackend,
+    unitEventBackend,
+    pairEventBackend,
+    hoistEventBackend,
+    narrowEventBackend,
+    narrowEventBackend',
+
+    -- * Primitive 'Event' resource management.
+
+    -- | Prefer the [resource-safe event allocation functions](#g:resourcesafe)
+    -- to these when possible.
+    finalize,
+    failEvent,
+    newEvent,
+    newSubEvent,
+  )
+where
+
+import Control.Exception
+import Control.Monad.Catch
+import Control.Monad.IO.Unlift
+import Data.Acquire
+import Data.Functor
+import Observe.Event.Implementation
+
+-- | An instrumentation event.
+--
+-- 'Event's are the core of the instrumenting user's interface
+-- to eventuo11y. Typical usage would be to create an 'Event'
+-- from an 'EventBackend' with 'withEvent', or as a child of
+-- an another 'Event' with 'withSubEvent', and add fields to
+-- the 'Event' at appropriate points in your code with
+-- 'addField'.
+--
+-- [@m@]: The monad we're instrumenting in.
+-- [@r@]: The type of event references. See 'reference'.
+-- [@s@]: The type of event selectors for child events. See 'EventBackend'.
+-- [@f@]: The type of fields on this event. See 'addField'.
+data Event m r s f = Event
+  { -- | The 'EventBackend' this 'Event' was generated from.
+    backend :: !(EventBackend m r s),
+    -- | The underlying 'EventImpl' implementing the event functionality.
+    impl :: !(EventImpl m r f),
+    -- | A 'OnceFlag' to ensure we only finish ('finalize' or 'failEvent') once.
+    finishFlag :: !(OnceFlag m)
+  }
+
+-- | Obtain a reference to an 'Event'.
+--
+-- References are used to link 'Event's together, either in
+-- parent-child relationships with 'addParent' or in
+-- cause-effect relationships with 'addProximate'.
+--
+-- References can live past when an event has been 'finalize'd or
+-- 'failEvent'ed.
+--
+-- Code being instrumented should always have @r@ as an unconstrained
+-- type parameter, both because it is an implementation concern for
+-- 'EventBackend's and because references are backend-specific and it
+-- would be an error to reference an event in one backend from an event
+-- in a different backend.
+reference :: Event m r s f -> r
+reference (Event {..}) = referenceImpl impl
+
+-- | Add a field to an 'Event'.
+--
+-- Fields make up the basic data captured in an event. They should be added
+-- to an 'Event' as the code progresses through various phases of work, and can
+-- be both milestone markers ("we got this far in the process") or more detailed
+-- instrumentation ("we've processed N records").
+--
+-- They are intended to be of a domain specific type per unit of functionality
+-- within an instrumented codebase (but see t'Observe.Event.Dynamic.DynamicField'
+-- for a generic option).
+addField ::
+  Event m r s f ->
+  -- | The field to add to the event.
+  f ->
+  m ()
+addField (Event {..}) = addFieldImpl impl
+
+-- | Mark another 'Event' as a parent of this 'Event'.
+addParent ::
+  Event m r s f ->
+  -- | A reference to the parent, obtained via 'reference'.
+  r ->
+  m ()
+addParent (Event {..}) = addParentImpl impl
+
+-- | Mark another 'Event' as a proximate cause of this 'Event'.
+addProximate ::
+  Event m r s f ->
+  -- | A reference to the proximate cause, obtained via 'reference'.
+  r ->
+  m ()
+addProximate (Event {..}) = addProximateImpl impl
+
+-- | Mark an 'Event' as finished.
+--
+-- In normal usage, this should be automatically called via the use of
+-- the [resource-safe event allocation functions](#g:resourcesafe).
+--
+-- This is a no-op if the 'Event' has already been 'finalize'd or
+-- 'failEvent'ed. As a result, it is likely pointless to call
+-- 'addField', 'addParent', or 'addProximate' after this call,
+-- though it still may be reasonable to call 'reference'.
+finalize :: (Monad m) => Event m r s f -> m ()
+finalize (Event {..}) = runOnce finishFlag $ finalizeImpl impl
+
+-- | Mark an 'Event' as having failed, possibly due to an 'Exception'.
+--
+-- In normal usage, this should be automatically called via the use of
+-- the [resource-safe event allocation functions](#g:resourcesafe).
+--
+-- This is a no-op if the 'Event' has already been 'finalize'd or
+-- 'failEvent'ed. As a result, it is likely pointless to call
+-- 'addField', 'addParent', or 'addProximate' after this call,
+-- though it still may be reasonable to call 'reference'.
+failEvent :: (Monad m) => Event m r s f -> Maybe SomeException -> m ()
+failEvent (Event {..}) = runOnce finishFlag . failImpl impl
+
+-- | Create a new 'Event', selected by the given selector.
+--
+-- The selector specifies the category of new event we're creating, as well
+-- as the type of fields that can be added to it (with 'addField').
+--
+-- Selectors are intended to be of a domain specific type per unit of
+-- functionality within an instrumented codebase, implemented as a GADT
+-- (but see t'Observe.Event.Dynamic.DynamicEventSelector' for a generic option).
+--
+-- Consider the [resource-safe event allocation functions](#g:resourcesafe) instead
+-- of calling this directly.
+newEvent ::
+  (Applicative m) =>
+  EventBackend m r s ->
+  forall f.
+  -- | The event selector.
+  s f ->
+  m (Event m r s f)
+newEvent backend@(EventBackend {..}) sel = do
+  impl <- newEventImpl sel
+  finishFlag <- newOnceFlag
+  pure Event {..}
+
+-- | Create a new 'Event' as a child of the given 'Event', selected by the given selector.
+--
+-- The selector specifies the category of new event we're creating, as well
+-- as the type of fields that can be added to it (with 'addField').
+--
+-- Selectors are intended to be of a domain specific type per unit of
+-- functionality within an instrumented codebase, implemented as a GADT
+-- (but see t'Observe.Event.Dynamic.DynamicEventSelector' for a generic option).
+--
+-- Consider the [resource-safe event allocation functions](#g:resourcesafe) instead
+-- of calling this directly.
+newSubEvent ::
+  (Monad m) =>
+  -- | The parent event.
+  Event m r s f ->
+  forall f'.
+  -- | The child event selector.
+  s f' ->
+  m (Event m r s f')
+newSubEvent (Event {..}) sel = do
+  child <- newEvent backend sel
+  addParent child $ referenceImpl impl
+  pure child
+
+-- | Run an action with a new 'Event', selected by the given selector.
+--
+-- The selector specifies the category of new event we're creating, as well
+-- as the type of fields that can be added to it (with 'addField').
+--
+-- Selectors are intended to be of a domain specific type per unit of
+-- functionality within an instrumented codebase, implemented as a GADT
+-- (but see t'Observe.Event.Dynamic.DynamicEventSelector' for a generic option).
+--
+-- The 'Event' is automatically 'finalize'd (or, if appropriate, 'failEvent'ed)
+-- at the end of the function it's passed to.
+withEvent ::
+  (MonadMask m) =>
+  EventBackend m r s ->
+  forall f.
+  -- | The event selector.
+  s f ->
+  (Event m r s f -> m a) ->
+  m a
+withEvent backend sel go = do
+  (res, ()) <- generalBracket (newEvent backend sel) release go
+  pure res
+  where
+    release ev (ExitCaseSuccess _) = finalize ev
+    release ev (ExitCaseException e) = failEvent ev $ Just e
+    release ev ExitCaseAbort = failEvent ev Nothing
+
+-- | Run an action with a new 'Event' as a child of the given 'Event', selected by the given selector.
+--
+-- The selector specifies the category of new event we're creating, as well
+-- as the type of fields that can be added to it (with 'addField').
+--
+-- Selectors are intended to be of a domain specific type per unit of
+-- functionality within an instrumented codebase, implemented as a GADT
+-- (but see t'Observe.Event.Dynamic.DynamicEventSelector' for a generic option).
+--
+-- The 'Event' is automatically 'finalize'd (or, if appropriate, 'failEvent'ed)
+-- at the end of the function it's passed to.
+withSubEvent ::
+  (MonadMask m) =>
+  -- | The parent 'Event'.
+  Event m r s f ->
+  forall f'.
+  -- | The child event selector.
+  s f' ->
+  (Event m r s f' -> m a) ->
+  m a
+withSubEvent (Event {..}) sel go = withEvent backend sel $ \child -> do
+  addParent child $ referenceImpl impl
+  go child
+
+-- | An 'Acquire' variant of 'withEvent', usable in a t'Control.Monad.Trans.Resource.MonadResource' with 'allocateAcquire'.
+--
+-- Until [snoyberg/conduit#460](https://github.com/snoyberg/conduit/issues/460) is addressed, exception
+-- information will not be captured.
+acquireEvent ::
+  (MonadUnliftIO m) =>
+  EventBackend m r s ->
+  forall f.
+  -- | The event selector.
+  s f ->
+  m (Acquire (Event m r s f))
+acquireEvent backend sel = withRunInIO $ \runInIO ->
+  pure $
+    mkAcquireType
+      (runInIO $ newEvent backend sel)
+      (release runInIO)
+  where
+    release runInIO ev ReleaseException = runInIO $ failEvent ev Nothing
+    release runInIO ev _ = runInIO $ finalize ev
+
+-- | An 'Acquire' variant of 'withSubEvent', usable in a t'Control.Monad.Trans.Resource.MonadResource' with 'allocateAcquire'.
+--
+-- Until [snoyberg/conduit#460](https://github.com/snoyberg/conduit/issues/460) is addressed, exception
+-- information will not be captured.
+acquireSubEvent ::
+  (MonadUnliftIO m) =>
+  -- | The parent event.
+  Event m r s f ->
+  forall f'.
+  -- | The child event selector.
+  s f' ->
+  m (Acquire (Event m r s f'))
+acquireSubEvent (Event {..}) sel = do
+  childAcq <- acquireEvent backend sel
+  withRunInIO $ \runInIO -> pure $ do
+    child <- childAcq
+    liftIO . runInIO . addParent child $ referenceImpl impl
+    pure child
+
+-- | An 'EventBackend' where every otherwise parentless event will be marked
+-- as a child of the given 'Event'.
+subEventBackend ::
+  (Monad m) =>
+  -- | The parent event.
+  Event m r s f ->
+  EventBackend m r s
+subEventBackend ev@(Event {..}) =
+  EventBackend
+    { newEventImpl = \sel -> do
+        EventImpl {..} <- newEventImpl backend sel
+        parentAdded <- newOnceFlag backend
+        pure $
+          EventImpl
+            { addParentImpl = \r -> do
+                _ <- checkAndSet parentAdded
+                addParentImpl r,
+              finalizeImpl = do
+                runOnce parentAdded (addParentImpl $ reference ev)
+                finalizeImpl,
+              failImpl = \e -> do
+                runOnce parentAdded (addParentImpl $ reference ev)
+                failImpl e,
+              ..
+            },
+      newOnceFlag = newOnceFlag backend
+    }
+
+-- | A no-op 'EventBackend'.
+--
+-- This can be used if calling instrumented code from an un-instrumented
+-- context, or to purposefully ignore instrumentation from some call.
+--
+-- 'unitEventBackend' is the algebraic unit of 'pairEventBackend'.
+unitEventBackend :: Applicative m => EventBackend m () s
+unitEventBackend =
+  EventBackend
+    { newEventImpl = \_ ->
+        pure $
+          EventImpl
+            { referenceImpl = (),
+              addFieldImpl = const $ pure (),
+              addParentImpl = const $ pure (),
+              addProximateImpl = const $ pure (),
+              finalizeImpl = pure (),
+              failImpl = const $ pure ()
+            },
+      newOnceFlag = pure alwaysNewOnceFlag
+    }
+
+-- | An 'EventBackend' which sequentially generates 'Event's in the two given 'EventBackend's.
+--
+-- This can be used to emit instrumentation in multiple ways (e.g. logs to grafana and metrics on
+-- a prometheus HTML page).
+pairEventBackend :: Applicative m => EventBackend m a s -> EventBackend m b s -> EventBackend m (a, b) s
+pairEventBackend x y =
+  EventBackend
+    { newEventImpl = \sel -> do
+        xImpl <- newEventImpl x sel
+        yImpl <- newEventImpl y sel
+        pure $
+          EventImpl
+            { referenceImpl = (referenceImpl xImpl, referenceImpl yImpl),
+              addFieldImpl = \f -> addFieldImpl xImpl f *> addFieldImpl yImpl f,
+              addParentImpl = \(px, py) -> addParentImpl xImpl px *> addParentImpl yImpl py,
+              addProximateImpl = \(px, py) -> addProximateImpl xImpl px *> addProximateImpl yImpl py,
+              finalizeImpl = finalizeImpl xImpl *> finalizeImpl yImpl,
+              failImpl = \e -> failImpl xImpl e *> failImpl yImpl e
+            },
+      newOnceFlag = do
+        xOnce <- newOnceFlag x
+        yOnce <- newOnceFlag y
+        pure $
+          OnceFlag $ do
+            xSet <- checkAndSet xOnce
+            ySet <- checkAndSet yOnce
+            pure $ case (xSet, ySet) of
+              (NewlySet, NewlySet) -> NewlySet
+              _ -> AlreadySet
+    }
+
+-- | Hoist an 'EventBackend' along a given natural transformation into a new monad.
+hoistEventBackend ::
+  (Functor m, Functor n) =>
+  -- | Natural transformation from @m@ to @n@.
+  (forall x. m x -> n x) ->
+  EventBackend m r s ->
+  EventBackend n r s
+hoistEventBackend nt backend =
+  EventBackend
+    { newEventImpl = nt . fmap hoistEventImpl . newEventImpl backend,
+      newOnceFlag = hoistOnceFlag nt <$> (nt $ newOnceFlag backend)
+    }
+  where
+    hoistEventImpl (EventImpl {..}) =
+      EventImpl
+        { referenceImpl,
+          addFieldImpl = nt . addFieldImpl,
+          addParentImpl = nt . addParentImpl,
+          addProximateImpl = nt . addProximateImpl,
+          finalizeImpl = nt $ finalizeImpl,
+          failImpl = nt . failImpl
+        }
+
+-- | Narrow an 'EventBackend' to a new selector type via a given injection function.
+--
+-- A typical usage, where component A calls component B, would be to have A's selector
+-- type have a constructor to take any value of B's selector type (and preserve the field)
+-- and then call 'narrowEventBackend' with that constructor when invoking functions in B.
+--
+-- See 'narrowEventBackend'' for a more general, if unweildy, variant.
+narrowEventBackend ::
+  (Functor m) =>
+  -- | Inject a narrow selector into the wider selector type.
+  (forall f. s f -> t f) ->
+  EventBackend m r t ->
+  EventBackend m r s
+narrowEventBackend inj =
+  narrowEventBackend'
+    (\sel withInjField -> withInjField (inj sel) id)
+
+-- | Narrow an 'EventBackend' to a new selector type via a given injection function.
+--
+-- See 'narrowEventBackend' for a simpler, if less general, variant.
+narrowEventBackend' ::
+  (Functor m) =>
+  -- | Simultaneously inject a narrow selector into the wider selector type
+  -- and the narrow selector's field into the wider selector's field type.
+  (forall f. s f -> forall a. (forall g. t g -> (f -> g) -> a) -> a) ->
+  EventBackend m r t ->
+  EventBackend m r s
+narrowEventBackend' inj backend =
+  EventBackend
+    { newEventImpl = \sel -> inj sel \sel' injField ->
+        newEventImpl backend sel' <&> \case
+          EventImpl {..} ->
+            EventImpl
+              { addFieldImpl = addFieldImpl . injField,
+                ..
+              },
+      newOnceFlag = newOnceFlag backend
+    }
diff --git a/src/Observe/Event/Dynamic.hs b/src/Observe/Event/Dynamic.hs
new file mode 100644
--- /dev/null
+++ b/src/Observe/Event/Dynamic.hs
@@ -0,0 +1,61 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+
+-- |
+-- Description : "Dynamically typed" Event selectors and fields.
+-- Copyright   : Copyright 2022 Shea Levy.
+-- License     : Apache-2.0
+-- Maintainer  : shea@shealevy.com
+--
+-- Instrumentors can use the types in this module if they don't want
+-- to define domain-specific types for the code they're instrumenting.
+module Observe.Event.Dynamic
+  ( DynamicEventSelector (..),
+    DynamicField (..),
+
+    -- * Shorthand types
+    DynamicEventBackend,
+    DynamicEvent,
+  )
+where
+
+import Data.Aeson
+import Data.String
+import Data.Text
+import Observe.Event
+
+-- | A simple type usable as an 'EventBackend' selector.
+--
+-- All 'Event's have 'DynamicField' field types.
+--
+-- Individual 'DynamicEventSelector's are typically constructed
+-- via the 'IsString' instance, e.g. @withEvent backend "foo" go@
+-- will call @go@ with an @Event m r DynamicEventSelector DynamicField@
+-- named "foo".
+data DynamicEventSelector f where
+  DynamicEventSelector :: !Text -> DynamicEventSelector DynamicField
+
+instance (f ~ DynamicField) => IsString (DynamicEventSelector f) where
+  fromString = DynamicEventSelector . fromString
+
+-- | A simple type usable as an 'Event' field type.
+--
+-- Individual 'DynamicField's are typically constructed via
+-- the 'IsString' instance, using 'ToJSON' for the value,
+-- e.g. @addField ev $ "foo" x@ will add @DynamicField "foo" (toJSON x)@
+-- as a field to @ev@.
+data DynamicField = DynamicField
+  { name :: !Text,
+    value :: !Value
+  }
+
+-- | Treat a string as a function to a 'DynamicField', calling
+-- 'toJSON' on its argument.
+instance (ToJSON x) => IsString (x -> DynamicField) where
+  fromString s = DynamicField (fromString s) . toJSON
+
+-- | Shorthand for an 'EventBackend' using 'DynamicEventSelector's.
+type DynamicEventBackend m r = EventBackend m r DynamicEventSelector
+
+-- | Shorthand for an 'Event' using 'DynamicField's.
+type DynamicEvent m r = Event m r DynamicEventSelector DynamicField
diff --git a/src/Observe/Event/Implementation.hs b/src/Observe/Event/Implementation.hs
new file mode 100644
--- /dev/null
+++ b/src/Observe/Event/Implementation.hs
@@ -0,0 +1,136 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE RankNTypes #-}
+
+-- |
+-- Description : Interface for implementing EventBackends
+-- Copyright   : Copyright 2022 Shea Levy.
+-- License     : Apache-2.0
+-- Maintainer  : shea@shealevy.com
+--
+-- This is the primary module needed to write new 'EventBackend's.
+module Observe.Event.Implementation
+  ( EventBackend (..),
+    EventImpl (..),
+
+    -- * OnceFlags
+
+    -- | Generic helper to make operations idempotent.
+    OnceFlag (..),
+    FlagState (..),
+    runOnce,
+    hoistOnceFlag,
+    alwaysNewOnceFlag,
+    newOnceFlagIO,
+  )
+where
+
+import Control.Concurrent.MVar
+import Control.Exception
+import Data.Functor
+
+-- | A backend for creating t'Observe.Event.Event's.
+--
+-- Different 'EventBackend's will be used to emit instrumentation to
+-- different systems. Multiple backends can be combined with
+-- 'Observe.Event.pairEventBackend'.
+--
+-- A simple 'EventBackend' for logging to a t'System.IO.Handle' can be
+-- created with 'Observe.Event.Render.IO.JSON.jsonHandleBackend'.
+--
+-- Typically the entrypoint for some eventuo11y-instrumented code will
+-- take an 'EventBackend', polymorphic in @r@ and possibly @m@. Calling
+-- code can use 'Observe.Event.subEventBackend' to place the resulting
+-- events in its hierarchy.
+--
+-- From an 'EventBackend', new events can be created via selectors
+-- (of type @s f@ for some field type @f@), typically with the
+-- [resource-safe allocation functions](Observe-Event.html#g:resourcesafe).
+-- Selectors are values which designate the general category of event
+-- being created, as well as the type of fields that can be added to it.
+-- For example, a web service's selector type may have a @ServicingRequest@
+-- constructor, whose field type includes a @ResponseCode@ constructor which
+-- records the HTTP status code.
+--
+-- Selectors are intended to be of a domain specific type per unit of
+-- functionality within an instrumented codebase, implemented as a GADT
+-- (but see t'Observe.Event.Dynamic.DynamicEventSelector' for a generic option).
+--
+-- Implementations must ensure that 'EventBackend's and their underlying t'Observe.Event.Event's
+-- are safe to use across threads.
+--
+-- [@m@]: The monad we're instrumenting in.
+-- [@r@]: The type of event references used in this 'EventBackend'. See 'Observe.Event.reference'.
+-- [@s@]: The type of event selectors.
+data EventBackend m r s = EventBackend
+  { -- | Create a new 'EventImpl' corresponding to the given selector.
+    newEventImpl :: !(forall f. s f -> m (EventImpl m r f)),
+    -- | Allocate a new 'OnceFlag' in our monad.
+    newOnceFlag :: !(m (OnceFlag m))
+  }
+
+-- | The internal implementation of an t'Observe.Event.Event'.
+--
+-- All fields have corresponding [event manipulation functions](Observe-Event.html#g:eventmanip),
+-- except that 'finalizeImpl' and 'failImpl' can assume that they will only ever be called
+-- once (i.e., 'EventImpl' implementations do __not__ have to implement locking internally).
+data EventImpl m r f = EventImpl
+  { referenceImpl :: !r,
+    addFieldImpl :: !(f -> m ()),
+    addParentImpl :: !(r -> m ()),
+    addProximateImpl :: !(r -> m ()),
+    finalizeImpl :: !(m ()),
+    failImpl :: !(Maybe SomeException -> m ())
+  }
+
+-- | The state of a 'OnceFlag'
+data FlagState
+  = -- | The flag was not set, but is now
+    NewlySet
+  | -- | The flag was already set
+    AlreadySet
+
+-- | A flag to ensure only one operation from some class is performed, once.
+--
+-- Typically consumed via 'runOnce'
+newtype OnceFlag m = OnceFlag
+  { -- | Get the state of the 'OnceFlag', and set the flag.
+    --
+    -- This operation should be atomic, and ideally would only
+    -- return 'NewlySet' once. In monads that don't support it,
+    -- at a minimum it must be monotonic (once one caller gets
+    -- 'AlreadySet', all callers will).
+    checkAndSet :: m FlagState
+  }
+
+-- | Run an operation if no other operations using this
+-- 'OnceFlag' have run.
+runOnce :: (Monad m) => OnceFlag m -> m () -> m ()
+runOnce f go =
+  checkAndSet f >>= \case
+    NewlySet -> go
+    AlreadySet -> pure ()
+
+-- | A 'OnceFlag' in 'IO' using an 'MVar'.
+newOnceFlagIO :: IO (OnceFlag IO)
+newOnceFlagIO = do
+  flag <- newEmptyMVar
+  pure $
+    OnceFlag $
+      tryPutMVar flag () <&> \case
+        False -> AlreadySet
+        True -> NewlySet
+
+-- | A 'OnceFlag' which is always 'NewlySet'.
+--
+-- Only safe to use if the operations to be guarded
+-- by the flag are already idempotent.
+alwaysNewOnceFlag :: (Applicative m) => OnceFlag m
+alwaysNewOnceFlag = OnceFlag $ pure NewlySet
+
+-- | Hoist a 'OnceFlag' along a given natural transformation into a new monad.
+hoistOnceFlag ::
+  -- | Natural transformation from @f@ to @g@
+  (forall x. f x -> g x) ->
+  OnceFlag f ->
+  OnceFlag g
+hoistOnceFlag nt (OnceFlag cs) = OnceFlag (nt cs)
diff --git a/src/Observe/Event/Render/IO/JSON.hs b/src/Observe/Event/Render/IO/JSON.hs
new file mode 100644
--- /dev/null
+++ b/src/Observe/Event/Render/IO/JSON.hs
@@ -0,0 +1,140 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RankNTypes #-}
+
+-- |
+-- Description : EventBackend for rendering events as JSON to a handle
+-- Copyright   : Copyright 2022 Shea Levy.
+-- License     : Apache-2.0
+-- Maintainer  : shea@shealevy.com
+module Observe.Event.Render.IO.JSON
+  ( jsonHandleBackend,
+    simpleJsonStderrBackend,
+
+    -- * Internals
+    JSONRef (..),
+  )
+where
+
+import Control.Concurrent.MVar
+import Control.Exception
+import Data.Aeson
+import Data.Aeson.KeyMap (insert)
+import Data.ByteString.Lazy.Char8 (hPutStrLn)
+import Data.Coerce
+import Data.IORef
+import Data.Time.Clock
+import Data.UUID (UUID)
+import Data.UUID.V4
+import Observe.Event
+import Observe.Event.Implementation
+import Observe.Event.Render.JSON
+import System.IO (Handle, stderr)
+
+-- | The reference type for 'EventBackend's generated by 'jsonHandleBackend'.
+--
+-- Only expected to be used by type inference or by code implementing other backends
+-- using this one.
+newtype JSONRef = JSONRef UUID deriving newtype (ToJSON)
+
+-- | An 'EventBackend' which posts events to a given 'Handle' as JSON.
+--
+-- Each 'Event' is posted as a single line, as it's completed. As a result, child events
+-- will typically be posted __before__ their parents (though still possible to correlate via
+-- event IDs).
+--
+-- The 'EventBackend' must be the exclusive writer to the 'Handle' while any events are live,
+-- but it does not 'System.IO.hClose' it itself.
+jsonHandleBackend ::
+  -- The type of structured exceptions
+  (Exception stex) =>
+  -- | Where to write 'Event's.
+  Handle ->
+  -- | Render a structured exception to JSON
+  RenderExJSON stex ->
+  -- | Render a selector, and the fields of 'Event's selected by it, to JSON
+  RenderSelectorJSON s ->
+  IO (EventBackend IO JSONRef s)
+jsonHandleBackend h renderEx renderSel = do
+  outputLock <- newMVar ()
+  let emit :: Object -> IO ()
+      emit o = withMVar outputLock \() ->
+        hPutStrLn h $ encode o
+  pure $
+    EventBackend
+      { newEventImpl = \sel -> do
+          let (k, renderField) = renderSel sel
+          eventRef <- coerce nextRandom
+          start <- getCurrentTime
+          fieldsRef <- newIORef mempty
+          parentsRef <- newIORef mempty
+          proximatesRef <- newIORef mempty
+          let finish r = do
+                end <- getCurrentTime
+                fields <- readIORef fieldsRef
+                parents <- readIORef parentsRef
+                proximates <- readIORef proximatesRef
+                emit
+                  ( k
+                      .= Object
+                        ( "event-id" .= eventRef
+                            <> "start" .= start
+                            <> "end" .= end
+                            <> ifNotNull "fields" fields
+                            <> ifNotNull "parents" parents
+                            <> ifNotNull "proximate-causes" proximates
+                            <> case r of
+                              Abort -> "abort" .= True
+                              StructuredFail e -> "structured-exception" .= renderEx e
+                              UnstructuredFail e -> "unstructured-exception" .= show e
+                              Finalized -> mempty
+                        )
+                  )
+          pure $
+            EventImpl
+              { referenceImpl = eventRef,
+                addFieldImpl = \field ->
+                  atomicModifyIORef' fieldsRef \fields ->
+                    (uncurry insert (renderField field) fields, ()),
+                addParentImpl = \r ->
+                  atomicModifyIORef' parentsRef \refs -> (r : refs, ()),
+                addProximateImpl = \r ->
+                  atomicModifyIORef' proximatesRef \refs -> (r : refs, ()),
+                finalizeImpl = finish Finalized,
+                failImpl = \me ->
+                  finish
+                    ( case me of
+                        Nothing -> Abort
+                        Just e -> case fromException e of
+                          Just se -> StructuredFail se
+                          Nothing -> UnstructuredFail e
+                    )
+              },
+        newOnceFlag = newOnceFlagIO
+      }
+
+-- | An 'EventBackend' which posts events to @stderr@ as JSON.
+--
+-- Each 'Event' is posted as a single line, as it's completed. As a result, child events
+-- will typically be posted __before__ their parents (though still possible to correlate via
+-- event IDs).
+--
+-- Any instrumented 'Exception's descended from 'SomeJSONException' will be structurally rendered.
+--
+-- The 'EventBackend' must be the exclusive writer to @stderr@ while any events are live,
+-- but it does not 'System.IO.hClose' it itself.
+simpleJsonStderrBackend :: RenderSelectorJSON s -> IO (EventBackend IO JSONRef s)
+simpleJsonStderrBackend = jsonHandleBackend stderr renderJSONException
+
+-- | Why did an 'Event' finish?
+data FinishReason stex
+  = Abort
+  | StructuredFail stex
+  | UnstructuredFail SomeException
+  | Finalized
+
+-- | Add k .= v if v is not 'null'.
+ifNotNull :: (Foldable f, ToJSON (f v)) => Key -> f v -> Object
+ifNotNull k v = if null v then mempty else k .= v
diff --git a/src/Observe/Event/Render/JSON.hs b/src/Observe/Event/Render/JSON.hs
new file mode 100644
--- /dev/null
+++ b/src/Observe/Event/Render/JSON.hs
@@ -0,0 +1,90 @@
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE RankNTypes #-}
+
+-- |
+-- Description : Renderers for serializing Events as JSON
+-- Copyright   : Copyright 2022 Shea Levy.
+-- License     : Apache-2.0
+-- Maintainer  : shea@shealevy.com
+--
+-- Instrumentors will need to provide instances of 'RenderSelectorJSON'
+-- and 'RenderFieldJSON' for their domain-specific types to use their
+--  t'Observe.Event.Event's with JSON-consuming t'Observe.Event.EventBackend's.
+module Observe.Event.Render.JSON
+  ( RenderSelectorJSON,
+    RenderFieldJSON,
+
+    -- * Rendering structured exceptions
+    RenderExJSON,
+
+    -- ** SomeJSONException
+    renderJSONException,
+    SomeJSONException (..),
+    jsonExceptionToException,
+    jsonExceptionFromException,
+
+    -- * Observe.Event.Dynamic support
+    renderDynamicEventSelectorJSON,
+    renderDynamicFieldJSON,
+  )
+where
+
+import Control.Exception
+import Data.Aeson
+import Data.Aeson.Key
+import Data.Typeable
+import Observe.Event.Dynamic
+
+-- | A function to render a given selector, its fields, as JSON.
+--
+-- The 'Key' is the event name/category.
+type RenderSelectorJSON sel = forall f. sel f -> (Key, RenderFieldJSON f)
+
+-- | A function to render a given @field@ as JSON.
+--
+-- The 'Key' is a field name, the 'Value' is an arbitrary
+-- rendering of the field value (if any).
+type RenderFieldJSON field = field -> (Key, Value)
+
+-- | A function to render a given structured exception to JSON.
+type RenderExJSON stex = stex -> Value
+
+-- | Render a 'DynamicEventSelector' and all its sub-fields.
+renderDynamicEventSelectorJSON :: RenderSelectorJSON DynamicEventSelector
+renderDynamicEventSelectorJSON (DynamicEventSelector n) =
+  (fromText n, renderDynamicFieldJSON)
+
+-- | Render a 'DynamicField'
+renderDynamicFieldJSON :: RenderFieldJSON DynamicField
+renderDynamicFieldJSON f = (fromText (name f), value f)
+
+-- | Render a 'SomeJSONException' to JSON.
+--
+-- It is __not__ necessary to use 'SomeJSONException' for the base of your
+-- structured exceptions in a JSON backend, so long as you provide a
+-- 'RenderExJSON' for your base exception type.
+renderJSONException :: RenderExJSON SomeJSONException
+renderJSONException (SomeJSONException e) = toJSON e
+
+-- | A possible base type for structured exceptions renderable to JSON.
+--
+-- It is __not__ necessary to use 'SomeJSONException' for the base of your
+-- structured exceptions in a JSON backend, so long as you provide a
+-- 'RenderExJSON' for your base exception type.
+data SomeJSONException = forall e. (Exception e, ToJSON e) => SomeJSONException e
+
+instance Show SomeJSONException where
+  show (SomeJSONException e) = show e
+  showsPrec i (SomeJSONException e) = showsPrec i e
+
+instance Exception SomeJSONException
+
+-- | Used to create sub-classes of 'SomeJSONException'.
+jsonExceptionToException :: (Exception e, ToJSON e) => e -> SomeException
+jsonExceptionToException = toException . SomeJSONException
+
+-- | Used to create sub-classes of 'SomeJSONException'.
+jsonExceptionFromException :: (Exception e) => SomeException -> Maybe e
+jsonExceptionFromException x = do
+  SomeJSONException a <- fromException x
+  cast a
