{-|
Module: OpenTracing.Tracer

This module provides mid and high level tracing functions.
-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes     #-}
{-# LANGUAGE StrictData     #-}

module OpenTracing.Tracer
    ( Tracer(..)
    , HasTracer(..)
    , runTracer

    , traced
    , traced_
    , startSpan
    , finishSpan
    )
where

import Control.Exception.Safe
import Control.Lens
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Reader
import Data.List.NonEmpty     (NonEmpty (..))
import Data.Time.Clock        (getCurrentTime)
import OpenTracing.Log
import OpenTracing.Span
import OpenTracing.Tags
import Prelude                hiding (span)

-- | A `Tracer` is a set of effectful actions that define the mid-level interface
-- to an [OpenTracing tracer](https://github.com/opentracing/specification/blob/master/specification.md#tracer)
--
-- Appliction code should generally construct a `Tracer` once and then use other
-- higher-level functions such as `traced`, `startSpan`, `finishedSpan`.
--
-- @since 0.1.0.0
data Tracer = Tracer
    { tracerStart  :: forall m. MonadIO m => SpanOpts     -> m Span
      -- ^ Start recording a new span with the given options. This is
      -- a mid-level operation that will handle start timing and random span ID
      -- generation.
      --
      -- Application code should supply this field with `stdTracer`.
    , tracerReport :: forall m. MonadIO m => FinishedSpan -> m ()
    -- ^ Report a finished span. What reporting means for each application will
    -- depend on where this data is going. There are multiple backends that define
    -- reporters for Google Cloudtrace, Zipkin, and Jaeger, for example.
    }

-- | Typeclass for application environments that contain a `Tracer`.
--
-- @since 0.1.0.0
class HasTracer a where
    tracer :: Getting r a Tracer

instance HasTracer Tracer where
    tracer = id

runTracer :: HasTracer r => r -> ReaderT r m a -> m a
runTracer = flip runReaderT

-- | Trace a computation as a span. This is a high-level operation that will handle
-- all aspects of the trace, including timing and reporting. If the traced computation
-- throws an excpetion, `traced` will clean up and add logs before rethrowing the
-- exception
--
-- @
--         traced tracer (spanOpts "hello" mempty          ) $ \parent ->
--         traced tracer (spanOpts "world" (childOf parent)) $ \child ->
--            liftIO $ do
--                putStrLn "doing some work..."
--                addLogRecord child (Message "doing some work")
--                threadDelay 500000
-- @
--
-- @since 0.1.0.0
traced
    :: ( HasTracer t
       , MonadMask m
       , MonadIO   m
       )
    => t -- ^ A tracer environment
    -> SpanOpts -- ^ The options to use when creating the span. Options include:
    --
    --   * Operation name
    --
    --   * Tags
    --
    --   * Relations to other spans
    -> (ActiveSpan -> m a) -- ^ the computation to trace. The argument is the
    -- span that is created. It can be used to:
    --
    --   * Add logs
    --
    --   * Add child spans
    -> m (Traced a)
traced t opt f = do
    span <- startSpan t opt
    -- /Note/: as per 'withException', we will be reporting any exception incl.
    -- async ones. Exceptions thrown by 'finishSpan'' will be ignored, and the
    -- one from 'f' will be rethrown. Observe that 'withException' does _not_
    -- run the error handler under `uninterruptibleMask', unlike 'bracket'. This
    -- is a good thing, as we might be doing blocking I/O.
    ret  <- withException (f span) (onErr span >=> void . finishSpan t)
    fin  <- finishSpan t span
    return Traced { tracedResult = ret, tracedSpan = fin }
  where
    onErr :: MonadIO m => ActiveSpan -> SomeException -> m ActiveSpan
    onErr span e = liftIO $ do
        now <- getCurrentTime
        modifyActiveSpan span $
              over spanTags (setTag (Error True))
            . over spanLogs (LogRecord now (ErrObj e :| []) :)
        pure span

-- | Variant of `traced` that doesn't return the wrapped value.
--
-- @since 0.1.0.0
traced_
    :: ( HasTracer t
       , MonadMask m
       , MonadIO   m
       )
    => t
    -> SpanOpts
    -> (ActiveSpan -> m a)
    -> m a
traced_ t opt f = tracedResult <$> traced t opt f

-- | Start recording a span
--
-- @since 0.1.0.0
startSpan :: (HasTracer t, MonadIO m) => t -> SpanOpts -> m ActiveSpan
startSpan t opt = do
    let Tracer{tracerStart} = view tracer t
    tracerStart opt >>= liftIO . mkActive

-- | Finish recording a span
--
-- @since 0.1.0.0
finishSpan :: (HasTracer t, MonadIO m) => t -> ActiveSpan -> m FinishedSpan
finishSpan t a = do
    let Tracer{tracerReport} = view tracer t
    span <- liftIO (readActiveSpan a) >>= spanFinish
    case view sampled span of
        Sampled    -> tracerReport span
        NotSampled -> return () -- TODO: record metric
    return span