diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,26 @@
-## [_Unreleased_](https://github.com/freckle/freckle-app/compare/v1.15.3.0...main)
+## [_Unreleased_](https://github.com/freckle/freckle-app/compare/v1.15.4.0...main)
+
+## [v1.15.4.0](https://github.com/freckle/freckle-app/compare/v1.15.3.0...v1.15.4.0)
+
+- Add `addThreadContextFromTracing` and deprecate existing middleware
+
+  Migrating away from the deprecated middleware would look like:
+
+  ```diff
+  - import Network.Wai.Middleware.OpenTelemetry (newOpenTelemetryWaiMiddleware)
+  + import Freckle.App.Wai (addThreadContextFromRequest)
+  + import OpenTelemetry.Instrumentation.Wai (newOpenTelemetryWaiMiddleware)
+
+    traceMiddleware <- newOpenTelemetryWaiMiddleware
+
+    run 3000
+      . traceMiddleware
+  +   . addThreadContextFromRequest
+      . ...
+      $ app
+  ```
+
+- Add `withTraceContext` and deprecate existing datadog-specific functions
 
 ## [v1.15.3.0](https://github.com/freckle/freckle-app/compare/v1.15.2.0...v1.15.3.0)
 
diff --git a/freckle-app.cabal b/freckle-app.cabal
--- a/freckle-app.cabal
+++ b/freckle-app.cabal
@@ -1,6 +1,6 @@
 cabal-version:      1.18
 name:               freckle-app
-version:            1.15.3.0
+version:            1.15.4.0
 license:            MIT
 license-file:       LICENSE
 maintainer:         Freckle Education
@@ -59,6 +59,7 @@
         Freckle.App.Memcached.MD5
         Freckle.App.Memcached.Servers
         Freckle.App.OpenTelemetry
+        Freckle.App.OpenTelemetry.ThreadContext
         Freckle.App.Prelude
         Freckle.App.Random
         Freckle.App.Scientist
diff --git a/library/Freckle/App/Kafka/Consumer.hs b/library/Freckle/App/Kafka/Consumer.hs
--- a/library/Freckle/App/Kafka/Consumer.hs
+++ b/library/Freckle/App/Kafka/Consumer.hs
@@ -24,6 +24,7 @@
 import Freckle.App.Exception (annotatedExceptionMessageFrom)
 import Freckle.App.Kafka.Producer (envKafkaBrokerAddresses)
 import Freckle.App.OpenTelemetry
