packages feed

shibuya-core 0.1.0.0 → 0.2.0.0

raw patch · 7 files changed

+258/−45 lines, 7 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Shibuya.Telemetry.Semantic: attrMessagingDestinationPartitionId :: Text
- Shibuya.Telemetry.Semantic: eventHandlerException :: Text
- Shibuya.Telemetry.Semantic: processMessageSpanName :: Text
+ Shibuya.Core.Types: instance Control.DeepSeq.NFData Shibuya.Core.Types.Cursor
+ Shibuya.Core.Types: instance Control.DeepSeq.NFData Shibuya.Core.Types.MessageId
+ Shibuya.Core.Types: instance Control.DeepSeq.NFData msg => Control.DeepSeq.NFData (Shibuya.Core.Types.Envelope msg)
+ Shibuya.Telemetry.Semantic: attrMessagingOperation :: Text
+ Shibuya.Telemetry.Semantic: attrShibuyaPartition :: Text
+ Shibuya.Telemetry.Semantic: processSpanName :: Text -> Text

Files

CHANGELOG.md view
@@ -1,5 +1,28 @@ # Changelog +## 0.2.0.0 — 2026-04-22++### Breaking Changes++- `Shibuya.Telemetry.Semantic`: rename `processMessageSpanName :: Text` to+  `processSpanName :: Text -> Text`. The span name is now built from the+  destination (processor id), yielding e.g. `"shibuya-consumer process"`, in+  line with the OpenTelemetry messaging-spans recommendation.+- `Shibuya.Telemetry.Semantic`: remove `attrMessagingDestinationPartitionId`+  (replaced by the Shibuya-specific `attrShibuyaPartition`).+- `Shibuya.Telemetry.Semantic`: remove `eventHandlerException`.++### New Features++- `Shibuya.Telemetry.Semantic`: add `attrMessagingOperation` and+  `attrShibuyaPartition` attribute keys. Messaging attribute keys are now+  sourced from the typed `AttributeKey` values exported by+  `OpenTelemetry.SemanticConventions`, so upstream renames surface as+  compile errors rather than silent wire-format drift.+- `Shibuya.Core.Types`: add `NFData` instances for `MessageId`, `Cursor`,+  and `Envelope a` (when `a` itself has an `NFData` instance). Benchmark+  authors no longer need to declare these as orphans.+ ## 0.1.0.0 — 2026-02-24  Initial release.
shibuya-core.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.14 name: shibuya-core-version: 0.1.0.0+version: 0.2.0.0 synopsis: Supervised queue processing framework for Haskell description:   A supervised queue processing framework inspired by Broadway (Elixir).@@ -65,11 +65,13 @@     base ^>=4.21.0.0,     bytestring ^>=0.12.2.0,     containers ^>=0.7,+    deepseq ^>=1.5,     effectful ^>=2.6.1.0,     effectful-core ^>=2.6.1.0,     generic-lens ^>=2.3.0.0,     hs-opentelemetry-api ^>=0.3,     hs-opentelemetry-propagator-w3c ^>=0.1,+    hs-opentelemetry-semantic-conventions ^>=0.1,     lens ^>=5.3.5,     nqe ^>=0.6,     stm ^>=2.5,@@ -115,6 +117,7 @@     Shibuya.RunnerSpec     Shibuya.Telemetry.EffectSpec     Shibuya.Telemetry.PropagationSpec+    Shibuya.Telemetry.SemanticSpec    build-depends:     QuickCheck ^>=2.15,@@ -122,6 +125,7 @@     bytestring,     effectful,     hs-opentelemetry-api,+    hs-opentelemetry-exporter-in-memory ^>=0.0,     hspec ^>=2.11,     memory,     nqe,@@ -132,3 +136,5 @@     text,     time,     unliftio,+    unordered-containers,+    vector,
src/Shibuya/Core/Types.hs view
@@ -16,6 +16,7 @@   ) where +import Control.DeepSeq (NFData) import Data.ByteString (ByteString) import Data.String (IsString) import Shibuya.Prelude@@ -25,6 +26,7 @@ newtype MessageId = MessageId {unMessageId :: Text}   deriving stock (Eq, Ord, Show, Generic)   deriving newtype (IsString)+  deriving anyclass (NFData)  -- | Optional cursor / offset / global position. -- Used to track position in ordered streams.@@ -32,6 +34,7 @@   = CursorInt !Int   | CursorText !Text   deriving stock (Eq, Ord, Show, Generic)+  deriving anyclass (NFData)  -- | W3C Trace Context headers for distributed tracing. -- Contains traceparent and optionally tracestate headers.@@ -54,3 +57,4 @@     payload :: !msg   }   deriving stock (Eq, Show, Functor, Generic)+  deriving anyclass (NFData)
src/Shibuya/Runner/Supervised.hs view
@@ -74,19 +74,20 @@   ) import Shibuya.Telemetry.Propagation (extractTraceContext) import Shibuya.Telemetry.Semantic-  ( attrMessagingDestinationPartitionId,+  ( attrMessagingDestinationName,     attrMessagingMessageId,+    attrMessagingOperation,     attrMessagingSystem,     attrShibuyaAckDecision,     attrShibuyaInflightCount,     attrShibuyaInflightMax,+    attrShibuyaPartition,     consumerSpanArgs,     eventAckDecision,     eventHandlerCompleted,-    eventHandlerException,     eventHandlerStarted,     mkEvent,-    processMessageSpanName,+    processSpanName,   ) import Streamly.Data.Fold qualified as Fold import Streamly.Data.Stream qualified as Stream@@ -160,7 +161,7 @@       runInIO $         -- Catch ProcessorHalt to prevent propagation via link         -- (Halt is intentional, not a failure - other processors should continue)-        ( runIngesterAndProcessor metricsVar doneVar inboxSize concurrency adapter handler+        ( runIngesterAndProcessor metricsVar procId doneVar inboxSize concurrency adapter handler             `catch` \(ProcessorHalt _) -> pure () -- Convert halt to graceful exit         )           `finally` unregisterProcessor master procId@@ -205,7 +206,7 @@   runIngesterWithMetrics metricsVar adapter.source inbox    -- Drain remaining messages from inbox-  drainInboxWithMetrics metricsVar handler inbox+  drainInboxWithMetrics metricsVar procId handler inbox    -- Mark done   liftIO $ atomically $ writeTVar doneVar True@@ -224,13 +225,14 @@ runIngesterAndProcessor ::   (IOE :> es, Tracing :> es) =>   TVar ProcessorMetrics ->+  ProcessorId ->   TVar Bool ->   Natural ->   Concurrency ->   Adapter es msg ->   Handler es msg ->   Eff es ()-runIngesterAndProcessor metricsVar doneVar inboxSize concurrency adapter handler = do+runIngesterAndProcessor metricsVar procId doneVar inboxSize concurrency adapter handler = do   -- Create bounded inbox (this is where inboxSize is used for backpressure)   inbox <- liftIO $ newBoundedInbox inboxSize @@ -247,7 +249,7 @@      UIO.withAsync ingesterWithSignal $ \_ ->       -- Processor: process messages, exit when stream done and inbox empty-      runInIO $ processUntilDrained metricsVar concurrency handler inbox streamDoneVar+      runInIO $ processUntilDrained metricsVar procId concurrency handler inbox streamDoneVar    -- Mark done when processor exits   liftIO $ atomically $ writeTVar doneVar True@@ -294,12 +296,13 @@ processUntilDrained ::   (IOE :> es, Tracing :> es) =>   TVar ProcessorMetrics ->+  ProcessorId ->   Concurrency ->   Handler es msg ->   Inbox (Ingested es msg) ->   TVar Bool ->   Eff es ()-processUntilDrained metricsVar concurrency handler inbox streamDoneVar = do+processUntilDrained metricsVar procId concurrency handler inbox streamDoneVar = do   haltRef <- liftIO $ newIORef Nothing    let maxConc = case concurrency of@@ -309,7 +312,7 @@    withEffToIO (ConcUnlift Persistent Unlimited) $ \runInIO -> do     let inboxStream = inboxToStream inbox streamDoneVar haltRef-        processAction = runInIO . processOne metricsVar maxConc haltRef handler+        processAction = runInIO . processOne metricsVar procId maxConc haltRef handler      case concurrency of       Serial ->@@ -333,10 +336,11 @@ drainInboxWithMetrics ::   (IOE :> es, Tracing :> es) =>   TVar ProcessorMetrics ->+  ProcessorId ->   Handler es msg ->   Inbox (Ingested es msg) ->   Eff es ()-drainInboxWithMetrics metricsVar handler inbox = do+drainInboxWithMetrics metricsVar procId handler inbox = do   haltRef <- liftIO $ newIORef Nothing   go haltRef   where@@ -344,7 +348,7 @@       empty <- liftIO $ mailboxEmpty inbox       unless empty $ do         ingested <- liftIO $ receive inbox-        processOne metricsVar 1 haltRef handler ingested+        processOne metricsVar procId 1 haltRef handler ingested         -- Check if halted         halted <- liftIO $ readIORef haltRef         case halted of@@ -356,25 +360,29 @@ processOne ::   (IOE :> es, Tracing :> es) =>   TVar ProcessorMetrics ->+  ProcessorId ->   Int ->   IORef (Maybe HaltReason) ->   Handler es msg ->   Ingested es msg ->   Eff es ()-processOne metricsVar maxConc haltRef handler ingested = do+processOne metricsVar procId maxConc haltRef handler ingested = do   -- Extract parent context from message headers for distributed tracing   let parentCtx = ingested.envelope.traceContext >>= extractTraceContext+      ProcessorId pidText = procId    withExtractedContext parentCtx $-    withSpan' processMessageSpanName consumerSpanArgs $ \traceSpan -> do+    withSpan' (processSpanName pidText) consumerSpanArgs $ \traceSpan -> do       -- Add messaging attributes       let MessageId msgIdText = ingested.envelope.messageId       addAttribute traceSpan attrMessagingSystem ("shibuya" :: Text)+      addAttribute traceSpan attrMessagingDestinationName pidText+      addAttribute traceSpan attrMessagingOperation ("process" :: Text)       addAttribute traceSpan attrMessagingMessageId msgIdText        -- Add partition if present       case ingested.envelope.partition of-        Just p -> addAttribute traceSpan attrMessagingDestinationPartitionId p+        Just p -> addAttribute traceSpan attrShibuyaPartition p         Nothing -> pure ()        -- Increment in-flight and add inflight attributes@@ -406,7 +414,6 @@           )           ( \ex -> do               recordException traceSpan ex-              addEvent traceSpan (mkEvent eventHandlerException [])               pure $ Left $ HandlerException $ Text.pack $ show ex           ) 
src/Shibuya/Telemetry/Semantic.hs view
@@ -1,26 +1,31 @@ -- | Semantic conventions for Shibuya OpenTelemetry instrumentation. -- Provides span names, attribute keys, and helpers following OTel conventions.+--+-- Attribute keys for the OTel @messaging.*@ namespace are derived from the+-- typed @AttributeKey@ values exported by @OpenTelemetry.SemanticConventions@,+-- so an upstream rename surfaces as a Haskell compile error rather than a+-- silent wire-format drift. module Shibuya.Telemetry.Semantic   ( -- * Span Names-    processMessageSpanName,+    processSpanName,     ingestSpanName,      -- * Attribute Keys (OTel Messaging Conventions)     attrMessagingSystem,     attrMessagingMessageId,     attrMessagingDestinationName,-    attrMessagingDestinationPartitionId,+    attrMessagingOperation,      -- * Attribute Keys (Shibuya-specific)     attrShibuyaProcessorId,     attrShibuyaInflightCount,     attrShibuyaInflightMax,     attrShibuyaAckDecision,+    attrShibuyaPartition,      -- * Event Names     eventHandlerStarted,     eventHandlerCompleted,-    eventHandlerException,     eventAckDecision,      -- * SpanArguments Helpers@@ -34,7 +39,8 @@  import Data.HashMap.Strict qualified as HashMap import Data.Text (Text)-import OpenTelemetry.Attributes (Attribute)+import OpenTelemetry.Attributes (Attribute, AttributeKey (..))+import OpenTelemetry.SemanticConventions qualified as Sem import OpenTelemetry.Trace.Core   ( NewEvent (..),     SpanArguments (..),@@ -46,8 +52,18 @@ --------------------------------------------------------------------------------  -- | Span name for processing a single message.-processMessageSpanName :: Text-processMessageSpanName = "shibuya.process.message"+--+-- Follows the OpenTelemetry messaging-spans recommendation that consumer+-- spans be named @\"<destination> <operation>\"@ — for Shibuya the+-- destination is the processor id and the operation is @\"process\"@,+-- yielding e.g. @\"shibuya-consumer process\"@.+--+-- This intentionally departs from+-- @hs-opentelemetry-instrumentation-hw-kafka-client@'s+-- @\"process <topic>\"@ order in favor of the spec's order; see+-- @docs/plans/2-align-opentelemetry-semantic-conventions.md@.+processSpanName :: Text -> Text+processSpanName destination = destination <> " process"  -- | Span name for the ingester lifecycle. ingestSpanName :: Text@@ -56,63 +72,94 @@ -------------------------------------------------------------------------------- -- Attribute Keys (OTel Messaging Semantic Conventions) -- See: https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/+--+-- Each constant is the wire string of a typed 'AttributeKey' exported by+-- 'OpenTelemetry.SemanticConventions'. Using @unkey@ (the AttributeKey+-- newtype selector) keeps these strings in lock-step with the upstream+-- code-generated keys. -------------------------------------------------------------------------------- --- | The messaging system identifier.+-- | The messaging system identifier (@messaging.system@). attrMessagingSystem :: Text-attrMessagingSystem = "messaging.system"+attrMessagingSystem = unkey Sem.messaging_system --- | The message identifier.+-- | The message identifier (@messaging.message.id@). attrMessagingMessageId :: Text-attrMessagingMessageId = "messaging.message.id"+attrMessagingMessageId = unkey Sem.messaging_message_id --- | The destination (queue/topic) name.+-- | The destination (queue/topic) name (@messaging.destination.name@). attrMessagingDestinationName :: Text-attrMessagingDestinationName = "messaging.destination.name"+attrMessagingDestinationName = unkey Sem.messaging_destination_name --- | The partition identifier.-attrMessagingDestinationPartitionId :: Text-attrMessagingDestinationPartitionId = "messaging.destination.partition.id"+-- | The messaging operation (@messaging.operation@).+--+-- One of the spec-defined enum values: @publish@, @receive@, @process@,+-- @settle@, @create@. Shibuya's per-message span uses @"process"@.+attrMessagingOperation :: Text+attrMessagingOperation = unkey Sem.messaging_operation  -------------------------------------------------------------------------------- -- Attribute Keys (Shibuya-specific)+--+-- These keys are intentionally outside the upstream OTel spec. The+-- semantic-conventions guide is explicit that non-spec attributes should+-- carry a vendor prefix; @shibuya.*@ satisfies that and cannot collide+-- with any current or future spec key. -------------------------------------------------------------------------------- --- | The processor identifier.+-- | The processor identifier (@shibuya.processor.id@).+-- Shibuya-specific: identifies a deployment-level processor instance. attrShibuyaProcessorId :: Text attrShibuyaProcessorId = "shibuya.processor.id" --- | Current number of in-flight messages.+-- | Current number of in-flight messages (@shibuya.inflight.count@).+-- Shibuya-specific: a property of Shibuya's supervision/concurrency model. attrShibuyaInflightCount :: Text attrShibuyaInflightCount = "shibuya.inflight.count" --- | Maximum concurrency limit.+-- | Maximum concurrency limit (@shibuya.inflight.max@).+-- Shibuya-specific: a property of Shibuya's supervision/concurrency model. attrShibuyaInflightMax :: Text attrShibuyaInflightMax = "shibuya.inflight.max" --- | The ack decision made by the handler.+-- | The ack decision made by the handler (@shibuya.ack.decision@).+-- Shibuya-specific: there is no upstream key for an explicit ack decision. attrShibuyaAckDecision :: Text attrShibuyaAckDecision = "shibuya.ack.decision" +-- | A generic partition identifier (@shibuya.partition@).+--+-- Shibuya-specific: in semantic-conventions v1.24 there is no portable+-- @messaging.destination.partition.id@ key — only system-specific ones+-- (@messaging.kafka.destination.partition@,+-- @messaging.eventhubs.destination.partition.id@). System-specific+-- adapters that know which broker is in use may emit those in addition,+-- but a generic Shibuya envelope only carries an opaque partition string,+-- which we surface under this vendor-prefixed key.+attrShibuyaPartition :: Text+attrShibuyaPartition = "shibuya.partition"+ -------------------------------------------------------------------------------- -- Event Names+--+-- Shibuya-namespaced; the upstream spec does not define handler.* events.+-- The standard @exception@ event (with @exception.type@/@exception.message@/+-- @exception.stacktrace@) is emitted by 'OpenTelemetry.Trace.Core.recordException'+-- and so is not duplicated here. -------------------------------------------------------------------------------- --- | Event recorded when handler execution starts.+-- | Event recorded when handler execution starts (@shibuya.handler.started@). eventHandlerStarted :: Text-eventHandlerStarted = "handler.started"+eventHandlerStarted = "shibuya.handler.started" --- | Event recorded when handler execution completes successfully.+-- | Event recorded when handler execution completes successfully+-- (@shibuya.handler.completed@). eventHandlerCompleted :: Text-eventHandlerCompleted = "handler.completed"---- | Event recorded when handler throws an exception.-eventHandlerException :: Text-eventHandlerException = "handler.exception"+eventHandlerCompleted = "shibuya.handler.completed" --- | Event recorded when ack decision is made.+-- | Event recorded when ack decision is made (@shibuya.ack.decision@). eventAckDecision :: Text-eventAckDecision = "ack.decision"+eventAckDecision = "shibuya.ack.decision"  -------------------------------------------------------------------------------- -- SpanArguments Helpers
test/Main.hs view
@@ -9,6 +9,7 @@ import Shibuya.RunnerSpec qualified import Shibuya.Telemetry.EffectSpec qualified import Shibuya.Telemetry.PropagationSpec qualified+import Shibuya.Telemetry.SemanticSpec qualified import Test.Hspec  main :: IO ()@@ -20,3 +21,4 @@   Shibuya.Runner.SupervisedSpec.spec   Shibuya.Telemetry.EffectSpec.spec   Shibuya.Telemetry.PropagationSpec.spec+  Shibuya.Telemetry.SemanticSpec.spec
+ test/Shibuya/Telemetry/SemanticSpec.hs view
@@ -0,0 +1,124 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Asserts that spans emitted by 'processOne' carry the wire names+-- defined by the OpenTelemetry messaging semantic conventions and the+-- shibuya-namespaced fallbacks. This is the guard that catches drift+-- between Shibuya's emitted attributes and the upstream spec — a future+-- rename in @hs-opentelemetry-semantic-conventions@ will break both+-- compilation (because @Shibuya.Telemetry.Semantic@ derives the strings+-- from typed @AttributeKey@s) and these assertions (because the wire+-- string changed).+module Shibuya.Telemetry.SemanticSpec (spec) where++import Data.Foldable (toList)+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.IORef (readIORef)+import Data.Text (Text)+import Effectful (runEff)+import OpenTelemetry.Attributes+  ( Attribute (..),+    PrimitiveAttribute (..),+    emptyAttributes,+    getAttributeMap,+  )+import OpenTelemetry.Exporter.InMemory.Span (inMemoryListExporter)+import OpenTelemetry.Trace.Core+  ( Event (..),+    ImmutableSpan (..),+    InstrumentationLibrary (..),+    createTracerProvider,+    emptyTracerProviderOptions,+    makeTracer,+    shutdownTracerProvider,+    tracerOptions,+  )+import OpenTelemetry.Util (appendOnlyBoundedCollectionValues)+import Shibuya.Adapter.Mock (listAdapter)+import Shibuya.Core.Ack (AckDecision (..))+import Shibuya.Core.AckHandle (AckHandle (..))+import Shibuya.Core.Ingested (Ingested (..))+import Shibuya.Core.Types (Envelope (..), MessageId (..))+import Shibuya.Runner.Metrics (ProcessorId (..))+import Shibuya.Runner.Supervised (runWithMetrics)+import Shibuya.Telemetry.Effect (runTracing)+import Test.Hspec++spec :: Spec+spec = describe "Shibuya.Telemetry.Semantic (wire-format)" $ do+  it "emits a process span with conventions-aligned attributes and events" $ do+    (processor, spansRef) <- inMemoryListExporter+    provider <- createTracerProvider [processor] emptyTracerProviderOptions+    let tracer =+          makeTracer+            provider+            ( InstrumentationLibrary+                { libraryName = "shibuya-test",+                  libraryVersion = "",+                  librarySchemaUrl = "",+                  libraryAttributes = emptyAttributes+                }+            )+            tracerOptions++    runEff $ runTracing tracer $ do+      let envelope =+            Envelope+              { messageId = MessageId "m-1",+                cursor = Nothing,+                partition = Nothing,+                enqueuedAt = Nothing,+                traceContext = Nothing,+                payload = ("hello" :: Text)+              }+          ingested =+            Ingested+              { envelope = envelope,+                ack = AckHandle (\_ -> pure ()),+                lease = Nothing+              }+          adapter = listAdapter [ingested]+          handler _ = pure AckOk+          procId = ProcessorId "test-proc"+      _ <- runWithMetrics 4 procId adapter handler+      pure ()++    _ <- shutdownTracerProvider provider+    spans <- readIORef spansRef+    case spans of+      [s] -> do+        spanName s `shouldBe` "test-proc process"+        let attrs = getAttributeMap (spanAttributes s)+        attrs `shouldHaveTextAttribute` ("messaging.system", "shibuya")+        attrs `shouldHaveTextAttribute` ("messaging.message.id", "m-1")+        attrs `shouldHaveTextAttribute` ("messaging.destination.name", "test-proc")+        attrs `shouldHaveTextAttribute` ("messaging.operation", "process")+        attrs `shouldHaveTextAttribute` ("shibuya.ack.decision", "ack_ok")+        attrs `shouldHaveIntAttribute` ("shibuya.inflight.count", 1)+        attrs `shouldHaveIntAttribute` ("shibuya.inflight.max", 1)+        let evNames = map eventName (toList (appendOnlyBoundedCollectionValues (spanEvents s)))+        evNames `shouldContain` ["shibuya.handler.started"]+        evNames `shouldContain` ["shibuya.handler.completed"]+      _ ->+        expectationFailure $ "expected exactly one span, got " <> show (length spans)+  where+    shouldHaveTextAttribute :: HashMap Text Attribute -> (Text, Text) -> Expectation+    shouldHaveTextAttribute attrs (k, expected) =+      case HashMap.lookup k attrs of+        Just (AttributeValue (TextAttribute v)) -> v `shouldBe` expected+        Just other ->+          expectationFailure $+            "attribute " <> show k <> " was not a Text: " <> show other+        Nothing ->+          expectationFailure $+            "attribute " <> show k <> " missing; have keys " <> show (HashMap.keys attrs)+    shouldHaveIntAttribute :: HashMap Text Attribute -> (Text, Int) -> Expectation+    shouldHaveIntAttribute attrs (k, expected) =+      case HashMap.lookup k attrs of+        Just (AttributeValue (IntAttribute v)) -> v `shouldBe` fromIntegral expected+        Just other ->+          expectationFailure $+            "attribute " <> show k <> " was not an Int: " <> show other+        Nothing ->+          expectationFailure $+            "attribute " <> show k <> " missing; have keys " <> show (HashMap.keys attrs)