effectful-data-cache 0.1.0.2 → 0.2.0.1
raw patch · 6 files changed
+93/−181 lines, 6 filesdep −hs-opentelemetry-apidep −hs-opentelemetry-exporter-in-memorydep −hs-opentelemetry-sdkPVP ok
version bump matches the API change (PVP)
Dependencies removed: hs-opentelemetry-api, hs-opentelemetry-exporter-in-memory, hs-opentelemetry-sdk, text, unordered-containers
API changes (from Hackage documentation)
- Effectful.Cache.OpenTelemetry: traceCache :: forall k v (es :: [Effect]) a. (Cache k v :> es, IOE :> es) => Tracer -> Eff es a -> Eff es a
Files
- CHANGELOG.md +29/−0
- README.md +2/−13
- effectful-data-cache.cabal +3/−10
- src/Effectful/Cache.hs +59/−12
- src/Effectful/Cache/OpenTelemetry.hs +0/−67
- test/Effectful/Cache/OpenTelemetrySpec.hs +0/−79
+ CHANGELOG.md view
@@ -0,0 +1,29 @@+## [2.0.1](https://github.com/haskell-github-trust/effectful-data-cache/compare/2.0.0...2.0.1) (2026-10-25)++## [2.0.0](https://github.com/haskell-github-trust/effectful-data-cache/compare/1.0.4...2.0.0) (2026-10-09)++### ⚠ BREAKING CHANGES++* split into base & otel packages++### Code Refactoring++* split into base & otel packages ([782b664](https://github.com/haskell-github-trust/effectful-data-cache/commit/782b664762aa56f2c9515ec3cff7cc4d612dd630))++## [1.0.4](https://github.com/haskell-github-trust/effectful-data-cache/compare/1.0.3...1.0.4) (2026-10-02)++## [1.0.3](https://github.com/haskell-github-trust/effectful-data-cache/compare/1.0.2...1.0.3) (2026-10-01)++## [1.0.2](https://github.com/haskell-github-trust/effectful-data-cache/compare/1.0.1...1.0.2) (2026-09-29)++## [1.0.1](https://github.com/haskell-github-trust/effectful-data-cache/compare/1.0.0...1.0.1) (2026-09-28)++### Features++* add otel telemetry ([9c0cdf2](https://github.com/haskell-github-trust/effectful-data-cache/commit/9c0cdf2a8e001644bde24cdcd0524243ee1651f7))++## 1.0.0 (2026-09-28)++### Features++* initial commit ([fefe10b](https://github.com/haskell-github-trust/effectful-data-cache/commit/fefe10b9003d85f70cb0348c7f715f3a1125bd4e))
README.md view
@@ -17,16 +17,5 @@ ## Tracing -`traceCache` wraps every operation in an OpenTelemetry span-(`cache.insert`, `cache.lookup`, …), with `cache.hit` on lookups and-`cache.size` on `size`/`keys`. Keys and values are never recorded.--```haskell-import Effectful.Cache.OpenTelemetry (traceCache)--main = runEff . runCache Nothing . traceCache tracer $ do- insert "key" (42 :: Int)-```--It is an interposer, so it layers over any interpreter and can be omitted-without touching call sites.+See [effectful-data-cache-opentelemetry](effectful-data-cache-opentelemetry)+for OpenTelemetry instrumentation.
effectful-data-cache.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: effectful-data-cache-version: 0.1.0.2+version: 0.2.0.1 synopsis: Data cache effect for the `effectful` system description: Please see the README on GitHub at <https://github.com/haskell-github-trust/effectful-data-cache#readme> category: cache@@ -19,6 +19,8 @@ build-type: Simple extra-source-files: README.md+extra-doc-files:+ CHANGELOG.md source-repository head type: git@@ -27,7 +29,6 @@ library exposed-modules: Effectful.Cache- Effectful.Cache.OpenTelemetry other-modules: Paths_effectful_data_cache autogen-modules:@@ -41,15 +42,12 @@ , clock ==0.8.* , effectful-core >=2.3 && <3 , hashable >=1.5 && <2- , hs-opentelemetry-api ==0.3.*- , text >=2.1 && <3 default-language: Haskell2010 test-suite effectful-data-cache-test type: exitcode-stdio-1.0 main-is: Spec.hs other-modules:- Effectful.Cache.OpenTelemetrySpec Effectful.CacheSpec Paths_effectful_data_cache autogen-modules:@@ -66,10 +64,5 @@ , effectful-core , effectful-data-cache , hashable >=1.5 && <2- , hs-opentelemetry-api- , hs-opentelemetry-exporter-in-memory- , hs-opentelemetry-sdk , hspec- , text- , unordered-containers default-language: Haskell2010
src/Effectful/Cache.hs view
@@ -7,6 +7,12 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} +-- | Dynamic effect over "Data.Cache", an in-memory key/value store with+-- optional per-item expiration.+--+-- > runEff . runCache @Text @Int Nothing $ do+-- > insert "a" 1+-- > lookup @Text @Int "a" module Effectful.Cache ( Cache (..), insert,@@ -41,49 +47,63 @@ import System.Clock (TimeSpec) import Prelude hiding (lookup) --- | @since 0.1.0.0+-- | Cache of values @v@ indexed by keys @k@.+--+-- @since 0.1.0.0 data Cache k v :: Effect where+ -- | See 'insert'. Insert :: Hashable k => k -> v -> Cache k v m ()+ -- | See 'insert''. Insert' :: Hashable k => Maybe TimeSpec -> k -> v -> Cache k v m ()+ -- | See 'lookup'. Lookup :: Hashable k => k -> Cache k v m (Maybe v)+ -- | See 'lookup''. Lookup' :: Hashable k => k -> Cache k v m (Maybe v)+ -- | See 'keys'. Keys :: Hashable k => Cache k v m [k]+ -- | See 'delete'. Delete :: Hashable k => k -> Cache k v m ()+ -- | See 'filterWithKey'. FilterWithKey :: Hashable k => (k -> v -> Bool) -> Cache k v m ()+ -- | See 'purge'. Purge :: Hashable k => Cache k v m ()+ -- | See 'purgeExpired'. PurgeExpired :: Hashable k => Cache k v m ()+ -- | See 'size'. Size :: Hashable k => Cache k v m Int+ -- | See 'defaultExpiration'. DefaultExpiration :: Hashable k => Cache k v m (Maybe TimeSpec)+ -- | See 'setDefaultExpiration'. SetDefaultExpiration :: Hashable k => Maybe TimeSpec@@ -91,7 +111,9 @@ type instance DispatchOf (Cache k v) = Dynamic --- | @since 0.1.0.0+-- | Insert an item with the cache's default expiration.+--+-- @since 0.1.0.0 insert :: forall k v es . Cache k v :> es@@ -101,7 +123,10 @@ -> Eff es () insert k v = send (Insert k v :: Cache k v (Eff es) ()) --- | @since 0.1.0.0+-- | Insert an item with an explicit expiration, relative to now;+-- 'Nothing' never expires. The default expiration is ignored.+--+-- @since 0.1.0.0 insert' :: forall k v es . Cache k v :> es@@ -112,7 +137,9 @@ -> Eff es () insert' ts k v = send (Insert' ts k v :: Cache k v (Eff es) ()) --- | @since 0.1.0.0+-- | Look up a non-expired item, evicting it if expired.+--+-- @since 0.1.0.0 lookup :: forall k v es . Cache k v :> es@@ -122,6 +149,7 @@ lookup k = send (Lookup k :: Cache k v (Eff es) (Maybe v)) -- | Like 'lookup' but never evicts the expired entry it read.+-- -- @since 0.1.0.0 lookup' :: forall k v es@@ -131,7 +159,9 @@ -> Eff es (Maybe v) lookup' k = send (Lookup' k :: Cache k v (Eff es) (Maybe v)) --- | @since 0.1.0.0+-- | All keys in the store, including expired ones.+--+-- @since 0.1.0.0 keys :: forall k v es . Cache k v :> es@@ -139,7 +169,9 @@ => Eff es [k] keys = send (Keys :: Cache k v (Eff es) [k]) --- | @since 0.1.0.0+-- | Delete an item; no-op if absent.+--+-- @since 0.1.0.0 delete :: forall k v es . Cache k v :> es@@ -148,7 +180,10 @@ -> Eff es () delete k = send (Delete k :: Cache k v (Eff es) ()) --- | @since 0.1.0.0+-- | Keep only items satisfying the predicate, which may be called+-- on expired items.+--+-- @since 0.1.0.0 filterWithKey :: forall k v es . Cache k v :> es@@ -157,7 +192,9 @@ -> Eff es () filterWithKey p = send (FilterWithKey p :: Cache k v (Eff es) ()) --- | @since 0.1.0.0+-- | Delete all items.+--+-- @since 0.1.0.0 purge :: forall k v es . Cache k v :> es@@ -165,7 +202,9 @@ => Eff es () purge = send (Purge :: Cache k v (Eff es) ()) --- | @since 0.1.0.0+-- | Delete all expired items, atomically.+--+-- @since 0.1.0.0 purgeExpired :: forall k v es . Cache k v :> es@@ -173,7 +212,9 @@ => Eff es () purgeExpired = send (PurgeExpired :: Cache k v (Eff es) ()) --- | @since 0.1.0.0+-- | Number of items, including expired ones.+--+-- @since 0.1.0.0 size :: forall k v es . Cache k v :> es@@ -181,7 +222,9 @@ => Eff es Int size = send (Size :: Cache k v (Eff es) Int) --- | @since 0.1.0.0+-- | Expiration applied by 'insert'.+--+-- @since 0.1.0.0 defaultExpiration :: forall k v es . Cache k v :> es@@ -189,7 +232,9 @@ => Eff es (Maybe TimeSpec) defaultExpiration = send (DefaultExpiration :: Cache k v (Eff es) (Maybe TimeSpec)) --- | @since 0.1.0.0+-- | Change the expiration applied by subsequent 'insert's.+--+-- @since 0.1.0.0 setDefaultExpiration :: forall k v es . Cache k v :> es@@ -199,6 +244,7 @@ setDefaultExpiration ts = send (SetDefaultExpiration ts :: Cache k v (Eff es) ()) -- | Run against a fresh store with the given default expiration.+-- -- @since 0.1.0.0 runCache :: forall k v es a@@ -211,6 +257,7 @@ runCacheWith c eff -- | Run against an existing 'C.Cache', e.g. one shared with non-effectful code.+-- -- @since 0.1.0.0 runCacheWith :: forall k v es a
− src/Effectful/Cache/OpenTelemetry.hs
@@ -1,67 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeOperators #-}---- | OpenTelemetry instrumentation for 'Cache', layered over any interpreter.------ > runEff . runCache Nothing . traceCache tracer $ do ...-module Effectful.Cache.OpenTelemetry (- traceCache,-) where--import Data.Maybe (isJust)-import Data.Text (Text)-import Effectful-import Effectful.Cache (Cache (..))-import Effectful.Dispatch.Dynamic (interpose, passthrough)-import OpenTelemetry.Trace.Core (- Tracer,- addAttribute,- defaultSpanArguments,- inSpan',- )---- | Wrap every cache operation in a span. Keys and values are never recorded:--- unbounded cardinality, and likely user data.------ @since 0.1.0.1-traceCache- :: forall k v es a- . Cache k v :> es- => IOE :> es- => Tracer- -> Eff es a- -> Eff es a-traceCache tracer = interpose @(Cache k v) $ \env op ->- inSpan' tracer (opName op) defaultSpanArguments $ \sp -> do- r <- passthrough env op- case op of- Lookup _ -> addAttribute sp "cache.hit" (isJust r)- Lookup' _ -> addAttribute sp "cache.hit" (isJust r)- Size -> addAttribute sp "cache.size" r- Keys -> addAttribute sp "cache.size" (length r)- _ -> pure ()- pure r--opName- :: Cache k v m a- -> Text-opName = \case- Insert{} -> "cache.insert"- Insert'{} -> "cache.insert"- Lookup{} -> "cache.lookup"- Lookup'{} -> "cache.lookup"- Keys{} -> "cache.keys"- Delete{} -> "cache.delete"- FilterWithKey{} -> "cache.filterWithKey"- Purge{} -> "cache.purge"- PurgeExpired{} -> "cache.purgeExpired"- Size{} -> "cache.size"- DefaultExpiration{} -> "cache.defaultExpiration"- SetDefaultExpiration{} -> "cache.setDefaultExpiration"
− test/Effectful/Cache/OpenTelemetrySpec.hs
@@ -1,79 +0,0 @@-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TypeApplications #-}--module Effectful.Cache.OpenTelemetrySpec (spec) where--import Data.IORef (readIORef)-import Data.Text (Text)-import Effectful-import Effectful.Cache-import Effectful.Cache.OpenTelemetry (traceCache)-import OpenTelemetry.Attributes (Attribute (..), PrimitiveAttribute (..), lookupAttribute)-import OpenTelemetry.Exporter.InMemory.Span (inMemoryListExporter)-import OpenTelemetry.Trace (- ImmutableSpan (..),- createTracerProvider,- emptyTracerProviderOptions,- makeTracer,- shutdownTracerProvider,- tracerOptions,- )-import Test.Hspec-import Prelude hiding (lookup)---- | Run the given cache program under a traced interpreter, returning its--- result plus every span that was exported.-traced- :: Eff '[Cache String Int, IOE] a- -> IO (a, [ImmutableSpan])-traced prog = do- (processor, ref) <- inMemoryListExporter- provider <- createTracerProvider [processor] emptyTracerProviderOptions- let tracer = makeTracer provider "effectful-data-cache-test" tracerOptions- r <- runEff . runCache @String @Int Nothing $ traceCache @String @Int tracer prog- shutdownTracerProvider provider- spans <- readIORef ref- pure (r, reverse spans)--attr- :: ImmutableSpan- -> Text- -> Maybe Attribute-attr s k = lookupAttribute (spanAttributes s) k--spec :: Spec-spec = do- it "emits a span per operation" $ do- (_, spans) <- traced $ do- insert @String @Int "a" (1 :: Int)- _ <- lookup @String @Int "a"- purge @String @Int- map spanName spans `shouldBe` ["cache.insert", "cache.lookup", "cache.purge"]-- it "records hits and misses" $ do- (_, spans) <- traced $ do- insert @String @Int "a" (1 :: Int)- _ <- lookup @String @Int "a"- _ <- lookup @String @Int "nope"- pure ()- let hits = [attr s "cache.hit" | s <- spans, spanName s == "cache.lookup"]- hits- `shouldBe` [ Just (AttributeValue (BoolAttribute True))- , Just (AttributeValue (BoolAttribute False))- ]-- it "records the size" $ do- (_, spans) <- traced $ do- insert @String @Int "a" (1 :: Int)- insert @String @Int "b" (2 :: Int)- _ <- size @String @Int- pure ()- [attr s "cache.size" | s <- spans, spanName s == "cache.size"]- `shouldBe` [Just (AttributeValue (IntAttribute 2))]-- it "still returns the underlying result" $ do- (r, _) <- traced $ do- insert @String @Int "a" (1 :: Int)- lookup @String @Int "a"- r `shouldBe` Just 1