packages feed

freckle-app 1.15.4.0 → 1.15.5.0

raw patch · 6 files changed

+306/−6 lines, 6 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Freckle.App.OpenTelemetry: SpanArguments :: SpanKind -> HashMap Text Attribute -> [NewLink] -> Maybe Timestamp -> SpanArguments
+ Freckle.App.OpenTelemetry: [attributes] :: SpanArguments -> HashMap Text Attribute
+ Freckle.App.OpenTelemetry: [kind] :: SpanArguments -> SpanKind
+ Freckle.App.OpenTelemetry: [links] :: SpanArguments -> [NewLink]
+ Freckle.App.OpenTelemetry: [startTime] :: SpanArguments -> Maybe Timestamp
+ Freckle.App.OpenTelemetry: data () => SpanArguments
+ Freckle.App.OpenTelemetry: data () => SpanId
+ Freckle.App.OpenTelemetry: data () => TraceId
+ Freckle.App.OpenTelemetry: spanIdToHex :: SpanId -> Text
+ Freckle.App.OpenTelemetry: traceIdToHex :: TraceId -> Text
+ Freckle.App.OpenTelemetry.Context: CustomTraceContext :: [(Text, Text)] -> CustomTraceContext
+ Freckle.App.OpenTelemetry.Context: [traceHeaders] :: CustomTraceContext -> [(Text, Text)]
+ Freckle.App.OpenTelemetry.Context: class HasHeaders a
+ Freckle.App.OpenTelemetry.Context: extractContext :: (MonadIO m, MonadTracer m, HasHeaders a) => a -> m ()
+ Freckle.App.OpenTelemetry.Context: headersL :: HasHeaders a => Lens' a [Header]
+ Freckle.App.OpenTelemetry.Context: injectContext :: (MonadIO m, MonadTracer m, HasHeaders a) => a -> m a
+ Freckle.App.OpenTelemetry.Context: instance Data.Aeson.Types.FromJSON.FromJSON Freckle.App.OpenTelemetry.Context.CustomTraceContext
+ Freckle.App.OpenTelemetry.Context: instance Data.Aeson.Types.ToJSON.ToJSON Freckle.App.OpenTelemetry.Context.CustomTraceContext
+ Freckle.App.OpenTelemetry.Context: instance Freckle.App.OpenTelemetry.Context.HasHeaders (Faktory.Job.Job a)
+ Freckle.App.OpenTelemetry.Context: instance Freckle.App.OpenTelemetry.Context.HasHeaders Freckle.App.OpenTelemetry.Context.CustomTraceContext
+ Freckle.App.OpenTelemetry.Context: instance Freckle.App.OpenTelemetry.Context.HasHeaders Network.HTTP.Client.Types.Request
+ Freckle.App.OpenTelemetry.Context: instance Freckle.App.OpenTelemetry.Context.HasHeaders [Network.HTTP.Types.Header.Header]
+ Freckle.App.OpenTelemetry.Context: instance GHC.Base.Monoid Freckle.App.OpenTelemetry.Context.CustomTraceContext
+ Freckle.App.OpenTelemetry.Context: instance GHC.Base.Semigroup Freckle.App.OpenTelemetry.Context.CustomTraceContext
+ Freckle.App.OpenTelemetry.Context: instance GHC.Generics.Generic Freckle.App.OpenTelemetry.Context.CustomTraceContext
+ Freckle.App.OpenTelemetry.Context: newtype CustomTraceContext
+ Freckle.App.OpenTelemetry.Context: processWithContext :: (MonadUnliftIO m, MonadMask m, MonadTracer m, HasHeaders a, HasCallStack) => Text -> SpanArguments -> a -> (a -> m b) -> m b

Files

