kim/opentracing

Can't create a Tracer type

t3hmrman opened this issue · 6 comments

I'm trying to get the WAI integration working, and I'm almost there but I can't create the last bit I need, a Tracer value.

I have the functions I want the tracer to run, but due to the type variable on tracer's definition (I think), haskell gets confused about which type to use. Even when I specialize to IO (instead of using a forall m. MonadIO m), I get this type error:

src/Tracing/TracingBackend.hs:91:118-124: error: …                                                                                                                                                                                                                                
    • Couldn't match type ‘m’ with ‘IO’                                                                                                                                                                                                                                           
      ‘m’ is a rigid type variable bound by                                                                                                                                                                                                                                       
        a type expected by the context:                                                                                                                                                                                                                                           
          forall (m :: * -> *). MonadIO m => SpanOpts -> m Span                                                                                                                                                                                                                   
        at /path/to/src/Tracing/TracingBackend.hs:91:97                                                                                                                                                                              
      Expected type: SpanOpts -> m Span                                                                                                                                                                                                                                           
        Actual type: SpanOpts -> IO Span                                                                                                                                                                                                                                          
    • In the ‘tracerStart’ field of a record                                                                                                                                                                                                                                      
      In the first argument of ‘return’, namely                                                                                                                                                                                                                                   
        ‘(tracer                                                                                                                                                                                                                                                                  
            {tracerStart = startFn, tracerReport = jaegerAgentReporter j})’                                                                                                                                                                                                       
      In the expression:                                                                                                                                                                                                                                                          
        return                                                                                                                                                                                                                                                                    
          (tracer                                                                                                                                                                                                                                                                 
             {tracerStart = startFn, tracerReport = jaegerAgentReporter j}) 

Here's what the code looks like:

class Tracing T
    -- ... other code ... 
    traceApplication :: t -> Application -> IO Application

instance Tracing TracingBackend where
    -- ... other code ... 
    -- TODO: check to ensure that app wide tracing is enabled                                                                                                                                                                                                                     
    traceApplication t app = if appWideTracingEnabled then instrumentApp app else pure app
        where
          jaegerOptions = tracingSettings t
          appWideTracingEnabled = tracingAppWide $ tracingCfg t
          sampling = probSampler $ tracingSamplingRate $ tracingCfg t
          propagation = jaegerPropagation

          defaultReporter :: FinishedSpan -> IO ()
          defaultReporter = makeLoggerReporter t

          -- Create Standard ENV for Tracer to use, create standard tracer from the env                                                                                                                                                                                           
          mkTracer :: IO (SpanOpts -> IO Span)
          mkTracer = newStdEnv sampling >>= pure . stdTracer

          -- Update Tracer with a JaegerAgentReporter (instead of log based one), produce the middleware                                                                                                                                                                          
          augmentTracerWithJaeger :: Tracer -> (SpanOpts -> IO Span) -> IO Tracer
          augmentTracerWithJaeger tracer startFn = withJaegerAgent jaegerOptions (\j -> return (tracer { tracerStart=startFn
                                                                                                       , tracerReport=jaegerAgentReporter j
                                                                                                       }))

          -- Use the created tracer to make the middleware                                                                                                                                                                                                                        
          makeMiddleware :: Application -> Tracer -> IO Application
          makeMiddleware app tracer = pure (opentracing tracer propagation (\activeSpan -> app))

          -- Instrument an actual application with the agent reporter                                                                                                                                                                                                             
          instrumentApp :: Application -> IO Application
          instrumentApp app = liftIO (logMsg t INFO "Instrumenting application...")
                              >> mkTracer
                              >>= augmentTracerWithJaeger (Tracer undefined undefined)
                              >>= makeMiddleware app
                              >>= pure