+import Freckle.App.OpenTelemetry.ThreadContext
 import Kafka.Consumer hiding
   ( Timeout
   , closeConsumer
@@ -174,7 +175,7 @@
   -> (a -> m ())
   -> m ()
 runConsumer pollTimeout onMessage =
-  withTraceIdContext $ immortalCreateLogged $ forever $ do
+  withTraceContext $ immortalCreateLogged $ forever $ do
     consumer <- view kafkaConsumerL
 
     flip catches handlers $ inSpan "kafka.consumer" consumerSpanArguments $ do
diff --git a/library/Freckle/App/OpenTelemetry.hs b/library/Freckle/App/OpenTelemetry.hs
--- a/library/Freckle/App/OpenTelemetry.hs
+++ b/library/Freckle/App/OpenTelemetry.hs
@@ -125,6 +125,7 @@
 getCurrentTraceIdAsDatadog :: MonadIO m => m (Maybe Word64)
 getCurrentTraceIdAsDatadog =
   fmap convertOpenTelemetryTraceIdToDatadogTraceId <$> getCurrentTraceId
+{-# DEPRECATED getCurrentTraceIdAsDatadog "Datadog can operate with Base16 ids" #-}
 
 getCurrentSpanContext :: MonadIO m => m (Maybe SpanContext)
 getCurrentSpanContext = withCurrentSpan getSpanContext
@@ -141,6 +142,7 @@
 withTraceIdContext f = do
   mTraceId <- getCurrentTraceIdAsDatadog
   maybe f (\traceId -> withThreadContext ["trace_id" .= traceId] f) mTraceId
+{-# DEPRECATED withTraceIdContext "Use Freckle.App.OpenTelemetry.ThreadContext" #-}
 
 -- | Convert a 'ByteString' to an 'Attribute' safely
 --
diff --git a/library/Freckle/App/OpenTelemetry/ThreadContext.hs b/library/Freckle/App/OpenTelemetry/ThreadContext.hs
new file mode 100644
--- /dev/null
+++ b/library/Freckle/App/OpenTelemetry/ThreadContext.hs
@@ -0,0 +1,53 @@
+{-# LANGUAGE NamedFieldPuns #-}
+
+module Freckle.App.OpenTelemetry.ThreadContext
+  ( withTraceContext
+  ) where
+
+import Freckle.App.Prelude
+
+import Blammo.Logging (MonadMask, withThreadContext)
+import Data.Aeson ((.=))
+import qualified Data.Aeson.Key as Key
+import Data.Aeson.Types (Pair)
+import Freckle.App.OpenTelemetry (getCurrentSpanContext)
+import OpenTelemetry.Trace.Core (SpanContext (..))
+import OpenTelemetry.Trace.Id
+  ( Base (..)
+  , spanIdBaseEncodedText
+  , traceIdBaseEncodedText
+  )
+import OpenTelemetry.Trace.TraceState (Key (..), TraceState (..), Value (..))
+
+-- | Add tracing context values to the logging context
+--
+-- Values are encoded to 'Base16' (i.e. hex).
+withTraceContext :: (MonadIO m, MonadMask m) => m a -> m a
+withTraceContext f = do
+  mSpanContext <- getCurrentSpanContext
+
+  case mSpanContext of
+    Nothing -> f
+    Just sc -> withThreadContext (toThreadContext sc) f
+
+toThreadContext :: SpanContext -> [Pair]
+toThreadContext SpanContext {traceId, spanId, traceState} =
+  [ "trace_id" .= traceIdBaseEncodedText Base16 traceId
+  , "span_id" .= spanIdBaseEncodedText Base16 spanId
+  , "trace_state" .= traceStatePairs
+  ]
+ where
+  traceStatePairs :: [Pair]
+  traceStatePairs = map traceStatePair $ unTraceState traceState
+
+  traceStatePair :: (Key, Value) -> Pair
+  traceStatePair = uncurry (.=) . bimap (Key.fromText . unKey) unValue
+
+unTraceState :: TraceState -> [(Key, Value)]
+unTraceState (TraceState x) = x
+
+unKey :: Key -> Text
+unKey (Key x) = x
+
+unValue :: Value -> Text
+unValue (Value x) = x
diff --git a/library/Freckle/App/Wai.hs b/library/Freckle/App/Wai.hs
--- a/library/Freckle/App/Wai.hs
+++ b/library/Freckle/App/Wai.hs
@@ -12,12 +12,16 @@
 
     -- * Tracing
   , newOpenTelemetryWaiMiddleware
+  , addThreadContextFromTracing
 
     -- * Metrics
   , addThreadContextFromStatsTags
   , requestStats
   ) where
 
+import Freckle.App.Prelude
+
+import Freckle.App.OpenTelemetry.ThreadContext
 import Network.Wai
 import Network.Wai.Middleware.AddHeaders
 import Network.Wai.Middleware.Cors
@@ -33,3 +37,7 @@
 -- | Middleware that adds header to deny all frame embedding
 denyFrameEmbeddingMiddleware :: Middleware
 denyFrameEmbeddingMiddleware = addHeaders [("X-Frame-Options", "DENY")]
+
+-- | Middleware that adds trace context to logging context
+addThreadContextFromTracing :: Middleware
+addThreadContextFromTracing app req = withTraceContext . app req
diff --git a/library/Network/Wai/Middleware/Cors.hs b/library/Network/Wai/Middleware/Cors.hs
--- a/library/Network/Wai/Middleware/Cors.hs
+++ b/library/Network/Wai/Middleware/Cors.hs
@@ -60,6 +60,7 @@
   , ("Access-Control-Allow-Credentials", "true")
   , ("Access-Control-Allow-Headers", "Content-Type, *")
   , ("Access-Control-Expose-Headers", BS.intercalate ", " exposedHeaders)
+  , ("Vary", "Origin")
   ]
  where
   validatedOrigin = if validateOrigin origin then origin else "BADORIGIN"
diff --git a/library/Network/Wai/Middleware/OpenTelemetry.hs b/library/Network/Wai/Middleware/OpenTelemetry.hs
--- a/library/Network/Wai/Middleware/OpenTelemetry.hs
+++ b/library/Network/Wai/Middleware/OpenTelemetry.hs
@@ -1,3 +1,5 @@
+{-# OPTIONS_GHC -Wno-deprecations #-}
+
 module Network.Wai.Middleware.OpenTelemetry
   ( newOpenTelemetryWaiMiddleware
   , openTelemetryMiddleware
@@ -15,6 +17,10 @@
 newOpenTelemetryWaiMiddleware = do
   otel <- Trace.newOpenTelemetryWaiMiddleware
   pure $ otel . openTelemetryMiddleware
+{-# DEPRECATED
+  newOpenTelemetryWaiMiddleware
+  "Use OpenTelemetry.Instrumentation.Wai.newOpenTelemetryWaiMiddleware"
+  #-}
 
 -- | Add 'TraceId' information to context and responses
 --
@@ -25,6 +31,7 @@
 openTelemetryMiddleware :: Middleware
 openTelemetryMiddleware app request respond =
   withTraceIdContext $ addTraceIdHeader app request respond
+{-# DEPRECATED openTelemetryMiddleware "Use addThreadContextFromTracing" #-}
 
 addTraceIdHeader :: Middleware
 addTraceIdHeader app request respond = do
diff --git a/package.yaml b/package.yaml
--- a/package.yaml
+++ b/package.yaml
@@ -1,5 +1,5 @@
 name: freckle-app
-version: 1.15.3.0
+version: 1.15.4.0
 maintainer: Freckle Education
 category: Utils
 github: freckle/freckle-app
