kim / opentracing

OpenTracing (https://opentracing.io) for Haskell
Apache License 2.0
40 stars 12 forks source link

Can't create a Tracer type #12

Closed t3hmrman closed 6 years ago

t3hmrman commented 6 years ago

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 6 years ago

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.

t3hmrman commented 6 years ago

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

t3hmrman commented 6 years ago

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 6 years ago

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.

t3hmrman commented 6 years ago

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!

t3hmrman commented 6 years ago

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: