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-otel
+
+## 0.1.0.0 -- 2023-01-13
+
+Initial release
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright 2023 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-otel.cabal b/eventuo11y-otel.cabal
new file mode 100644
--- /dev/null
+++ b/eventuo11y-otel.cabal
@@ -0,0 +1,34 @@
+cabal-version:      3.0
+name:               eventuo11y-otel
+version:            0.1.0.0
+synopsis:           OpenTelemetry-based rendering for eventuo11y
+description:
+  Render [eventuo11y](https://hackage.haskell.org/package/eventuo11y) 'Observe.Event.Event's
+  to OpenTelemetry traces.
+
+bug-reports:        https://github.com/shlevy/eventuo11y/issues
+license:            Apache-2.0
+license-file:       LICENSE
+author:             Shea Levy
+maintainer:         shea@shealevy.com
+copyright:          Copyright 2023 Shea Levy.
+category:           Observability
+extra-source-files: CHANGELOG.md
+tested-with:        GHC == { 8.10.7, 9.2.4 }
+
+source-repository head
+  type:     git
+  location: https://github.com/shlevy/eventuo11y
+
+library
+  exposed-modules:
+    Observe.Event.Render.OpenTelemetry
+
+  build-depends:
+    , base                 ^>= { 4.14, 4.16, 4.17 }
+    , eventuo11y           ^>= { 0.7 }
+    , hs-opentelemetry-api ^>= { 0.0.3 }
+    , text                 ^>= { 1.2, 2.0 }
+
+  hs-source-dirs:   src
+  default-language: Haskell2010
diff --git a/src/Observe/Event/Render/OpenTelemetry.hs b/src/Observe/Event/Render/OpenTelemetry.hs
new file mode 100644
--- /dev/null
+++ b/src/Observe/Event/Render/OpenTelemetry.hs
@@ -0,0 +1,108 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE RecordWildCards #-}
+
+-- |
+-- Description : EventBackend for rendering events as OpenTelemetry traces
+-- Copyright   : Copyright 2023 Shea Levy.
+-- License     : Apache-2.0
+-- Maintainer  : shea@shealevy.com
+module Observe.Event.Render.OpenTelemetry where
+
+import Control.Monad.IO.Class
+import Data.Text (Text, pack)
+import Observe.Event.Backend
+import OpenTelemetry.Context
+import OpenTelemetry.Context.ThreadLocal
+import OpenTelemetry.Trace.Core hiding (Event)
+import OpenTelemetry.Trace.Id
+
+-- | An 'EventBackend' built on a 'Tracer'.
+--
+-- When no explicit parent is set, the backend will try to find a parent in the "OpenTelemetry.Context.ThreadLocal" 'Context'.
+-- However, it will never update that 'Context', as the primitive 'EventBackend' API has no way to determine if it's being
+-- consumed in a scoped context or one allowing for general interleaving.
+--
+-- When possible, events created with 'emitImmediateEvent' will use the span event API. However, this requires a parent event
+-- (explicitly specified or found in the thread-local 'Context'), so the backend will fallback to creating and 'finalize'ing a new
+-- 'Span'. If a span event is created, the resulting 'reference' will be to its parent, as span events cannot be parents/links. Span
+-- events do not allow for non-parent links, so any `newEventCauses` are dropped; in the future, we may either add them as custom
+-- 'Attribute's or fall back to a full span if any are specified.
+--
+-- Event 'Link's are currently not given any attributes. In the future, arbitrary link metadata could be added to the core 'EventBackend'
+-- API, in which case we could add a renderer for the link metadata type.
+--
+-- Currently the backend lets the underlying 'Tracer' set all timestamps. In the future, 'RenderSelectorOTel' could be allowed to run in
+-- @m@ and have a timestamp field.
+--
+-- Exceptions passed to 'finalize' are 'recordException'ed without any custom attributes. In the future, an @Exception -> [Text, Attribute]@
+-- argument could be added, or arbitrary exception metadata added to 'finalize'.
+tracerEventBackend :: (MonadIO m) => Tracer -> RenderSelectorOTel s -> EventBackend m Span s
+tracerEventBackend tracer render = backend
+  where
+    backend =
+      EventBackend
+        { newEvent = \args@(NewEventArgs {..}) -> do
+            ctx <- maybe empty id <$> lookupContext
+            let ctx' = case newEventParent of
+                  Just s -> insertSpan s ctx
+                  Nothing -> ctx
+                OTelRendered {..} = render newEventSelector
+            links <- traverse (fmap (flip NewLink []) . getSpanContext) newEventCauses
+            s <-
+              createSpanWithoutCallStack tracer ctx' eventName $
+                SpanArguments
+                  { kind = eventKind,
+                    attributes = concatMap renderField newEventInitialFields,
+                    links = links,
+                    startTime = Nothing
+                  }
+            pure $
+              Event
+                { reference = s,
+                  addField = addAttributes s . renderField,
+                  finalize = \me -> do
+                    let recordError e = do
+                          recordException s [("exception.escaped", toAttribute True)] Nothing e
+                          setStatus s . Error . pack $ show e
+                    maybe (setStatus s Ok) recordError me
+                    endSpan s Nothing
+                },
+          emitImmediateEvent = \args@(NewEventArgs {..}) -> case newEventParent of
+            Nothing -> do
+              m_ctx <- lookupContext
+              case m_ctx >>= lookupSpan of
+                Just s ->
+                  emitImmediateEvent backend $
+                    args
+                      { newEventParent = Just s
+                      }
+                Nothing -> do
+                  ev <- newEvent backend args
+                  finalize ev Nothing
+                  pure $ reference ev
+            Just s -> do
+              let OTelRendered {..} = render newEventSelector
+              addEvent s $
+                NewEvent
+                  { newEventName = eventName,
+                    newEventAttributes = concatMap renderField newEventInitialFields,
+                    newEventTimestamp = Nothing
+                  }
+              pure s
+        }
+
+-- | Render a given selector (and all of its fields) to OpenTelemetry
+type RenderSelectorOTel s = forall f. s f -> OTelRendered f
+
+-- | The result of rendering a specific selector with field type @f@
+data OTelRendered f = OTelRendered
+  { -- | The name of the event. See section on "span name" at <https://opentelemetry.io/docs/reference/specification/trace/api/#span>
+    eventName :: !Text,
+    -- | See the specification on [SpanKind](https://opentelemetry.io/docs/reference/specification/trace/api/#spankind)
+    eventKind :: !SpanKind,
+    -- | Render a field to a set of span [attributes](https://opentelemetry.io/docs/reference/specification/common/#attribute).
+    --
+    -- Note especially the [attribute naming guidelines](https://opentelemetry.io/docs/reference/specification/common/attribute-naming/)
+    renderField :: !(f -> [(Text, Attribute)])
+  }
