nri-observability 0.1.1.1 → 0.1.1.2
raw patch · 9 files changed
+163/−23 lines, 9 filesdep ~aesonPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependency ranges changed: aeson
API changes (from Hackage documentation)
+ Reporter.Honeycomb: [modifyApdexTimeMs] :: Settings -> Int -> TracingSpan -> Int
+ Reporter.Honeycomb: [modifyFractionOfSuccessRequestsLogged] :: Settings -> Float -> TracingSpan -> Float
- Reporter.Honeycomb: Settings :: Secret ByteString -> Text -> Text -> Float -> Int -> Settings
+ Reporter.Honeycomb: Settings :: Secret ByteString -> Text -> Text -> Float -> Int -> (Float -> TracingSpan -> Float) -> (Int -> TracingSpan -> Int) -> Settings
Files
- CHANGELOG.md +4/−0
- nri-observability.cabal +5/−3
- src/Observability.hs +8/−5
- src/Platform/AesonHelpers.hs +52/−0
- src/Platform/ReporterHelpers.hs +7/−6
- src/Reporter/Bugsnag/Internal.hs +2/−1
- src/Reporter/Honeycomb.hs +1/−1
- src/Reporter/Honeycomb/Internal.hs +23/−7
- tests/Spec/Reporter/Honeycomb.hs +61/−0
CHANGELOG.md view
@@ -1,3 +1,7 @@+# 0.1.1.2++- Support `aeson-2.0.x`.+ # 0.1.1.1 - Add `Observability` glue module (still needs better documentation!)
nri-observability.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: nri-observability-version: 0.1.1.1+version: 0.1.1.2 synopsis: Report log spans collected by nri-prelude. description: Please see the README at <https://github.com/NoRedInk/haskell-libraries/tree/trunk/nri-observability#readme>. category: Web@@ -39,6 +39,7 @@ Reporter.Honeycomb Observability other-modules:+ Platform.AesonHelpers Platform.ReporterHelpers Platform.Timer Reporter.Bugsnag.Internal@@ -64,7 +65,7 @@ TypeOperators ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wpartial-fields -Wredundant-constraints -Wincomplete-uni-patterns -fplugin=NriPrelude.Plugin build-depends:- aeson >=1.4.6.0 && <1.6+ aeson >=1.4.6.0 && <2.1 , aeson-pretty >=0.8.0 && <0.9 , async >=2.2.2 && <2.3 , base >=4.12.0.0 && <4.16@@ -101,6 +102,7 @@ Log.RedisCommands Log.SqlQuery Observability+ Platform.AesonHelpers Platform.ReporterHelpers Platform.Timer Reporter.Bugsnag@@ -132,7 +134,7 @@ ExtendedDefaultRules ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wpartial-fields -Wredundant-constraints -Wincomplete-uni-patterns -fplugin=NriPrelude.Plugin -threaded -rtsopts "-with-rtsopts=-N -T" -fno-warn-type-defaults build-depends:- aeson >=1.4.6.0 && <1.6+ aeson >=1.4.6.0 && <2.1 , aeson-pretty >=0.8.0 && <0.9 , async >=2.2.2 && <2.3 , base >=4.12.0.0 && <4.16
src/Observability.hs view
@@ -35,11 +35,12 @@ import qualified Prelude -- | A handler for reporting to logging/monitoring/observability platforms.------ The `report` function takes a span containing all the tracing information we--- collected for a single request and then reports it to all platforms we--- enabled when creating this handler.-newtype Handler = Handler {report :: Text -> Platform.TracingSpan -> Prelude.IO ()}+newtype Handler = Handler+ { -- | `report` takes a span containing all the tracing information we+ -- collected for a single request and then reports it to all platforms we+ -- enabled when creating this handler.+ report :: Text -> Platform.TracingSpan -> Prelude.IO ()+ } deriving (Prelude.Semigroup, Prelude.Monoid) -- | Function for creating an observability handler. The settings we pass in@@ -195,6 +196,8 @@ stdout :: Settings -> File.Settings stdout settings = (file settings) {File.logFile = "/dev/stdout"} +-- | Read 'Settings' from environment variables. Default variables will be used+-- in case no environment variable is set for an option. decoder :: Environment.Decoder Settings decoder = pure Settings
+ src/Platform/AesonHelpers.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE CPP #-}++-- | This module supports working with aeson objects in a way compatible with+-- both the 1.x and 2.x versions of the aseon library.+--+-- Aeson has a Value type for representing JSON values. The Value type has+-- constructors for JSON strings, numbers, arrays, and objects. Aeson represents+-- JSON objects using an Object type, which in versions 1.x of aeson is a type+-- alias for a HashMap. Version 2.x of library make Object an opaque type.+--+-- nri-observability needs to perform some operations on JSON objects. Depending+-- on which version of aeson we got, we need to use different functions to+-- perform these operations. This module contains implementations for both 1.x+-- and 2.x versions of Aeson, and automatically picks the right version+-- depending on which version of the library is detected.+--+-- Once we're done supporting versions 1.x of aeson we can drop this module and+-- inline the 2.x versions of functions wherever they are called.+module Platform.AesonHelpers (foldObject, mergeObjects) where++#if MIN_VERSION_aeson(2,0,0)++import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Key as Key+import qualified Data.Aeson.KeyMap as KeyMap++foldObject :: (Text -> Aeson.Value -> acc -> acc) -> acc -> Aeson.Object -> acc+foldObject fn = KeyMap.foldrWithKey (\key val acc -> fn (Key.toText key) val acc)++mergeObjects ::+ (Aeson.Value -> Aeson.Value -> Aeson.Value) ->+ Aeson.Object ->+ Aeson.Object ->+ Aeson.Object+mergeObjects = KeyMap.unionWith++#else++import qualified Data.Aeson as Aeson+import qualified Data.HashMap.Strict as HashMap++foldObject :: (Text -> Aeson.Value -> acc -> acc) -> acc -> Aeson.Object -> acc+foldObject = HashMap.foldrWithKey++mergeObjects ::+ (Aeson.Value -> Aeson.Value -> Aeson.Value) ->+ Aeson.Object ->+ Aeson.Object ->+ Aeson.Object+mergeObjects merge object1 object2 = HashMap.unionWith merge object1 object2++#endif
src/Platform/ReporterHelpers.hs view
@@ -5,6 +5,7 @@ import qualified Data.HashMap.Strict as HashMap import qualified GHC.Stack as Stack import qualified List+import qualified Platform.AesonHelpers as AesonHelpers import qualified Text import qualified Prelude @@ -30,19 +31,19 @@ toHashMap :: Aeson.ToJSON a => a -> HashMap.HashMap Text Text toHashMap x = case Aeson.toJSON x of- Aeson.Object dict ->- HashMap.foldlWithKey'- (\acc key value -> acc ++ jsonAsText key value)+ Aeson.Object object ->+ AesonHelpers.foldObject+ (\key value acc -> jsonAsText key value ++ acc) HashMap.empty- dict+ object val -> jsonAsText "value" val jsonAsText :: Text -> Aeson.Value -> HashMap.HashMap Text Text jsonAsText key val = case val of Aeson.Object dict ->- HashMap.foldlWithKey'- (\acc key2 value -> acc ++ jsonAsText (key ++ "." ++ key2) value)+ AesonHelpers.foldObject+ (\key2 value acc -> jsonAsText (key ++ "." ++ key2) value ++ acc) HashMap.empty dict Aeson.Array vals ->
src/Reporter/Bugsnag/Internal.hs view
@@ -24,6 +24,7 @@ import qualified Network.HTTP.Client.TLS as HTTP.TLS import qualified Network.HostName import qualified Platform+import qualified Platform.AesonHelpers as AesonHelpers import qualified Platform.ReporterHelpers as Helpers import qualified Platform.Timer as Timer import qualified Prelude@@ -380,7 +381,7 @@ } mergeJson :: Aeson.Value -> Aeson.Value -> Aeson.Value-mergeJson (Aeson.Object x) (Aeson.Object y) = Aeson.Object (HashMap.unionWith mergeJson x y)+mergeJson (Aeson.Object x) (Aeson.Object y) = Aeson.Object (AesonHelpers.mergeObjects mergeJson x y) mergeJson _ last = last mergeMetaData ::
src/Reporter/Honeycomb.hs view
@@ -3,7 +3,7 @@ ( Internal.report, Internal.handler, Internal.Handler,- Internal.Settings(..),+ Internal.Settings (..), Internal.decoder, ) where
src/Reporter/Honeycomb/Internal.hs view
@@ -5,7 +5,6 @@ import qualified Control.Exception.Safe as Exception import qualified Data.Aeson as Aeson import qualified Data.ByteString as ByteString-import qualified Data.HashMap.Strict as HashMap import qualified Data.List import qualified Data.List.NonEmpty as NonEmpty import qualified Data.Text.Encoding as Encoding@@ -27,6 +26,7 @@ import qualified Network.HTTP.Client.TLS as HTTP.TLS import qualified Network.HostName import qualified Platform+import qualified Platform.AesonHelpers as AesonHelpers import qualified Platform.Timer as Timer import qualified System.Random as Random import qualified Text@@ -99,13 +99,15 @@ -- healthcheck endpoints don't populate `HttpRequest.endpoint`. -- Fix that first before trying this. Just requestPath -> List.any (requestPath ==) ["/health/readiness", "/metrics", "/health/liveness"]- baseRate = fractionOfSuccessRequestsLogged settings+ baseRate = modifyFractionOfSuccessRequestsLogged settings (fractionOfSuccessRequestsLogged settings) rootSpan requestDurationMs = Timer.difference (Platform.started rootSpan) (Platform.finished rootSpan) |> Platform.inMicroseconds |> Prelude.fromIntegral |> (*) 1e-3- apdexTMs = Prelude.fromIntegral (apdexTimeMs settings)+ apdexTMs =+ modifyApdexTimeMs settings (apdexTimeMs settings) rootSpan+ |> Prelude.fromIntegral in if isNonAppRequestPath then -- -- We have 2678400 seconds in a month@@ -332,11 +334,11 @@ renderDetailsGeneric :: Text -> Platform.SomeTracingSpanDetails -> Span -> Span renderDetailsGeneric keyPrefix details honeycombSpan = case Aeson.toJSON details of- Aeson.Object hashMap ->- HashMap.foldrWithKey+ Aeson.Object object ->+ AesonHelpers.foldObject (\key value -> addField (useAsKey (keyPrefix ++ "." ++ key)) value) honeycombSpan- hashMap+ object jsonVal -> addField keyPrefix jsonVal honeycombSpan -- LogContext is an unbounded list of key value pairs with possibly nested@@ -571,7 +573,19 @@ -- -- [@environment variable@] HONEYCOMB_APDEX_TIME_IN_MILLISECONDS -- [@default value@] 100- apdexTimeMs :: Int+ apdexTimeMs :: Int,+ -- | Allows overriding the default sample rates for given spans.+ -- This allows us to change the sample rate for certain endpoints within an+ -- application, for example if a path is critical but low volume we may choose+ -- to increase the rate.+ -- [@default value@] the input float+ modifyFractionOfSuccessRequestsLogged :: Float -> Platform.TracingSpan -> Float,+ -- | Allows overriding the default apdex rates for given spans.+ -- This allows us to change the apdex for certain endpoints within an+ -- application, for example if a path is significantly lower volume than+ -- another the apdex may require tuning.+ -- [@default value@] the input int+ modifyApdexTimeMs :: Int -> Platform.TracingSpan -> Int } -- | Read 'Settings' from environment variables. Default variables will be used@@ -584,6 +598,8 @@ |> andMap honeycombAppNameDecoder |> andMap fractionOfSuccessRequestsLoggedDecoder |> andMap apdexTimeMsDecoder+ |> andMap (Prelude.pure always)+ |> andMap (Prelude.pure always) honeycombApiKeyDecoder :: Environment.Decoder (Log.Secret ByteString.ByteString) honeycombApiKeyDecoder =
tests/Spec/Reporter/Honeycomb.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE NumericUnderscores #-}+ module Spec.Reporter.Honeycomb (tests) where import qualified Control.Exception.Safe as Exception@@ -20,6 +22,7 @@ import qualified Platform.Timer as Timer import qualified Reporter.Honeycomb.Internal as Honeycomb import Test (Test, describe, test)+import qualified Prelude tests :: Test tests =@@ -315,6 +318,52 @@ |> (:) ("apdex T: " ++ Text.fromFloat apdexTMs ++ "ms") |> Text.join "\n" |> Expect.equalToContentsOf "test/golden-results/observability-spec-honeycomb-sampling"+ ],+ describe+ "deriveSampleRate"+ [ test "modifies configured sample rate by route" <| \_ -> do+ let matchingSpan =+ emptyTracingSpan+ { Platform.name = "matching-span"+ }+ let customRate = 1.0+ let defaultRate = 0.1+ let settings =+ emptySettings+ { Honeycomb.fractionOfSuccessRequestsLogged = defaultRate,+ Honeycomb.modifyFractionOfSuccessRequestsLogged =+ \rate span ->+ case Platform.name span of+ "matching-span" -> customRate+ _ -> rate+ }+ Honeycomb.deriveSampleRate matchingSpan settings+ |> Expect.within (Expect.Absolute 0.001) customRate++ Honeycomb.deriveSampleRate emptyTracingSpan settings+ |> Expect.within (Expect.Absolute 0.001) defaultRate,+ test "modifies configured apdex by route" <| \_ -> do+ let matchingSpan =+ emptyTracingSpan+ { Platform.name = "matching-span",+ Platform.started = 0,+ Platform.finished = 1000+ }+ let settings =+ emptySettings+ { Honeycomb.fractionOfSuccessRequestsLogged = 0.1,+ Honeycomb.apdexTimeMs = Prelude.maxBound,+ Honeycomb.modifyApdexTimeMs =+ \defaultApdex span ->+ case Platform.name span of+ "matching-span" -> 10+ _ -> defaultApdex+ }+ Honeycomb.deriveSampleRate matchingSpan settings+ |> Expect.notWithin (Expect.Absolute 0.001) 0.1++ Honeycomb.deriveSampleRate emptyTracingSpan settings+ |> Expect.within (Expect.Absolute 0.001) 0.1 ] ] @@ -341,6 +390,18 @@ Platform.succeeded = Platform.Succeeded, Platform.allocated = 0, Platform.children = []+ }++emptySettings :: Honeycomb.Settings+emptySettings =+ Honeycomb.Settings+ { Honeycomb.apiKey = Log.mkSecret "api-key",+ Honeycomb.datasetName = "dataset",+ Honeycomb.serviceName = "service",+ Honeycomb.fractionOfSuccessRequestsLogged = 0.0,+ Honeycomb.apdexTimeMs = 10,+ Honeycomb.modifyFractionOfSuccessRequestsLogged = always,+ Honeycomb.modifyApdexTimeMs = always } toBatchEvents :: Platform.TracingSpan -> [Honeycomb.BatchEvent]