packages feed

hs-opentelemetry-api-1.0.0.0: src/OpenTelemetry/Context/ThreadLocal/Propagation.hs

{- |
Module      :  OpenTelemetry.Context.ThreadLocal.Propagation
Copyright   :  (c) Ian Duncan, 2021
License     :  BSD-3
Description :  Primitives for carrying OpenTelemetry context across thread boundaries.
Maintainer  :  Ian Duncan
Stability   :  experimental
Portability :  non-portable (GHC extensions)

Haskell's green threads do not inherit thread-local state. Any time you
fork a thread, directly with 'Control.Concurrent.forkIO', or indirectly
through @async@, @mapConcurrently@, etc., the child starts with an
empty 'Context'. This silently breaks trace propagation and baggage
flow.

This module provides drop-in replacements for common concurrency
primitives that capture the caller's 'Context' and install it in the
child thread before running the user action. The pattern mirrors
Go's explicit @ctx@ parameter and Java\/Python's automatic context
inheritance for child tasks.

== Quick start

@
import OpenTelemetry.Context.ThreadLocal.Propagation

-- Instead of @async work@:
a <- tracedAsync work

-- Instead of @forkIO work@:
tid <- tracedForkIO work

-- Instead of @mapConcurrently f xs@:
results <- tracedMapConcurrently f xs
@

If you need to integrate with a concurrency primitive not covered here,
use 'propagateContext' to wrap any @IO@ action so it inherits the
current thread's context:

@
myCustomFork action = customFork (propagateContext action)
@

@since 0.4.0.0
-}
module OpenTelemetry.Context.ThreadLocal.Propagation (
  -- * Core combinator
  propagateContext,

  -- * Common concurrency wrappers
  tracedForkIO,
  tracedAsync,
  tracedWithAsync,
  tracedConcurrently,
  tracedMapConcurrently,
  tracedForConcurrently,
) where

import Control.Concurrent (ThreadId, forkIO)
import Control.Concurrent.Async (Async, async, concurrently, forConcurrently, mapConcurrently, withAsync)
import OpenTelemetry.Context (Context)
import OpenTelemetry.Context.ThreadLocal (attachContext, getContext)


{- | Capture the current thread's 'Context' and return an action that,
when run in /any/ thread, installs that context before executing the
wrapped computation.

This is the fundamental building block. The other combinators in this
module are thin wrappers around it.

@since 0.4.0.0
-}
propagateContext :: IO a -> IO (IO a)
propagateContext action = do
  ctx <- getContext
  pure (installContext ctx >> action)
{-# INLINE propagateContext #-}


{- | 'forkIO' with automatic context propagation.

@since 0.4.0.0
-}
tracedForkIO :: IO () -> IO ThreadId
tracedForkIO action = do
  wrapped <- propagateContext action
  forkIO wrapped
{-# INLINE tracedForkIO #-}


{- | 'async' with automatic context propagation.

@since 0.4.0.0
-}
tracedAsync :: IO a -> IO (Async a)
tracedAsync action = do
  wrapped <- propagateContext action
  async wrapped
{-# INLINE tracedAsync #-}


{- | 'withAsync' with automatic context propagation.

@since 0.4.0.0
-}
tracedWithAsync :: IO a -> (Async a -> IO b) -> IO b
tracedWithAsync action k = do
  wrapped <- propagateContext action
  withAsync wrapped k
{-# INLINE tracedWithAsync #-}


{- | 'concurrently' with automatic context propagation for both branches.

@since 0.4.0.0
-}
tracedConcurrently :: IO a -> IO b -> IO (a, b)
tracedConcurrently left right = do
  l <- propagateContext left
  r <- propagateContext right
  concurrently l r
{-# INLINE tracedConcurrently #-}


{- | 'mapConcurrently' with automatic context propagation.

Each concurrent worker inherits the caller's context.

@since 0.4.0.0
-}
tracedMapConcurrently :: (Traversable t) => (a -> IO b) -> t a -> IO (t b)
tracedMapConcurrently f ta = do
  ctx <- getContext
  mapConcurrently (\a -> installContext ctx >> f a) ta
{-# INLINE tracedMapConcurrently #-}


{- | 'forConcurrently' with automatic context propagation.

@since 0.4.0.0
-}
tracedForConcurrently :: (Traversable t) => t a -> (a -> IO b) -> IO (t b)
tracedForConcurrently ta f = do
  ctx <- getContext
  forConcurrently ta (\a -> installContext ctx >> f a)
{-# INLINE tracedForConcurrently #-}


installContext :: Context -> IO ()
installContext ctx = do
  _ <- attachContext ctx
  pure ()
{-# INLINE installContext #-}