To me it seems like haskell is expecting the IO to be an m (as bound by tracer's definition), and cannot get over the requirement.

I've tried setting ScopedTypeVariables (and abstracting the MonadIO m type in the typeclass code too), but it still won't convince Haskell that both ms are the same, I just get the same error with m1 vs m instead of m and IO.

I've looked at Backends.hs and I'm honestly not sure how it works when you're in the IO monad yet manage to create a Tracer properly.

kim commented

Not sure why you need a typeclass here, this seems to complicate things unnecessarily.

The problem at hand is that your startFn has type SpanOpts -> IO Span, but Tracer expects MonadIO m => SpanOpts -> m Span. liftIO startFn should make the types align.

Hey @kim thanks for the speedy response. The typeclass has to do with how my app is structured -- I use it to separate the functionality of large-ish components. This typeclass is specific but I think it's helpful when others aren't so specific (and only have one implementation)

I tried to lift the function earlier and it didn't work but I will try again, IIRC the m is wasn't being deduced as the same m. I'll give it another shot

Maybe I'm still doing it wrong, here's the code changed:

-- Create Standard ENV for Tracer to use, create standard tracer from the env                                                                                                                                                                                           mkTracer :: MonadIO m => IO (SpanOpts -> m Span)
mkTracer = newStdEnv sampling
           >>= \env -> pure (\spanOpts -> liftIO ((stdTracer env) spanOpts))

-- Update Tracer with a JaegerAgentReporter (instead of log based one), produce the middleware                                                                                                                                                                          
augmentTracerWithJaeger :: MonadIO m => Tracer -> (SpanOpts -> m Span) -> IO Tracer
augmentTracerWithJaeger tracer startFn = withJaegerAgent jaegerOptions (\j -> return (tracer { tracerStart=startFn
                                                                                            , tracerReport=jaegerAgentReporter j
                                                                                             }))

It's a little messy, but I'm super careful to reassemble into a type that matches, but it's unclear (to the compiler) that that m is the same as the m in Tracer. I enabled ScopedTypeVariables, no difference -- Is it possible this is broken for me since I'm inside a typeclass function?

Here's the error that still emerges:

src/Tracing/TracingBackend.hs:92:118-124: error: …
    • Couldn't match type ‘m’ with ‘m1’
      ‘m’ is a rigid type variable bound by
        the type signature for:
          augmentTracerWithJaeger :: forall (m :: * -> *).
                                     MonadIO m =>
                                     Tracer -> (SpanOpts -> m Span) -> IO Tracer
        at /path/to/src/Tracing/TracingBackend.hs:91:38
      ‘m1’ is a rigid type variable bound by
        a type expected by the context:
          forall (m1 :: * -> *). MonadIO m1 => SpanOpts -> m1 Span
        at /path/to/src/Tracing/TracingBackend.hs:92:97
      Expected type: SpanOpts -> m1 Span
        Actual type: SpanOpts -> m Span
    • In the ‘tracerStart’ field of a record
      In the first argument of ‘return’, namely
        ‘(tracer
            {tracerStart = startFn, tracerReport = jaegerAgentReporter j})’
      In the expression:
        return
          (tracer
             {tracerStart = startFn, tracerReport = jaegerAgentReporter j})
    • Relevant bindings include
        startFn :: SpanOpts -> m Span
          (bound at /path/to/src/Tracing/TracingBackend.hs:92:42)
        augmentTracerWithJaeger :: Tracer
                                   -> (SpanOpts -> m Span) -> IO Tracer
          (bound at /path/to/src/Tracing/TracingBackend.hs:92:11)
src/Tracing/TracingBackend.hs:103:34-41: error: …
    • Ambiguous type variable ‘m0’ arising from a use of ‘mkTracer’
      prevents the constraint ‘(MonadIO m0)’ from being solved.
      Probable fix: use a type annotation to specify what ‘m0’ should be.
      These potential instances exist:
        instance [safe] MonadIO IO -- Defined in ‘Control.Monad.IO.Class’
        ...plus 18 instances involving out-of-scope types
        (use -fprint-potential-instances to see them all)
    • In the second argument of ‘(>>)’, namely ‘mkTracer’
      In the first argument of ‘(>>=)’, namely
        ‘liftIO (logMsg t INFO "Instrumenting application...") >> mkTracer’
      In the first argument of ‘(>>=)’, namely
        ‘liftIO (logMsg t INFO "Instrumenting application...") >> mkTracer
         >>= augmentTracerWithJaeger (Tracer undefined undefined)’
kim commented

Hey @t3hmrman, it's a bit hard to follow because I can't take your code and have a machine typecheck it for me 😬

That said, I guess the signature of augmentTracerWithJaeger should stay the same as before (with concrete IO, the MonadIO constraint is what makes the m "rigid"), and then use liftIO in the function body. Apart from that, it doesn't seem like you need to set the tracerStart field - it can only meaningfully be the stdTracer currently. Just make a tracer like here and pass that to augmentTracerWithJaeger, where you only update the tracerReport field.

Hope this helps.

hey @kim thanks I think that does help -- I'll keep trying at it. The code you linked too should be pretty helpful. I'm basically trying my hardest not to have to set tracerStart.

Thanks for your help!

After some more fiddling I got it to type check!

    traceApplication t app = if appWideTracingEnabled then instrumentApp app else pure app
        where
          jOpts = tracingSettings t
          appWideTracingEnabled = tracingAppWide $ tracingCfg t
          sampling = probSampler $ tracingSamplingRate $ tracingCfg t
          propagation = jaegerPropagation

          defaultReporter :: FinishedSpan -> IO ()
          defaultReporter = makeLoggerReporter t

          -- Create Standard ENV for Tracer to use, create standard tracer from the env                                                                                                                                                                                           
          mkTracer :: MonadIO m => m Tracer
          mkTracer = newStdEnv sampling
                     >>= \env -> liftIO (pure (Tracer (stdTracer env) undefined))

          -- Update Tracer with a JaegerAgentReporter (instead of log based one), produce the middleware                                                                                                                                                                          
          augmentTracerWithJaeger :: Tracer -> IO Tracer
          augmentTracerWithJaeger tracer = withJaegerAgent jOpts (\j -> return (tracer { tracerReport=jaegerAgentReporter j }))

          -- Use the created tracer to make the middleware                                                                                                                                                                                                                        
          makeMiddleware :: Application -> Tracer -> IO Application
          makeMiddleware app tracer = pure (opentracing tracer propagation (\activeSpan -> app))

          -- Instrument an actual application with the agent reporter                                                                                                                                                                                                             
          instrumentApp :: Application -> IO Application
          instrumentApp app = liftIO (logMsg t INFO "Instrumenting application...")
                              >> mkTracer
                              >>= augmentTracerWithJaeger
                              >>= makeMiddleware app
                              >>= pure

Hopefully it will help anyone else that finds this issue. I think I did two key thigs that might have helped:

  • Removing ScopedTypeVariables (I think it was binding the m to my local function, ignoring the constraint in Tracer.hs)
  • Completely imported OpenTracing.Standard (before I was only selectively importing bits of it)