CHANGELOG.md view
@@ -1,5 +1,10 @@-## [_Unreleased_](https://github.com/freckle/freckle-app/compare/v1.15.4.0...main)+## [_Unreleased_](https://github.com/freckle/freckle-app/compare/v1.15.5.0...main) +## [v1.15.5.0](https://github.com/freckle/freckle-app/compare/v1.15.4.0...v1.15.5.0)++- Add `Freckle.App.OpenTelemetry.Context` with helpers for propagating trace+  context across services.+ ## [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@@ -8,14 +13,14 @@    ```diff   - import Network.Wai.Middleware.OpenTelemetry (newOpenTelemetryWaiMiddleware)-  + import Freckle.App.Wai (addThreadContextFromRequest)+  + import Freckle.App.Wai (addThreadContextFromTracing)   + import OpenTelemetry.Instrumentation.Wai (newOpenTelemetryWaiMiddleware)      traceMiddleware <- newOpenTelemetryWaiMiddleware      run 3000       . traceMiddleware-  +   . addThreadContextFromRequest+  +   . addThreadContextFromTracing       . ...       $ app   ```
freckle-app.cabal view
@@ -1,6 +1,6 @@ cabal-version:      1.18 name:               freckle-app-version:            1.15.4.0+version:            1.15.5.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.Context         Freckle.App.OpenTelemetry.ThreadContext         Freckle.App.Prelude         Freckle.App.Random@@ -237,6 +238,7 @@         Freckle.App.HttpSpec         Freckle.App.Memcached.ServersSpec         Freckle.App.MemcachedSpec+        Freckle.App.OpenTelemetry.ContextSpec         Freckle.App.Test.Http.MatchRequestSpec         Freckle.App.Test.Properties.JSONSpec         Freckle.App.Test.Properties.PathPieceSpec@@ -273,6 +275,7 @@         conduit >=1.3.5,         errors >=2.3.0,         freckle-app,+        hs-opentelemetry-api >=0.1.0.0,         hspec >=2.9.7,         hspec-expectations-json >=1.0.0.7,         hspec-expectations-lifted >=0.10.0,
library/Freckle/App/OpenTelemetry.hs view
@@ -31,6 +31,7 @@     -- * Effects   , MonadTracer (..)   , inSpan+  , SpanArguments (..)   , defaultSpanArguments   , serverSpanArguments   , clientSpanArguments@@ -43,6 +44,12 @@   , getCurrentTraceIdAsDatadog   , getCurrentSpanContext +    -- * Ids+  , TraceId+  , traceIdToHex+  , SpanId+  , spanIdToHex+     -- * Attributes   , ToAttribute (..)   , addCurrentSpanAttributes@@ -76,7 +83,13 @@ import OpenTelemetry.Trace hiding (inSpan) import OpenTelemetry.Trace.Core (getSpanContext) import qualified OpenTelemetry.Trace.Core as Trace (SpanContext (..))-import OpenTelemetry.Trace.Id (TraceId)+import OpenTelemetry.Trace.Id+  ( Base (..)+  , SpanId+  , TraceId+  , spanIdBaseEncodedText+  , traceIdBaseEncodedText+  ) import OpenTelemetry.Trace.Monad import UnliftIO.Exception (bracket) @@ -143,6 +156,12 @@   mTraceId <- getCurrentTraceIdAsDatadog   maybe f (\traceId -> withThreadContext ["trace_id" .= traceId] f) mTraceId {-# DEPRECATED withTraceIdContext "Use Freckle.App.OpenTelemetry.ThreadContext" #-}++traceIdToHex :: TraceId -> Text+traceIdToHex = traceIdBaseEncodedText Base16++spanIdToHex :: SpanId -> Text+spanIdToHex = spanIdBaseEncodedText Base16  -- | Convert a 'ByteString' to an 'Attribute' safely --
+ library/Freckle/App/OpenTelemetry/Context.hs view
@@ -0,0 +1,116 @@+module Freckle.App.OpenTelemetry.Context+  ( HasHeaders (..)+  , CustomTraceContext (..)+  , extractContext+  , injectContext+  , processWithContext+  ) where++import Freckle.App.Prelude++import Control.Error.Util (hush)+import Control.Lens (Lens', lens, (&), (.~), (^.))+import Control.Monad.Catch (MonadMask)+import Data.Aeson (FromJSON, ToJSON)+import Data.ByteString (ByteString)+import Data.CaseInsensitive (CI)+import qualified Data.CaseInsensitive as CI+import Faktory.Job (Job, custom, jobOptions)+import Faktory.Job.Custom (fromCustom, toCustom)+import Faktory.JobOptions (JobOptions (..))+import Freckle.App.OpenTelemetry+  ( MonadTracer (..)+  , SpanArguments+  , inSpan+  )+import Freckle.App.OpenTelemetry.ThreadContext (withTraceContext)+import Network.HTTP.Client (Request, requestHeaders)+import Network.HTTP.Simple (setRequestHeaders)+import Network.HTTP.Types.Header (Header)+import OpenTelemetry.Context (Context)+import OpenTelemetry.Context.ThreadLocal (attachContext, getContext)+import OpenTelemetry.Propagator (Propagator, extract, inject)+import OpenTelemetry.Trace.Core+  ( getTracerProviderPropagators+  , getTracerTracerProvider+  )++class HasHeaders a where+  headersL :: Lens' a [Header]++instance HasHeaders [Header] where+  headersL = id++instance HasHeaders Request where+  headersL = lens requestHeaders $ \req hs -> setRequestHeaders hs req++instance HasHeaders (Job a) where+  headersL = optionsL . customTraceContextL . headersL++optionsL :: Lens' (Job a) JobOptions+optionsL = lens jobOptions $ \x y -> x {jobOptions = y}++customTraceContextL :: Lens' JobOptions CustomTraceContext+customTraceContextL = lens get set+ where+  get jo = fromMaybe mempty $ hush . fromCustom =<< joCustom jo+  set jo = (jo <>) . custom . toCustom++-- | A type that can be stored as the @custom@ field of a Faktory 'Job'+newtype CustomTraceContext = CustomTraceContext+  { traceHeaders :: [(Text, Text)]+  }+  deriving stock (Generic)+  deriving newtype (Semigroup, Monoid)+  deriving anyclass (FromJSON, ToJSON)++instance HasHeaders CustomTraceContext where+  headersL = lens (map encode . traceHeaders) $ \x y -> x {traceHeaders = map decode y}++encode :: (Text, Text) -> (CI ByteString, ByteString)+encode = bimap (CI.mk . encodeUtf8) encodeUtf8++decode :: (CI ByteString, ByteString) -> (Text, Text)+decode = bimap (decodeUtf8 . CI.original) decodeUtf8++-- | Update our trace context from that extracted from the given item's headers+extractContext+  :: (MonadIO m, MonadTracer m, HasHeaders a) => a -> m ()+extractContext a = do+  context <- getContext+  propagator <- getPropagator+  updatedContext <- extract propagator (a ^. headersL) context+  void $ attachContext updatedContext++-- | Inject our trace context into the given item's headers+injectContext+  :: (MonadIO m, MonadTracer m, HasHeaders a) => a -> m a+injectContext a = do+  context <- getContext+  propagator <- getPropagator+  headers <- inject propagator context $ a ^. headersL+  pure $ a & headersL .~ headers++getPropagator :: MonadTracer m => m (Propagator Context [Header] [Header])+getPropagator =+  getTracerProviderPropagators . getTracerTracerProvider <$> getTracer++-- | Process an item (a request, a Job, etc) in a top-level span and context+processWithContext+  :: ( MonadUnliftIO m+     , MonadMask m+     , MonadTracer m+     , HasHeaders a+     , HasCallStack+     )+  => Text+  -- ^ Span name+  -> SpanArguments+  -> a+  -> (a -> m b)+  -> m b+processWithContext name args a f = do+  extractContext a+  inSpan name args $ do+    a' <- injectContext a+    withTraceContext $ f a'
package.yaml view
@@ -1,5 +1,5 @@ name: freckle-app-version: 1.15.4.0+version: 1.15.5.0 maintainer: Freckle Education category: Utils github: freckle/freckle-app@@ -168,6 +168,7 @@       - conduit       - errors       - freckle-app+      - hs-opentelemetry-api       - hspec       - hspec-expectations-json       - hspec-expectations-lifted
+ tests/Freckle/App/OpenTelemetry/ContextSpec.hs view
@@ -0,0 +1,156 @@+module Freckle.App.OpenTelemetry.ContextSpec+  ( spec+  ) where++import Freckle.App.Test++import Blammo.Logging+import Blammo.Logging.Logger (newTestLogger)+import Control.Lens (lens)+import qualified Data.Text as T+import Freckle.App.OpenTelemetry+import Freckle.App.OpenTelemetry.Context+import Network.HTTP.Types.Header (Header)+import qualified OpenTelemetry.Trace.Core as Trace++data App = App+  { appLogger :: Logger+  , appTracer :: Tracer+  }++instance HasLogger App where+  loggerL = lens appLogger $ \x y -> x {appLogger = y}++instance HasTracer App where+  tracerL = lens appTracer $ \x y -> x {appTracer = y}++loadApp :: (App -> IO ()) -> IO ()+loadApp f = do+  appLogger <- newTestLogger defaultLogSettings++  withTracerProvider $ \tracerProvider -> do+    let appTracer = makeTracer tracerProvider "app" tracerOptions+    f App {..}++spec :: Spec+spec = withApp loadApp $ do+  describe "injectContext" $ do+    it "sets request headers from existing context" $ appExample $ do+      inSpan "example" defaultSpanArguments $ do+        spanContext <- assertCurrentSpanContext++        let expectedTraceParent =+              toTraceParent+                (traceIdToHex $ Trace.traceId spanContext)+                (spanIdToHex $ Trace.spanId spanContext)++        injectContext ([] :: [Header])+          `shouldReturn` [ ("traceparent", encodeUtf8 expectedTraceParent)+                         , ("tracestate", "")+                         ]++  describe "extractContext" $ do+    it "sets the context from the headers" $ appExample $ do+      let+        traceId = "fba7cd19bff3ef866a599d6f6a85baef"+        spanId = "5747fd6b144f4009"++        headers :: [Header]+        headers =+          [ ("traceparent", encodeUtf8 $ toTraceParent traceId spanId)+          , ("tracestate", "")+          ]++      extractContext headers++      spanContext <- assertCurrentSpanContext+      traceIdToHex (Trace.traceId spanContext) `shouldBe` traceId+      spanIdToHex (Trace.spanId spanContext) `shouldBe` spanId++    it "does nothing with no headers" $ appExample $ do+      getCurrentSpanContext `shouldReturn` Nothing++      extractContext ([] :: [Header])++      getCurrentSpanContext `shouldReturn` Nothing++  describe "processWithContext" $ do+    it "creates a child-span context" $ appExample $ do+      inSpan "example" defaultSpanArguments $ do+        outerSpanContext <- assertCurrentSpanContext++        let+          traceId = traceIdToHex $ Trace.traceId outerSpanContext+          spanId = spanIdToHex $ Trace.spanId outerSpanContext++          headers :: [Header]+          headers =+            [ ("traceparent", encodeUtf8 $ toTraceParent traceId spanId)+            , ("tracestate", "")+            ]++        processWithContext "loop" defaultSpanArguments headers $ \_ -> do+          innerSpanContext <- assertCurrentSpanContext++          -- Ideally we'd assert the inner span's parent is the outer span, but+          -- the library doesn't expose the necessary functionality. All we can+          -- do is assert it's a fresh span of the same trace.+          Trace.traceId innerSpanContext `shouldBe` Trace.traceId outerSpanContext+          Trace.spanId innerSpanContext `shouldNotBe` Trace.spanId outerSpanContext++    it "sets the current context back into the headers" $ appExample $ do+      inSpan "example" defaultSpanArguments $ do+        let+          traceId = "fba7cd19bff3ef866a599d6f6a85baef"+          spanId = "5747fd6b144f4009"++          headers :: [Header]+          headers =+            [ ("traceparent", encodeUtf8 $ toTraceParent traceId spanId)+            , ("tracestate", "")+            ]++        processWithContext "loop" defaultSpanArguments headers $ \headers' -> do+          spanContext <- assertCurrentSpanContext++          let headerTraceParent = do+                bs <- lookup "traceparent" headers'+                fromTraceParent $ decodeUtf8 bs++          fmap fst headerTraceParent `shouldBe` Just traceId+          fmap snd headerTraceParent+            `shouldBe` Just (spanIdToHex $ Trace.spanId spanContext)++    it "sets a fresh context back into the headers" $ appExample $ do+      inSpan "example" defaultSpanArguments $ do+        let+          headers :: [Header]+          headers = []++        processWithContext "loop" defaultSpanArguments headers $ \headers' -> do+          spanContext <- assertCurrentSpanContext++          let headerTraceParent = do+                bs <- lookup "traceparent" headers'+                fromTraceParent $ decodeUtf8 bs++          fmap fst headerTraceParent+            `shouldBe` Just (traceIdToHex $ Trace.traceId spanContext)+          fmap snd headerTraceParent+            `shouldBe` Just (spanIdToHex $ Trace.spanId spanContext)++assertCurrentSpanContext :: (MonadIO m, HasCallStack) => m Trace.SpanContext+assertCurrentSpanContext =+  getCurrentSpanContext >>= \case+    Nothing -> expectationFailure "Expect there to be a SpanContext"+    Just spanContext -> pure spanContext++toTraceParent :: Text -> Text -> Text+toTraceParent traceId spanId = "00-" <> traceId <> "-" <> spanId <> "-00"++fromTraceParent :: Text -> Maybe (Text, Text)+fromTraceParent a = do+  b <- T.stripPrefix "00-" a+  c <- T.stripSuffix "-00" b+  [traceId, spanId] <- Just $ T.splitOn "-" c+  pure (traceId, spanId)