From bfa78271cea3cfe6ad23279ee975b1a9369e3663 Mon Sep 17 00:00:00 2001 From: evanlauer1 Date: Wed, 12 Jun 2024 12:49:12 -0700 Subject: [PATCH 1/8] Refactored LogRecords to be mutable so that attributes can be modified after creation --- api/hs-opentelemetry-api.cabal | 1 + .../OpenTelemetry/Internal/Logging/Core.hs | 96 ++++++++++++++-- .../OpenTelemetry/Internal/Logging/Types.hs | 15 ++- api/src/OpenTelemetry/LogAttributes.hs | 6 +- api/src/OpenTelemetry/Logging/Core.hs | 3 + api/src/OpenTelemetry/Resource.hs | 1 + api/test/OpenTelemetry/Logging/CoreSpec.hs | 107 ++++++++++++++++++ api/test/Spec.hs | 2 + 8 files changed, 218 insertions(+), 13 deletions(-) create mode 100644 api/test/OpenTelemetry/Logging/CoreSpec.hs diff --git a/api/hs-opentelemetry-api.cabal b/api/hs-opentelemetry-api.cabal index 41a379fc..408a87ef 100644 --- a/api/hs-opentelemetry-api.cabal +++ b/api/hs-opentelemetry-api.cabal @@ -106,6 +106,7 @@ test-suite hs-opentelemetry-api-test main-is: Spec.hs other-modules: OpenTelemetry.BaggageSpec + OpenTelemetry.Logging.CoreSpec OpenTelemetry.SemanticsConfigSpec OpenTelemetry.Trace.SamplerSpec OpenTelemetry.Trace.TraceFlagsSpec diff --git a/api/src/OpenTelemetry/Internal/Logging/Core.hs b/api/src/OpenTelemetry/Internal/Logging/Core.hs index 0a16ddb0..95459995 100644 --- a/api/src/OpenTelemetry/Internal/Logging/Core.hs +++ b/api/src/OpenTelemetry/Internal/Logging/Core.hs @@ -11,6 +11,9 @@ module OpenTelemetry.Internal.Logging.Core ( emitLogRecord, logDroppedAttributes, emitOTelLogRecord, + addAttribute, + addAttributes, + logRecordGetAttributes, ) where import Control.Applicative @@ -18,6 +21,7 @@ import Control.Monad (void, when) import Control.Monad.Trans import Control.Monad.Trans.Maybe import Data.Coerce +import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as H import Data.IORef import Data.Maybe @@ -32,6 +36,7 @@ import OpenTelemetry.Context.ThreadLocal import OpenTelemetry.Internal.Common.Types import OpenTelemetry.Internal.Logging.Types import OpenTelemetry.Internal.Trace.Types (SpanContext (..), getSpanContext) +import OpenTelemetry.LogAttributes (LogAttributes, ToValue) import qualified OpenTelemetry.LogAttributes as LA import OpenTelemetry.Resource (MaterializedResources, emptyMaterializedResources) import Paths_hs_opentelemetry_api (version) @@ -100,16 +105,12 @@ makeLogger makeLogger loggerProvider loggerInstrumentationScope = Logger {..} -{- | Emits a LogRecord with properties specified by the passed in Logger and LogRecordArguments. -If observedTimestamp is not set in LogRecordArguments, it will default to the current timestamp. -If context is not specified in LogRecordArguments it will default to the current context. --} -emitLogRecord +createImmutableLogRecord :: (MonadIO m) => Logger -> LogRecordArguments body - -> m (LogRecord body) -emitLogRecord Logger {..} LogRecordArguments {..} = do + -> m (ImmutableLogRecord body) +createImmutableLogRecord logger@Logger {..} LogRecordArguments {..} = do currentTimestamp <- getCurrentTimestamp let logRecordObservedTimestamp = fromMaybe currentTimestamp observedTimestamp @@ -128,7 +129,7 @@ emitLogRecord Logger {..} LogRecordArguments {..} = do when (LA.attributesDropped logRecordAttributes > 0) $ void logDroppedAttributes pure - LogRecord + ImmutableLogRecord { logRecordTimestamp = timestamp , logRecordObservedTimestamp , logRecordTracingDetails @@ -138,6 +139,7 @@ emitLogRecord Logger {..} LogRecordArguments {..} = do , logRecordResource = loggerProviderResource loggerProvider , logRecordInstrumentationScope = loggerInstrumentationScope , logRecordAttributes + , logRecordLogger = logger } @@ -164,3 +166,81 @@ emitOTelLogRecord attrs severity body = do { severityNumber = Just severity , attributes = attrs } + + +{- | Emits a LogRecord with properties specified by the passed in Logger and LogRecordArguments. +If observedTimestamp is not set in LogRecordArguments, it will default to the current timestamp. +If context is not specified in LogRecordArguments it will default to the current context. +-} +emitLogRecord + :: (MonadIO m) + => Logger + -> LogRecordArguments body + -> m (LogRecord body) +emitLogRecord l args = do + ilr <- createImmutableLogRecord l args + lr <- liftIO $ newIORef ilr + pure $ LogRecord lr + + +{- | Add an attribute to a @LogRecord@. + +As an application developer when you need to record an attribute first consult existing semantic conventions for Resources, Spans, and Metrics. If an appropriate name does not exists you will need to come up with a new name. To do that consider a few options: + +The name is specific to your company and may be possibly used outside the company as well. To avoid clashes with names introduced by other companies (in a distributed system that uses applications from multiple vendors) it is recommended to prefix the new name by your company’s reverse domain name, e.g. 'com.acme.shopname'. + +The name is specific to your application that will be used internally only. If you already have an internal company process that helps you to ensure no name clashes happen then feel free to follow it. Otherwise it is recommended to prefix the attribute name by your application name, provided that the application name is reasonably unique within your organization (e.g. 'myuniquemapapp.longitude' is likely fine). Make sure the application name does not clash with an existing semantic convention namespace. + +The name may be generally applicable to applications in the industry. In that case consider submitting a proposal to this specification to add a new name to the semantic conventions, and if necessary also to add a new namespace. + +It is recommended to limit names to printable Basic Latin characters (more precisely to 'U+0021' .. 'U+007E' subset of Unicode code points), although the Haskell OpenTelemetry specification DOES provide full Unicode support. + +Attribute names that start with 'otel.' are reserved to be defined by OpenTelemetry specification. These are typically used to express OpenTelemetry concepts in formats that don’t have a corresponding concept. + +For example, the 'otel.library.name' attribute is used to record the instrumentation library name, which is an OpenTelemetry concept that is natively represented in OTLP, but does not have an equivalent in other telemetry formats and protocols. + +Any additions to the 'otel.*' namespace MUST be approved as part of OpenTelemetry specification. +-} +addAttribute :: (MonadIO m, ToValue a) => LogRecord body -> Text -> a -> m () +addAttribute (LogRecord lr) k v = + liftIO $ + modifyIORef' + lr + ( \ilr@ImmutableLogRecord {logRecordAttributes, logRecordLogger} -> + ilr + { logRecordAttributes = + LA.addAttribute + (loggerProviderAttributeLimits $ loggerProvider logRecordLogger) + logRecordAttributes + k + v + } + ) + + +{- | A convenience function related to 'addAttribute' that adds multiple attributes to a @LogRecord@ at the same time. + + This function may be slightly more performant than repeatedly calling 'addAttribute'. +-} +addAttributes :: (MonadIO m, ToValue a) => LogRecord body -> HashMap Text a -> m () +addAttributes (LogRecord lr) attrs = + liftIO $ + modifyIORef' + lr + ( \ilr@ImmutableLogRecord {logRecordAttributes, logRecordLogger} -> + ilr + { logRecordAttributes = + LA.addAttributes + (loggerProviderAttributeLimits $ loggerProvider logRecordLogger) + logRecordAttributes + attrs + } + ) + + +{- | This can be useful for pulling data for attributes and + using it to copy / otherwise use the data to further enrich + instrumentation. +-} +logRecordGetAttributes :: (MonadIO m) => LogRecord a -> m LogAttributes +logRecordGetAttributes (LogRecord lr) = liftIO $ logRecordAttributes <$> readIORef lr diff --git a/api/src/OpenTelemetry/Internal/Logging/Types.hs b/api/src/OpenTelemetry/Internal/Logging/Types.hs index fdc252b4..f7af1550 100644 --- a/api/src/OpenTelemetry/Internal/Logging/Types.hs +++ b/api/src/OpenTelemetry/Internal/Logging/Types.hs @@ -5,6 +5,7 @@ module OpenTelemetry.Internal.Logging.Types ( LoggerProvider (..), Logger (..), LogRecord (..), + ImmutableLogRecord (..), LogRecordArguments (..), emptyLogRecordArguments, SeverityNumber (..), @@ -13,13 +14,14 @@ module OpenTelemetry.Internal.Logging.Types ( import Data.Function (on) import qualified Data.HashMap.Strict as H +import Data.IORef (IORef) +import Data.Int (Int64) import Data.Text (Text) -import OpenTelemetry.Attributes (AttributeLimits) import OpenTelemetry.Common (Timestamp, TraceFlags) -import OpenTelemetry.Context.Types +import OpenTelemetry.Context.Types (Context) import OpenTelemetry.Internal.Common.Types (InstrumentationLibrary) import OpenTelemetry.Internal.Trace.Id (SpanId, TraceId) -import OpenTelemetry.LogAttributes (AnyValue, LogAttributes) +import OpenTelemetry.LogAttributes (AnyValue, AttributeLimits, LogAttributes) import OpenTelemetry.Resource (MaterializedResources) @@ -28,6 +30,7 @@ data LoggerProvider = LoggerProvider { loggerProviderResource :: MaterializedResources , loggerProviderAttributeLimits :: AttributeLimits } + deriving (Show, Eq) {- | @LogRecords@ can be created from @Loggers@. @Logger@s are uniquely identified by the @libraryName@, @libraryVersion@, @schemaUrl@ fields of @InstrumentationLibrary@. @@ -44,7 +47,10 @@ data Logger = Logger {- | This is a data type that can represent logs from various sources: application log files, machine generated events, system logs, etc. [Specification outlined here.](https://opentelemetry.io/docs/specs/otel/logs/data-model/) Existing log formats can be unambiguously mapped to this data type. Reverse mapping from this data type is also possible to the extent that the target log format has equivalent capabilities. -} -data LogRecord body = LogRecord +data LogRecord a = LogRecord (IORef (ImmutableLogRecord a)) + + +data ImmutableLogRecord body = ImmutableLogRecord { logRecordTimestamp :: Maybe Timestamp -- ^ Time when the event occurred measured by the origin clock. This field is optional, it may be missing if the timestamp is unknown. , logRecordObservedTimestamp :: Timestamp @@ -113,6 +119,7 @@ data LogRecord body = LogRecord -- ^ Additional information about the specific event occurrence. Unlike the Resource field, which is fixed for a particular source, Attributes can vary for each occurrence of the event coming from the same source. -- Can contain information about the request context (other than Trace Context Fields). The log attribute model MUST support any type, a superset of standard Attribute, to preserve the semantics of structured attributes -- emitted by the applications. This field is optional. + , logRecordLogger :: Logger } deriving (Functor) diff --git a/api/src/OpenTelemetry/LogAttributes.hs b/api/src/OpenTelemetry/LogAttributes.hs index 8148837d..62055cab 100644 --- a/api/src/OpenTelemetry/LogAttributes.hs +++ b/api/src/OpenTelemetry/LogAttributes.hs @@ -16,6 +16,10 @@ module OpenTelemetry.LogAttributes ( AnyValue (..), ToValue (..), + -- * Attribute limits + AttributeLimits (..), + defaultAttributeLimits, + -- * unsafe utilities unsafeLogAttributesFromListIgnoringLimits, unsafeMergeLogAttributesIgnoringLimits, @@ -30,7 +34,7 @@ import Data.String (IsString (..)) import Data.Text (Text) import qualified Data.Text as T import GHC.Generics (Generic) -import OpenTelemetry.Attributes (AttributeLimits (..)) +import OpenTelemetry.Attributes (AttributeLimits (..), defaultAttributeLimits) data LogAttributes = LogAttributes diff --git a/api/src/OpenTelemetry/Logging/Core.hs b/api/src/OpenTelemetry/Logging/Core.hs index e8298edb..f0060225 100644 --- a/api/src/OpenTelemetry/Logging/Core.hs +++ b/api/src/OpenTelemetry/Logging/Core.hs @@ -18,6 +18,9 @@ module OpenTelemetry.Logging.Core ( SeverityNumber (..), toShortName, emitLogRecord, + addAttribute, + addAttributes, + logRecordGetAttributes, ) where import OpenTelemetry.Internal.Common.Types diff --git a/api/src/OpenTelemetry/Resource.hs b/api/src/OpenTelemetry/Resource.hs index e2fb01e7..1ea4d5f4 100644 --- a/api/src/OpenTelemetry/Resource.hs +++ b/api/src/OpenTelemetry/Resource.hs @@ -176,6 +176,7 @@ data MaterializedResources = MaterializedResources { materializedResourcesSchema :: Maybe String , materializedResourcesAttributes :: Attributes } + deriving (Show, Eq) {- | A placeholder for 'MaterializedResources' when no resource information is diff --git a/api/test/OpenTelemetry/Logging/CoreSpec.hs b/api/test/OpenTelemetry/Logging/CoreSpec.hs new file mode 100644 index 00000000..34393cf9 --- /dev/null +++ b/api/test/OpenTelemetry/Logging/CoreSpec.hs @@ -0,0 +1,107 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} + +module OpenTelemetry.Logging.CoreSpec where + +import qualified Data.HashMap.Strict as H +import Data.IORef +import qualified OpenTelemetry.Attributes as A +import OpenTelemetry.Internal.Logging.Types +import qualified OpenTelemetry.LogAttributes as LA +import OpenTelemetry.Logging.Core +import OpenTelemetry.Resource +import OpenTelemetry.Resource.OperatingSystem +import Test.Hspec + + +spec :: Spec +spec = describe "Core" $ do + describe "getGlobalLoggerProvider" $ do + it "Returns a no-op LoggerProvider when not initialized" $ do + LoggerProvider {..} <- getGlobalLoggerProvider + loggerProviderResource `shouldBe` emptyMaterializedResources + loggerProviderAttributeLimits `shouldBe` LA.defaultAttributeLimits + describe "setGlobalLoggerProvider" $ do + it "works" $ do + lp <- + createLoggerProvider $ + LoggerProviderOptions + { loggerProviderOptionsResource = + materializeResources $ + toResource + OperatingSystem + { osType = "exampleOs" + , osDescription = Nothing + , osName = Nothing + , osVersion = Nothing + } + , loggerProviderOptionsAttributeLimits = + LA.AttributeLimits + { attributeCountLimit = Just 50 + , attributeLengthLimit = Just 50 + } + } + setGlobalLoggerProvider lp + glp <- getGlobalLoggerProvider + glp `shouldBe` lp + describe "addAttribute" $ do + it "works" $ do + lp <- getGlobalLoggerProvider + let l = makeLogger lp InstrumentationLibrary {libraryName = "exampleLibrary", libraryVersion = "", librarySchemaUrl = "", libraryAttributes = A.emptyAttributes} + lr <- + emitLogRecord + l + LogRecordArguments + { timestamp = Nothing + , observedTimestamp = Nothing + , context = Nothing + , severityText = Nothing + , severityNumber = Nothing + , body = Nothing + , attributes = + H.fromList + [ ("something", "a thing") + ] + } + addAttribute lr "anotherThing" ("another thing" :: LA.AnyValue) + + (_, attrs) <- LA.getAttributes <$> logRecordGetAttributes lr + + attrs + `shouldBe` H.fromList + [ ("anotherThing", "another thing") + , ("something", "a thing") + ] + describe "addAttributes" $ do + it "works" $ do + lp <- getGlobalLoggerProvider + let l = makeLogger lp InstrumentationLibrary {libraryName = "exampleLibrary", libraryVersion = "", librarySchemaUrl = "", libraryAttributes = A.emptyAttributes} + lr <- + emitLogRecord + l + LogRecordArguments + { timestamp = Nothing + , observedTimestamp = Nothing + , context = Nothing + , severityText = Nothing + , severityNumber = Nothing + , body = Nothing + , attributes = + H.fromList + [ ("something", "a thing") + ] + } + addAttributes lr $ + H.fromList + [ ("anotherThing", "another thing" :: LA.AnyValue) + , ("twoThing", "the second another thing") + ] + + (_, attrs) <- LA.getAttributes <$> logRecordGetAttributes lr + + attrs + `shouldBe` H.fromList + [ ("anotherThing", "another thing") + , ("something", "a thing") + , ("twoThing", "the second another thing") + ] diff --git a/api/test/Spec.hs b/api/test/Spec.hs index ba2bb467..949318b7 100644 --- a/api/test/Spec.hs +++ b/api/test/Spec.hs @@ -13,6 +13,7 @@ import OpenTelemetry.Attributes (lookupAttribute) import qualified OpenTelemetry.BaggageSpec as Baggage import OpenTelemetry.Context +import qualified OpenTelemetry.Logging.CoreSpec as CoreSpec import qualified OpenTelemetry.SemanticsConfigSpec as SemanticsConfigSpec import OpenTelemetry.Trace.Core import qualified OpenTelemetry.Trace.SamplerSpec as Sampler @@ -56,3 +57,4 @@ main = hspec $ do Sampler.spec TraceFlags.spec SemanticsConfigSpec.spec + CoreSpec.spec From 662979b697fa233d1547b91b32207e5468d4c268 Mon Sep 17 00:00:00 2001 From: evanlauer1 Date: Fri, 14 Jun 2024 17:25:02 -0700 Subject: [PATCH 2/8] Updated test suite --- .../OpenTelemetry/Internal/Logging/Types.hs | 2 +- api/test/OpenTelemetry/Logging/CoreSpec.hs | 77 ++++++------------- 2 files changed, 26 insertions(+), 53 deletions(-) diff --git a/api/src/OpenTelemetry/Internal/Logging/Types.hs b/api/src/OpenTelemetry/Internal/Logging/Types.hs index f7af1550..ce913702 100644 --- a/api/src/OpenTelemetry/Internal/Logging/Types.hs +++ b/api/src/OpenTelemetry/Internal/Logging/Types.hs @@ -21,7 +21,7 @@ import OpenTelemetry.Common (Timestamp, TraceFlags) import OpenTelemetry.Context.Types (Context) import OpenTelemetry.Internal.Common.Types (InstrumentationLibrary) import OpenTelemetry.Internal.Trace.Id (SpanId, TraceId) -import OpenTelemetry.LogAttributes (AnyValue, AttributeLimits, LogAttributes) +import OpenTelemetry.LogAttributes import OpenTelemetry.Resource (MaterializedResources) diff --git a/api/test/OpenTelemetry/Logging/CoreSpec.hs b/api/test/OpenTelemetry/Logging/CoreSpec.hs index 34393cf9..fbdff367 100644 --- a/api/test/OpenTelemetry/Logging/CoreSpec.hs +++ b/api/test/OpenTelemetry/Logging/CoreSpec.hs @@ -16,57 +16,44 @@ import Test.Hspec spec :: Spec spec = describe "Core" $ do - describe "getGlobalLoggerProvider" $ do + describe "The global logger provider" $ do it "Returns a no-op LoggerProvider when not initialized" $ do LoggerProvider {..} <- getGlobalLoggerProvider loggerProviderResource `shouldBe` emptyMaterializedResources loggerProviderAttributeLimits `shouldBe` LA.defaultAttributeLimits - describe "setGlobalLoggerProvider" $ do - it "works" $ do - lp <- - createLoggerProvider $ - LoggerProviderOptions - { loggerProviderOptionsResource = - materializeResources $ - toResource - OperatingSystem - { osType = "exampleOs" - , osDescription = Nothing - , osName = Nothing - , osVersion = Nothing + it "Allows a LoggerProvider to be set and returns that with subsequent calls to getGlobalLoggerProvider" $ do + let lp = + createLoggerProvider $ + LoggerProviderOptions + { loggerProviderOptionsResource = + materializeResources $ + toResource + OperatingSystem + { osType = "exampleOs" + , osDescription = Nothing + , osName = Nothing + , osVersion = Nothing + } + , loggerProviderOptionsAttributeLimits = + LA.AttributeLimits + { attributeCountLimit = Just 50 + , attributeLengthLimit = Just 50 } - , loggerProviderOptionsAttributeLimits = - LA.AttributeLimits - { attributeCountLimit = Just 50 - , attributeLengthLimit = Just 50 - } - } + } + setGlobalLoggerProvider lp + glp <- getGlobalLoggerProvider glp `shouldBe` lp describe "addAttribute" $ do it "works" $ do lp <- getGlobalLoggerProvider let l = makeLogger lp InstrumentationLibrary {libraryName = "exampleLibrary", libraryVersion = "", librarySchemaUrl = "", libraryAttributes = A.emptyAttributes} - lr <- - emitLogRecord - l - LogRecordArguments - { timestamp = Nothing - , observedTimestamp = Nothing - , context = Nothing - , severityText = Nothing - , severityNumber = Nothing - , body = Nothing - , attributes = - H.fromList - [ ("something", "a thing") - ] - } + lr <- emitLogRecord l $ (emptyLogRecordArguments ()) {attributes = H.fromList [("something", "a thing")]} + addAttribute lr "anotherThing" ("another thing" :: LA.AnyValue) (_, attrs) <- LA.getAttributes <$> logRecordGetAttributes lr - attrs `shouldBe` H.fromList [ ("anotherThing", "another thing") @@ -76,21 +63,8 @@ spec = describe "Core" $ do it "works" $ do lp <- getGlobalLoggerProvider let l = makeLogger lp InstrumentationLibrary {libraryName = "exampleLibrary", libraryVersion = "", librarySchemaUrl = "", libraryAttributes = A.emptyAttributes} - lr <- - emitLogRecord - l - LogRecordArguments - { timestamp = Nothing - , observedTimestamp = Nothing - , context = Nothing - , severityText = Nothing - , severityNumber = Nothing - , body = Nothing - , attributes = - H.fromList - [ ("something", "a thing") - ] - } + lr <- emitLogRecord l $ (emptyLogRecordArguments ()) {attributes = H.fromList [("something", "a thing")]} + addAttributes lr $ H.fromList [ ("anotherThing", "another thing" :: LA.AnyValue) @@ -98,7 +72,6 @@ spec = describe "Core" $ do ] (_, attrs) <- LA.getAttributes <$> logRecordGetAttributes lr - attrs `shouldBe` H.fromList [ ("anotherThing", "another thing") From f6299502bc2014bedfd7a550db2acfdce1de6766 Mon Sep 17 00:00:00 2001 From: evanlauer1 Date: Tue, 2 Jul 2024 17:32:17 -0700 Subject: [PATCH 3/8] Added ReadableLogRecord and ReadWriteLogRecord interfaces --- .../OpenTelemetry/Internal/Logging/Core.hs | 19 ++++++------ .../OpenTelemetry/Internal/Logging/Types.hs | 31 +++++++++++++++++-- api/src/OpenTelemetry/Logging/Core.hs | 4 ++- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/api/src/OpenTelemetry/Internal/Logging/Core.hs b/api/src/OpenTelemetry/Internal/Logging/Core.hs index 95459995..a8e456de 100644 --- a/api/src/OpenTelemetry/Internal/Logging/Core.hs +++ b/api/src/OpenTelemetry/Internal/Logging/Core.hs @@ -179,8 +179,7 @@ emitLogRecord -> m (LogRecord body) emitLogRecord l args = do ilr <- createImmutableLogRecord l args - lr <- liftIO $ newIORef ilr - pure $ LogRecord lr + liftIO $ mkLogRecord ilr {- | Add an attribute to a @LogRecord@. @@ -201,10 +200,10 @@ For example, the 'otel.library.name' attribute is used to record the instrumenta Any additions to the 'otel.*' namespace MUST be approved as part of OpenTelemetry specification. -} -addAttribute :: (MonadIO m, ToValue a) => LogRecord body -> Text -> a -> m () -addAttribute (LogRecord lr) k v = +addAttribute :: (ReadWriteLogRecord r, MonadIO m, ToValue a) => r body -> Text -> a -> m () +addAttribute lr k v = liftIO $ - modifyIORef' + modifyLogRecord lr ( \ilr@ImmutableLogRecord {logRecordAttributes, logRecordLogger} -> ilr @@ -222,10 +221,10 @@ addAttribute (LogRecord lr) k v = This function may be slightly more performant than repeatedly calling 'addAttribute'. -} -addAttributes :: (MonadIO m, ToValue a) => LogRecord body -> HashMap Text a -> m () -addAttributes (LogRecord lr) attrs = +addAttributes :: (ReadWriteLogRecord r, MonadIO m, ToValue a) => r body -> HashMap Text a -> m () +addAttributes lr attrs = liftIO $ - modifyIORef' + modifyLogRecord lr ( \ilr@ImmutableLogRecord {logRecordAttributes, logRecordLogger} -> ilr @@ -242,5 +241,5 @@ addAttributes (LogRecord lr) attrs = using it to copy / otherwise use the data to further enrich instrumentation. -} -logRecordGetAttributes :: (MonadIO m) => LogRecord a -> m LogAttributes -logRecordGetAttributes (LogRecord lr) = liftIO $ logRecordAttributes <$> readIORef lr +logRecordGetAttributes :: (ReadableLogRecord r, MonadIO m) => r a -> m LogAttributes +logRecordGetAttributes lr = liftIO $ logRecordAttributes <$> readLogRecord lr diff --git a/api/src/OpenTelemetry/Internal/Logging/Types.hs b/api/src/OpenTelemetry/Internal/Logging/Types.hs index ce913702..b6f3d4c3 100644 --- a/api/src/OpenTelemetry/Internal/Logging/Types.hs +++ b/api/src/OpenTelemetry/Internal/Logging/Types.hs @@ -4,7 +4,10 @@ module OpenTelemetry.Internal.Logging.Types ( LoggerProvider (..), Logger (..), - LogRecord (..), + LogRecord, + mkLogRecord, + ReadableLogRecord (..), + ReadWriteLogRecord (..), ImmutableLogRecord (..), LogRecordArguments (..), emptyLogRecordArguments, @@ -14,7 +17,7 @@ module OpenTelemetry.Internal.Logging.Types ( import Data.Function (on) import qualified Data.HashMap.Strict as H -import Data.IORef (IORef) +import Data.IORef (IORef, atomicModifyIORef, modifyIORef, newIORef, readIORef) import Data.Int (Int64) import Data.Text (Text) import OpenTelemetry.Common (Timestamp, TraceFlags) @@ -47,7 +50,29 @@ data Logger = Logger {- | This is a data type that can represent logs from various sources: application log files, machine generated events, system logs, etc. [Specification outlined here.](https://opentelemetry.io/docs/specs/otel/logs/data-model/) Existing log formats can be unambiguously mapped to this data type. Reverse mapping from this data type is also possible to the extent that the target log format has equivalent capabilities. -} -data LogRecord a = LogRecord (IORef (ImmutableLogRecord a)) +newtype LogRecord a = LogRecord (IORef (ImmutableLogRecord a)) + + +mkLogRecord :: ImmutableLogRecord body -> IO (LogRecord body) +mkLogRecord = fmap LogRecord . newIORef + + +class ReadableLogRecord r where + readLogRecord :: r a -> IO (ImmutableLogRecord a) + + +class (ReadableLogRecord r) => ReadWriteLogRecord r where + modifyLogRecord :: r a -> (ImmutableLogRecord a -> ImmutableLogRecord a) -> IO () + atomicModifyLogRecord :: r a -> (ImmutableLogRecord a -> (ImmutableLogRecord a, b)) -> IO b + + +instance ReadableLogRecord LogRecord where + readLogRecord (LogRecord ref) = readIORef ref + + +instance ReadWriteLogRecord LogRecord where + modifyLogRecord (LogRecord ref) = modifyIORef ref + atomicModifyLogRecord (LogRecord ref) = atomicModifyIORef ref data ImmutableLogRecord body = ImmutableLogRecord diff --git a/api/src/OpenTelemetry/Logging/Core.hs b/api/src/OpenTelemetry/Logging/Core.hs index f0060225..14143a17 100644 --- a/api/src/OpenTelemetry/Logging/Core.hs +++ b/api/src/OpenTelemetry/Logging/Core.hs @@ -13,7 +13,9 @@ module OpenTelemetry.Logging.Core ( makeLogger, -- * @LogRecord@ operations - LogRecord (..), + LogRecord, + ReadableLogRecord (..), + ReadWriteLogRecord (..), LogRecordArguments (..), SeverityNumber (..), toShortName, From e60ddbfe07c70033e9a30796b0e8ecc50faf424c Mon Sep 17 00:00:00 2001 From: evanlauer1 Date: Wed, 3 Jul 2024 10:29:07 -0700 Subject: [PATCH 4/8] Added typeclasses for ReadableLogRecords and ReadWriteLogRecords --- .../OpenTelemetry/Internal/Logging/Core.hs | 75 ++++++++++--------- .../OpenTelemetry/Internal/Logging/Types.hs | 68 +++++++++++++---- 2 files changed, 93 insertions(+), 50 deletions(-) diff --git a/api/src/OpenTelemetry/Internal/Logging/Core.hs b/api/src/OpenTelemetry/Internal/Logging/Core.hs index a8e456de..5f89df5c 100644 --- a/api/src/OpenTelemetry/Internal/Logging/Core.hs +++ b/api/src/OpenTelemetry/Internal/Logging/Core.hs @@ -9,11 +9,11 @@ module OpenTelemetry.Internal.Logging.Core ( getGlobalLoggerProvider, makeLogger, emitLogRecord, - logDroppedAttributes, - emitOTelLogRecord, addAttribute, addAttributes, logRecordGetAttributes, + logDroppedAttributes, + emitOTelLogRecord, ) where import Control.Applicative @@ -107,10 +107,10 @@ makeLogger loggerProvider loggerInstrumentationScope = Logger {..} createImmutableLogRecord :: (MonadIO m) - => Logger + => LA.AttributeLimits -> LogRecordArguments body -> m (ImmutableLogRecord body) -createImmutableLogRecord logger@Logger {..} LogRecordArguments {..} = do +createImmutableLogRecord attributeLimits LogRecordArguments {..} = do currentTimestamp <- getCurrentTimestamp let logRecordObservedTimestamp = fromMaybe currentTimestamp observedTimestamp @@ -122,7 +122,7 @@ createImmutableLogRecord logger@Logger {..} LogRecordArguments {..} = do let logRecordAttributes = LA.addAttributes - (loggerProviderAttributeLimits loggerProvider) + attributeLimits LA.emptyAttributes attributes @@ -136,10 +136,7 @@ createImmutableLogRecord logger@Logger {..} LogRecordArguments {..} = do , logRecordSeverityNumber = severityNumber , logRecordSeverityText = severityText <|> (toShortName =<< severityNumber) , logRecordBody = body - , logRecordResource = loggerProviderResource loggerProvider - , logRecordInstrumentationScope = loggerInstrumentationScope , logRecordAttributes - , logRecordLogger = logger } @@ -178,12 +175,14 @@ emitLogRecord -> LogRecordArguments body -> m (LogRecord body) emitLogRecord l args = do - ilr <- createImmutableLogRecord l args - liftIO $ mkLogRecord ilr + ilr <- createImmutableLogRecord (loggerProviderAttributeLimits $ loggerProvider l) args + liftIO $ mkLogRecord l ilr {- | Add an attribute to a @LogRecord@. +This is not an atomic modification + As an application developer when you need to record an attribute first consult existing semantic conventions for Resources, Spans, and Metrics. If an appropriate name does not exists you will need to come up with a new name. To do that consider a few options: The name is specific to your company and may be possibly used outside the company as well. To avoid clashes with names introduced by other companies (in a distributed system that uses applications from multiple vendors) it is recommended to prefix the new name by your company’s reverse domain name, e.g. 'com.acme.shopname'. @@ -202,39 +201,43 @@ Any additions to the 'otel.*' namespace MUST be approved as part of OpenTelemetr -} addAttribute :: (ReadWriteLogRecord r, MonadIO m, ToValue a) => r body -> Text -> a -> m () addAttribute lr k v = - liftIO $ - modifyLogRecord - lr - ( \ilr@ImmutableLogRecord {logRecordAttributes, logRecordLogger} -> - ilr - { logRecordAttributes = - LA.addAttribute - (loggerProviderAttributeLimits $ loggerProvider logRecordLogger) - logRecordAttributes - k - v - } - ) + let attributeLimits = readLogRecordAttributeLimits lr + in liftIO $ + modifyLogRecord + lr + ( \ilr@ImmutableLogRecord {logRecordAttributes} -> + ilr + { logRecordAttributes = + LA.addAttribute + attributeLimits + logRecordAttributes + k + v + } + ) {- | A convenience function related to 'addAttribute' that adds multiple attributes to a @LogRecord@ at the same time. - This function may be slightly more performant than repeatedly calling 'addAttribute'. +This function may be slightly more performant than repeatedly calling 'addAttribute'. + +This is not an atomic modification -} addAttributes :: (ReadWriteLogRecord r, MonadIO m, ToValue a) => r body -> HashMap Text a -> m () addAttributes lr attrs = - liftIO $ - modifyLogRecord - lr - ( \ilr@ImmutableLogRecord {logRecordAttributes, logRecordLogger} -> - ilr - { logRecordAttributes = - LA.addAttributes - (loggerProviderAttributeLimits $ loggerProvider logRecordLogger) - logRecordAttributes - attrs - } - ) + let attributeLimits = readLogRecordAttributeLimits lr + in liftIO $ + modifyLogRecord + lr + ( \ilr@ImmutableLogRecord {logRecordAttributes} -> + ilr + { logRecordAttributes = + LA.addAttributes + attributeLimits + logRecordAttributes + attrs + } + ) {- | This can be useful for pulling data for attributes and diff --git a/api/src/OpenTelemetry/Internal/Logging/Types.hs b/api/src/OpenTelemetry/Internal/Logging/Types.hs index b6f3d4c3..dc429187 100644 --- a/api/src/OpenTelemetry/Internal/Logging/Types.hs +++ b/api/src/OpenTelemetry/Internal/Logging/Types.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE InstanceSigs #-} +{-# LANGUAGE NamedFieldPuns #-} module OpenTelemetry.Internal.Logging.Types ( LoggerProvider (..), @@ -18,7 +19,6 @@ module OpenTelemetry.Internal.Logging.Types ( import Data.Function (on) import qualified Data.HashMap.Strict as H import Data.IORef (IORef, atomicModifyIORef, modifyIORef, newIORef, readIORef) -import Data.Int (Int64) import Data.Text (Text) import OpenTelemetry.Common (Timestamp, TraceFlags) import OpenTelemetry.Context.Types (Context) @@ -31,6 +31,10 @@ import OpenTelemetry.Resource (MaterializedResources) -- | @Logger@s can be created from @LoggerProvider@s data LoggerProvider = LoggerProvider { loggerProviderResource :: MaterializedResources + -- ^ Describes the source of the log, aka resource. Multiple occurrences of events coming from the same event source can happen across time and they all have the same value of Resource. + -- Can contain for example information about the application that emits the record or about the infrastructure where the application runs. Data formats that represent this data model + -- may be designed in a manner that allows the Resource field to be recorded only once per batch of log records that come from the same source. SHOULD follow OpenTelemetry semantic conventions for Resources. + -- This field is optional. , loggerProviderAttributeLimits :: AttributeLimits } deriving (Show, Eq) @@ -49,30 +53,73 @@ data Logger = Logger {- | This is a data type that can represent logs from various sources: application log files, machine generated events, system logs, etc. [Specification outlined here.](https://opentelemetry.io/docs/specs/otel/logs/data-model/) Existing log formats can be unambiguously mapped to this data type. Reverse mapping from this data type is also possible to the extent that the target log format has equivalent capabilities. +Uses an IORef under the hood to allow mutability. -} -newtype LogRecord a = LogRecord (IORef (ImmutableLogRecord a)) +data LogRecord a = LogRecord Logger (IORef (ImmutableLogRecord a)) + + +mkLogRecord :: Logger -> ImmutableLogRecord body -> IO (LogRecord body) +mkLogRecord l = fmap (LogRecord l) . newIORef -mkLogRecord :: ImmutableLogRecord body -> IO (LogRecord body) -mkLogRecord = fmap LogRecord . newIORef +{- | This is a typeclass representing @LogRecord@s that can be read from. +A function receiving this as an argument MUST be able to access all the information added to the LogRecord. It MUST also be able to access the Instrumentation Scope and Resource information (implicitly) associated with the LogRecord. +The trace context fields MUST be populated from the resolved Context (either the explicitly passed Context or the current Context) when emitted. + +Counts for attributes due to collection limits MUST be available for exporters to report as described in the transformation to non-OTLP formats specification. +-} class ReadableLogRecord r where + -- | Reads the current state of the @LogRecord@ from its internal @IORef@. The implementation mirrors @readIORef@. readLogRecord :: r a -> IO (ImmutableLogRecord a) + -- | Reads the @InstrumentationScope@ from the @Logger@ that emitted the @LogRecord@ + readLogRecordInstrumentationScope :: r a -> InstrumentationLibrary + + + -- | Reads the @Resource@ from the @LoggerProvider@ that emitted the @LogRecord@ + readLogRecordResource :: r a -> MaterializedResources + + +{- | This is a typeclass representing @LogRecord@s that can be read from or written to. All @ReadWriteLogRecord@s are @ReadableLogRecord@s. + +A function receiving this as an argument MUST additionally be able to modify the following information added to the LogRecord: + +- Timestamp +- ObservedTimestamp +- SeverityText +- SeverityNumber +- Body +- Attributes (addition, modification, removal) +- TraceId +- SpanId +- TraceFlags +-} class (ReadableLogRecord r) => ReadWriteLogRecord r where + -- | Reads the attribute limits from the @LoggerProvider@ that emitted the @LogRecord@. These are needed to add more attributes. + readLogRecordAttributeLimits :: r a -> AttributeLimits + + + -- | Modifies the @LogRecord@ using its internal @IORef@. This is lazy and is not an atomic operation. The implementation mirrors @modifyIORef@. modifyLogRecord :: r a -> (ImmutableLogRecord a -> ImmutableLogRecord a) -> IO () + + + -- | An atomic version of @modifyLogRecord@. This function is lazy. The implementation mirrors @atomicModifyIORef@. atomicModifyLogRecord :: r a -> (ImmutableLogRecord a -> (ImmutableLogRecord a, b)) -> IO b instance ReadableLogRecord LogRecord where - readLogRecord (LogRecord ref) = readIORef ref + readLogRecord (LogRecord _ ref) = readIORef ref + readLogRecordInstrumentationScope (LogRecord (Logger {loggerInstrumentationScope}) _) = loggerInstrumentationScope + readLogRecordResource (LogRecord Logger {loggerProvider = LoggerProvider {loggerProviderResource}} _) = loggerProviderResource instance ReadWriteLogRecord LogRecord where - modifyLogRecord (LogRecord ref) = modifyIORef ref - atomicModifyLogRecord (LogRecord ref) = atomicModifyIORef ref + readLogRecordAttributeLimits (LogRecord Logger {loggerProvider = LoggerProvider {loggerProviderAttributeLimits}} _) = loggerProviderAttributeLimits + modifyLogRecord (LogRecord _ ref) = modifyIORef ref + atomicModifyLogRecord (LogRecord _ ref) = atomicModifyIORef ref data ImmutableLogRecord body = ImmutableLogRecord @@ -134,17 +181,10 @@ data ImmutableLogRecord body = ImmutableLogRecord -- - A byte array, -- - An array (a list) of any values, -- - A map. - , logRecordResource :: MaterializedResources - -- ^ Describes the source of the log, aka resource. Multiple occurrences of events coming from the same event source can happen across time and they all have the same value of Resource. - -- Can contain for example information about the application that emits the record or about the infrastructure where the application runs. Data formats that represent this data model - -- may be designed in a manner that allows the Resource field to be recorded only once per batch of log records that come from the same source. SHOULD follow OpenTelemetry semantic conventions for Resources. - -- This field is optional. - , logRecordInstrumentationScope :: InstrumentationLibrary , logRecordAttributes :: LogAttributes -- ^ Additional information about the specific event occurrence. Unlike the Resource field, which is fixed for a particular source, Attributes can vary for each occurrence of the event coming from the same source. -- Can contain information about the request context (other than Trace Context Fields). The log attribute model MUST support any type, a superset of standard Attribute, to preserve the semantics of structured attributes -- emitted by the applications. This field is optional. - , logRecordLogger :: Logger } deriving (Functor) From 5cc35e5e8aa270eadc74071a71e79c42b6d7fd81 Mon Sep 17 00:00:00 2001 From: evanlauer1 Date: Thu, 11 Jul 2024 20:22:04 -0700 Subject: [PATCH 5/8] Replaced type variable in LogRecord with AnyValue --- .../OpenTelemetry/Internal/Common/Types.hs | 95 ++++++++++++++++++- .../OpenTelemetry/Internal/Logging/Core.hs | 18 ++-- .../OpenTelemetry/Internal/Logging/Types.hs | 29 +++--- api/src/OpenTelemetry/LogAttributes.hs | 86 +---------------- api/test/OpenTelemetry/Logging/CoreSpec.hs | 5 +- 5 files changed, 121 insertions(+), 112 deletions(-) diff --git a/api/src/OpenTelemetry/Internal/Common/Types.hs b/api/src/OpenTelemetry/Internal/Common/Types.hs index e4123918..b78dfd18 100644 --- a/api/src/OpenTelemetry/Internal/Common/Types.hs +++ b/api/src/OpenTelemetry/Internal/Common/Types.hs @@ -1,9 +1,21 @@ +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE InstanceSigs #-} -module OpenTelemetry.Internal.Common.Types (InstrumentationLibrary (..)) where +module OpenTelemetry.Internal.Common.Types ( + InstrumentationLibrary (..), + AnyValue (..), + ToValue (..), +) where +import Data.ByteString (ByteString) +import Data.Data (Data) +import qualified Data.HashMap.Strict as H import Data.Hashable (Hashable) +import Data.Int (Int64) import Data.String (IsString (fromString)) import Data.Text (Text) import GHC.Generics (Generic) @@ -59,3 +71,84 @@ instance Hashable InstrumentationLibrary instance IsString InstrumentationLibrary where fromString :: String -> InstrumentationLibrary fromString str = InstrumentationLibrary (fromString str) "" "" emptyAttributes + + +{- | An attribute represents user-provided metadata about a span, link, or event. + + 'Any' values are used in place of 'Standard Attributes' in logs because third-party + logs may not conform to the 'Standard Attribute' format. + + Telemetry tools may use this data to support high-cardinality querying, visualization + in waterfall diagrams, trace sampling decisions, and more. +-} +data AnyValue + = TextValue Text + | BoolValue Bool + | DoubleValue Double + | IntValue Int64 + | ByteStringValue ByteString + | ArrayValue [AnyValue] + | HashMapValue (H.HashMap Text AnyValue) + deriving stock (Read, Show, Eq, Ord, Data, Generic) + deriving anyclass (Hashable) + + +-- | Create a `TextAttribute` from the string value. +instance IsString AnyValue where + fromString :: String -> AnyValue + fromString = TextValue . fromString + + +{- | Convert a Haskell value to an 'Any' value. + + @ + + data Foo = Foo + + instance ToValue Foo where + toValue Foo = TextValue "Foo" + + @ +-} +class ToValue a where + toValue :: a -> AnyValue + + +instance ToValue Text where + toValue :: Text -> AnyValue + toValue = TextValue + + +instance ToValue Bool where + toValue :: Bool -> AnyValue + toValue = BoolValue + + +instance ToValue Double where + toValue :: Double -> AnyValue + toValue = DoubleValue + + +instance ToValue Int64 where + toValue :: Int64 -> AnyValue + toValue = IntValue + + +instance ToValue ByteString where + toValue :: ByteString -> AnyValue + toValue = ByteStringValue + + +instance (ToValue a) => ToValue [a] where + toValue :: (ToValue a) => [a] -> AnyValue + toValue = ArrayValue . fmap toValue + + +instance (ToValue a) => ToValue (H.HashMap Text a) where + toValue :: (ToValue a) => H.HashMap Text a -> AnyValue + toValue = HashMapValue . fmap toValue + + +instance ToValue AnyValue where + toValue :: AnyValue -> AnyValue + toValue = id diff --git a/api/src/OpenTelemetry/Internal/Logging/Core.hs b/api/src/OpenTelemetry/Internal/Logging/Core.hs index 5f89df5c..43e13ec9 100644 --- a/api/src/OpenTelemetry/Internal/Logging/Core.hs +++ b/api/src/OpenTelemetry/Internal/Logging/Core.hs @@ -108,8 +108,8 @@ makeLogger loggerProvider loggerInstrumentationScope = Logger {..} createImmutableLogRecord :: (MonadIO m) => LA.AttributeLimits - -> LogRecordArguments body - -> m (ImmutableLogRecord body) + -> LogRecordArguments + -> m ImmutableLogRecord createImmutableLogRecord attributeLimits LogRecordArguments {..} = do currentTimestamp <- getCurrentTimestamp let logRecordObservedTimestamp = fromMaybe currentTimestamp observedTimestamp @@ -141,12 +141,12 @@ createImmutableLogRecord attributeLimits LogRecordArguments {..} = do -- | WARNING: this function should only be used to emit logs from the hs-opentelemetry-api library. DO NOT USE this function in any other context. -logDroppedAttributes :: (MonadIO m) => m (LogRecord Text) +logDroppedAttributes :: (MonadIO m) => m LogRecord logDroppedAttributes = emitOTelLogRecord H.empty Warn "At least 1 attribute was discarded due to the attribute limits set in the logger provider." -- | WARNING: this function should only be used to emit logs from the hs-opentelemetry-api library. DO NOT USE this function in any other context. -emitOTelLogRecord :: (MonadIO m) => H.HashMap Text LA.AnyValue -> SeverityNumber -> Text -> m (LogRecord Text) +emitOTelLogRecord :: (MonadIO m) => H.HashMap Text LA.AnyValue -> SeverityNumber -> Text -> m LogRecord emitOTelLogRecord attrs severity body = do glp <- getGlobalLoggerProvider let gl = @@ -172,8 +172,8 @@ If context is not specified in LogRecordArguments it will default to the current emitLogRecord :: (MonadIO m) => Logger - -> LogRecordArguments body - -> m (LogRecord body) + -> LogRecordArguments + -> m LogRecord emitLogRecord l args = do ilr <- createImmutableLogRecord (loggerProviderAttributeLimits $ loggerProvider l) args liftIO $ mkLogRecord l ilr @@ -199,7 +199,7 @@ For example, the 'otel.library.name' attribute is used to record the instrumenta Any additions to the 'otel.*' namespace MUST be approved as part of OpenTelemetry specification. -} -addAttribute :: (ReadWriteLogRecord r, MonadIO m, ToValue a) => r body -> Text -> a -> m () +addAttribute :: (ReadWriteLogRecord r, MonadIO m, ToValue a) => r -> Text -> a -> m () addAttribute lr k v = let attributeLimits = readLogRecordAttributeLimits lr in liftIO $ @@ -223,7 +223,7 @@ This function may be slightly more performant than repeatedly calling 'addAttrib This is not an atomic modification -} -addAttributes :: (ReadWriteLogRecord r, MonadIO m, ToValue a) => r body -> HashMap Text a -> m () +addAttributes :: (ReadWriteLogRecord r, MonadIO m, ToValue a) => r -> HashMap Text a -> m () addAttributes lr attrs = let attributeLimits = readLogRecordAttributeLimits lr in liftIO $ @@ -244,5 +244,5 @@ addAttributes lr attrs = using it to copy / otherwise use the data to further enrich instrumentation. -} -logRecordGetAttributes :: (ReadableLogRecord r, MonadIO m) => r a -> m LogAttributes +logRecordGetAttributes :: (ReadableLogRecord r, MonadIO m) => r -> m LogAttributes logRecordGetAttributes lr = liftIO $ logRecordAttributes <$> readLogRecord lr diff --git a/api/src/OpenTelemetry/Internal/Logging/Types.hs b/api/src/OpenTelemetry/Internal/Logging/Types.hs index dc429187..20001107 100644 --- a/api/src/OpenTelemetry/Internal/Logging/Types.hs +++ b/api/src/OpenTelemetry/Internal/Logging/Types.hs @@ -55,10 +55,10 @@ data Logger = Logger Existing log formats can be unambiguously mapped to this data type. Reverse mapping from this data type is also possible to the extent that the target log format has equivalent capabilities. Uses an IORef under the hood to allow mutability. -} -data LogRecord a = LogRecord Logger (IORef (ImmutableLogRecord a)) +data LogRecord = LogRecord Logger (IORef ImmutableLogRecord) -mkLogRecord :: Logger -> ImmutableLogRecord body -> IO (LogRecord body) +mkLogRecord :: Logger -> ImmutableLogRecord -> IO LogRecord mkLogRecord l = fmap (LogRecord l) . newIORef @@ -72,15 +72,15 @@ Counts for attributes due to collection limits MUST be available for exporters t -} class ReadableLogRecord r where -- | Reads the current state of the @LogRecord@ from its internal @IORef@. The implementation mirrors @readIORef@. - readLogRecord :: r a -> IO (ImmutableLogRecord a) + readLogRecord :: r -> IO ImmutableLogRecord -- | Reads the @InstrumentationScope@ from the @Logger@ that emitted the @LogRecord@ - readLogRecordInstrumentationScope :: r a -> InstrumentationLibrary + readLogRecordInstrumentationScope :: r -> InstrumentationLibrary -- | Reads the @Resource@ from the @LoggerProvider@ that emitted the @LogRecord@ - readLogRecordResource :: r a -> MaterializedResources + readLogRecordResource :: r -> MaterializedResources {- | This is a typeclass representing @LogRecord@s that can be read from or written to. All @ReadWriteLogRecord@s are @ReadableLogRecord@s. @@ -99,15 +99,15 @@ A function receiving this as an argument MUST additionally be able to modify the -} class (ReadableLogRecord r) => ReadWriteLogRecord r where -- | Reads the attribute limits from the @LoggerProvider@ that emitted the @LogRecord@. These are needed to add more attributes. - readLogRecordAttributeLimits :: r a -> AttributeLimits + readLogRecordAttributeLimits :: r -> AttributeLimits -- | Modifies the @LogRecord@ using its internal @IORef@. This is lazy and is not an atomic operation. The implementation mirrors @modifyIORef@. - modifyLogRecord :: r a -> (ImmutableLogRecord a -> ImmutableLogRecord a) -> IO () + modifyLogRecord :: r -> (ImmutableLogRecord -> ImmutableLogRecord) -> IO () -- | An atomic version of @modifyLogRecord@. This function is lazy. The implementation mirrors @atomicModifyIORef@. - atomicModifyLogRecord :: r a -> (ImmutableLogRecord a -> (ImmutableLogRecord a, b)) -> IO b + atomicModifyLogRecord :: r -> (ImmutableLogRecord -> (ImmutableLogRecord, b)) -> IO b instance ReadableLogRecord LogRecord where @@ -122,7 +122,7 @@ instance ReadWriteLogRecord LogRecord where atomicModifyLogRecord (LogRecord _ ref) = atomicModifyIORef ref -data ImmutableLogRecord body = ImmutableLogRecord +data ImmutableLogRecord = ImmutableLogRecord { logRecordTimestamp :: Maybe Timestamp -- ^ Time when the event occurred measured by the origin clock. This field is optional, it may be missing if the timestamp is unknown. , logRecordObservedTimestamp :: Timestamp @@ -170,7 +170,7 @@ data ImmutableLogRecord body = ImmutableLogRecord -- -- In the contexts where severity participates in less-than / greater-than comparisons SeverityNumber field should be used. -- SeverityNumber can be compared to another SeverityNumber or to numbers in the 1..24 range (or to the corresponding short names). - , logRecordBody :: body + , logRecordBody :: AnyValue -- ^ A value containing the body of the log record. Can be for example a human-readable string message (including multi-line) describing the event in a free form or it can be a -- structured data composed of arrays and maps of other values. Body MUST support any type to preserve the semantics of structured logs emitted by the applications. -- Can vary for each occurrence of the event coming from the same source. This field is optional. @@ -186,25 +186,24 @@ data ImmutableLogRecord body = ImmutableLogRecord -- Can contain information about the request context (other than Trace Context Fields). The log attribute model MUST support any type, a superset of standard Attribute, to preserve the semantics of structured attributes -- emitted by the applications. This field is optional. } - deriving (Functor) {- | Arguments that may be set on LogRecord creation. If observedTimestamp is not set, it will default to the current timestamp. If context is not specified it will default to the current context. Refer to the documentation of @LogRecord@ for descriptions of the fields. -} -data LogRecordArguments body = LogRecordArguments +data LogRecordArguments = LogRecordArguments { timestamp :: Maybe Timestamp , observedTimestamp :: Maybe Timestamp , context :: Maybe Context , severityText :: Maybe Text , severityNumber :: Maybe SeverityNumber - , body :: body + , body :: AnyValue , attributes :: H.HashMap Text AnyValue } -emptyLogRecordArguments :: body -> LogRecordArguments body +emptyLogRecordArguments :: (ToValue body) => body -> LogRecordArguments emptyLogRecordArguments body = LogRecordArguments { timestamp = Nothing @@ -212,7 +211,7 @@ emptyLogRecordArguments body = , context = Nothing , severityText = Nothing , severityNumber = Nothing - , body = body + , body = toValue body , attributes = H.empty } diff --git a/api/src/OpenTelemetry/LogAttributes.hs b/api/src/OpenTelemetry/LogAttributes.hs index 62055cab..45a8f34a 100644 --- a/api/src/OpenTelemetry/LogAttributes.hs +++ b/api/src/OpenTelemetry/LogAttributes.hs @@ -1,10 +1,6 @@ {-# LANGUAGE BangPatterns #-} -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE InstanceSigs #-} module OpenTelemetry.LogAttributes ( LogAttributes (..), @@ -35,6 +31,7 @@ import Data.Text (Text) import qualified Data.Text as T import GHC.Generics (Generic) import OpenTelemetry.Attributes (AttributeLimits (..), defaultAttributeLimits) +import OpenTelemetry.Internal.Common.Types data LogAttributes = LogAttributes @@ -90,87 +87,6 @@ limitLengths limit (HashMapValue h) = HashMapValue $ fmap (limitLengths limit) h limitLengths _ val = val -{- | An attribute represents user-provided metadata about a span, link, or event. - - 'Any' values are used in place of 'Standard Attributes' in logs because third-party - logs may not conform to the 'Standard Attribute' format. - - Telemetry tools may use this data to support high-cardinality querying, visualization - in waterfall diagrams, trace sampling decisions, and more. --} -data AnyValue - = TextValue Text - | BoolValue Bool - | DoubleValue Double - | IntValue Int64 - | ByteStringValue ByteString - | ArrayValue [AnyValue] - | HashMapValue (H.HashMap Text AnyValue) - deriving stock (Read, Show, Eq, Ord, Data, Generic) - deriving anyclass (Hashable) - - --- | Create a `TextAttribute` from the string value. -instance IsString AnyValue where - fromString :: String -> AnyValue - fromString = TextValue . fromString - - -{- | Convert a Haskell value to an 'Any' value. - - @ - - data Foo = Foo - - instance ToValue Foo where - toValue Foo = TextValue "Foo" - - @ --} -class ToValue a where - toValue :: a -> AnyValue - - -instance ToValue Text where - toValue :: Text -> AnyValue - toValue = TextValue - - -instance ToValue Bool where - toValue :: Bool -> AnyValue - toValue = BoolValue - - -instance ToValue Double where - toValue :: Double -> AnyValue - toValue = DoubleValue - - -instance ToValue Int64 where - toValue :: Int64 -> AnyValue - toValue = IntValue - - -instance ToValue ByteString where - toValue :: ByteString -> AnyValue - toValue = ByteStringValue - - -instance (ToValue a) => ToValue [a] where - toValue :: (ToValue a) => [a] -> AnyValue - toValue = ArrayValue . fmap toValue - - -instance (ToValue a) => ToValue (H.HashMap Text a) where - toValue :: (ToValue a) => H.HashMap Text a -> AnyValue - toValue = HashMapValue . fmap toValue - - -instance ToValue AnyValue where - toValue :: AnyValue -> AnyValue - toValue = id - - unsafeMergeLogAttributesIgnoringLimits :: LogAttributes -> LogAttributes -> LogAttributes unsafeMergeLogAttributesIgnoringLimits (LogAttributes l lc ld) (LogAttributes r rc rd) = LogAttributes (l <> r) (lc + rc) (ld + rd) diff --git a/api/test/OpenTelemetry/Logging/CoreSpec.hs b/api/test/OpenTelemetry/Logging/CoreSpec.hs index fbdff367..5bf7a919 100644 --- a/api/test/OpenTelemetry/Logging/CoreSpec.hs +++ b/api/test/OpenTelemetry/Logging/CoreSpec.hs @@ -5,6 +5,7 @@ module OpenTelemetry.Logging.CoreSpec where import qualified Data.HashMap.Strict as H import Data.IORef +import qualified Data.Text as T import qualified OpenTelemetry.Attributes as A import OpenTelemetry.Internal.Logging.Types import qualified OpenTelemetry.LogAttributes as LA @@ -49,7 +50,7 @@ spec = describe "Core" $ do it "works" $ do lp <- getGlobalLoggerProvider let l = makeLogger lp InstrumentationLibrary {libraryName = "exampleLibrary", libraryVersion = "", librarySchemaUrl = "", libraryAttributes = A.emptyAttributes} - lr <- emitLogRecord l $ (emptyLogRecordArguments ()) {attributes = H.fromList [("something", "a thing")]} + lr <- emitLogRecord l $ (emptyLogRecordArguments ("" :: LA.AnyValue)) {attributes = H.fromList [("something", "a thing")]} addAttribute lr "anotherThing" ("another thing" :: LA.AnyValue) @@ -63,7 +64,7 @@ spec = describe "Core" $ do it "works" $ do lp <- getGlobalLoggerProvider let l = makeLogger lp InstrumentationLibrary {libraryName = "exampleLibrary", libraryVersion = "", librarySchemaUrl = "", libraryAttributes = A.emptyAttributes} - lr <- emitLogRecord l $ (emptyLogRecordArguments ()) {attributes = H.fromList [("something", "a thing")]} + lr <- emitLogRecord l $ (emptyLogRecordArguments ("" :: LA.AnyValue)) {attributes = H.fromList [("something", "a thing")]} addAttributes lr $ H.fromList From 2c80ef7034923044b040b24cf4a2d339a05b062a Mon Sep 17 00:00:00 2001 From: evanlauer1 Date: Mon, 15 Jul 2024 12:46:09 -0700 Subject: [PATCH 6/8] Changed Readable and ReadWriteLogRecord to be concrete types --- .../OpenTelemetry/Internal/Logging/Core.hs | 14 +++--- .../OpenTelemetry/Internal/Logging/Types.hs | 49 ++++++++++++------- api/src/OpenTelemetry/Logging/Core.hs | 9 ++-- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/api/src/OpenTelemetry/Internal/Logging/Core.hs b/api/src/OpenTelemetry/Internal/Logging/Core.hs index 43e13ec9..c283d065 100644 --- a/api/src/OpenTelemetry/Internal/Logging/Core.hs +++ b/api/src/OpenTelemetry/Internal/Logging/Core.hs @@ -141,12 +141,12 @@ createImmutableLogRecord attributeLimits LogRecordArguments {..} = do -- | WARNING: this function should only be used to emit logs from the hs-opentelemetry-api library. DO NOT USE this function in any other context. -logDroppedAttributes :: (MonadIO m) => m LogRecord +logDroppedAttributes :: (MonadIO m) => m ReadWriteLogRecord logDroppedAttributes = emitOTelLogRecord H.empty Warn "At least 1 attribute was discarded due to the attribute limits set in the logger provider." -- | WARNING: this function should only be used to emit logs from the hs-opentelemetry-api library. DO NOT USE this function in any other context. -emitOTelLogRecord :: (MonadIO m) => H.HashMap Text LA.AnyValue -> SeverityNumber -> Text -> m LogRecord +emitOTelLogRecord :: (MonadIO m) => H.HashMap Text LA.AnyValue -> SeverityNumber -> Text -> m ReadWriteLogRecord emitOTelLogRecord attrs severity body = do glp <- getGlobalLoggerProvider let gl = @@ -173,10 +173,10 @@ emitLogRecord :: (MonadIO m) => Logger -> LogRecordArguments - -> m LogRecord + -> m ReadWriteLogRecord emitLogRecord l args = do ilr <- createImmutableLogRecord (loggerProviderAttributeLimits $ loggerProvider l) args - liftIO $ mkLogRecord l ilr + liftIO $ mkReadWriteLogRecord l ilr {- | Add an attribute to a @LogRecord@. @@ -199,7 +199,7 @@ For example, the 'otel.library.name' attribute is used to record the instrumenta Any additions to the 'otel.*' namespace MUST be approved as part of OpenTelemetry specification. -} -addAttribute :: (ReadWriteLogRecord r, MonadIO m, ToValue a) => r -> Text -> a -> m () +addAttribute :: (IsReadWriteLogRecord r, MonadIO m, ToValue a) => r -> Text -> a -> m () addAttribute lr k v = let attributeLimits = readLogRecordAttributeLimits lr in liftIO $ @@ -223,7 +223,7 @@ This function may be slightly more performant than repeatedly calling 'addAttrib This is not an atomic modification -} -addAttributes :: (ReadWriteLogRecord r, MonadIO m, ToValue a) => r -> HashMap Text a -> m () +addAttributes :: (IsReadWriteLogRecord r, MonadIO m, ToValue a) => r -> HashMap Text a -> m () addAttributes lr attrs = let attributeLimits = readLogRecordAttributeLimits lr in liftIO $ @@ -244,5 +244,5 @@ addAttributes lr attrs = using it to copy / otherwise use the data to further enrich instrumentation. -} -logRecordGetAttributes :: (ReadableLogRecord r, MonadIO m) => r -> m LogAttributes +logRecordGetAttributes :: (IsReadableLogRecord r, MonadIO m) => r -> m LogAttributes logRecordGetAttributes lr = liftIO $ logRecordAttributes <$> readLogRecord lr diff --git a/api/src/OpenTelemetry/Internal/Logging/Types.hs b/api/src/OpenTelemetry/Internal/Logging/Types.hs index 20001107..f61c618d 100644 --- a/api/src/OpenTelemetry/Internal/Logging/Types.hs +++ b/api/src/OpenTelemetry/Internal/Logging/Types.hs @@ -5,10 +5,12 @@ module OpenTelemetry.Internal.Logging.Types ( LoggerProvider (..), Logger (..), - LogRecord, - mkLogRecord, - ReadableLogRecord (..), - ReadWriteLogRecord (..), + ReadWriteLogRecord, + mkReadWriteLogRecord, + ReadableLogRecord, + mkReadableLogRecord, + IsReadableLogRecord (..), + IsReadWriteLogRecord (..), ImmutableLogRecord (..), LogRecordArguments (..), emptyLogRecordArguments, @@ -55,11 +57,18 @@ data Logger = Logger Existing log formats can be unambiguously mapped to this data type. Reverse mapping from this data type is also possible to the extent that the target log format has equivalent capabilities. Uses an IORef under the hood to allow mutability. -} -data LogRecord = LogRecord Logger (IORef ImmutableLogRecord) +data ReadWriteLogRecord = ReadWriteLogRecord Logger (IORef ImmutableLogRecord) -mkLogRecord :: Logger -> ImmutableLogRecord -> IO LogRecord -mkLogRecord l = fmap (LogRecord l) . newIORef +mkReadWriteLogRecord :: Logger -> ImmutableLogRecord -> IO ReadWriteLogRecord +mkReadWriteLogRecord l = fmap (ReadWriteLogRecord l) . newIORef + + +newtype ReadableLogRecord = ReadableLogRecord {readableLogRecord :: ReadWriteLogRecord} + + +mkReadableLogRecord :: Logger -> ImmutableLogRecord -> IO ReadableLogRecord +mkReadableLogRecord l = fmap ReadableLogRecord . mkReadWriteLogRecord l {- | This is a typeclass representing @LogRecord@s that can be read from. @@ -70,7 +79,7 @@ The trace context fields MUST be populated from the resolved Context (either the Counts for attributes due to collection limits MUST be available for exporters to report as described in the transformation to non-OTLP formats specification. -} -class ReadableLogRecord r where +class IsReadableLogRecord r where -- | Reads the current state of the @LogRecord@ from its internal @IORef@. The implementation mirrors @readIORef@. readLogRecord :: r -> IO ImmutableLogRecord @@ -97,7 +106,7 @@ A function receiving this as an argument MUST additionally be able to modify the - SpanId - TraceFlags -} -class (ReadableLogRecord r) => ReadWriteLogRecord r where +class (IsReadableLogRecord r) => IsReadWriteLogRecord r where -- | Reads the attribute limits from the @LoggerProvider@ that emitted the @LogRecord@. These are needed to add more attributes. readLogRecordAttributeLimits :: r -> AttributeLimits @@ -110,16 +119,22 @@ class (ReadableLogRecord r) => ReadWriteLogRecord r where atomicModifyLogRecord :: r -> (ImmutableLogRecord -> (ImmutableLogRecord, b)) -> IO b -instance ReadableLogRecord LogRecord where - readLogRecord (LogRecord _ ref) = readIORef ref - readLogRecordInstrumentationScope (LogRecord (Logger {loggerInstrumentationScope}) _) = loggerInstrumentationScope - readLogRecordResource (LogRecord Logger {loggerProvider = LoggerProvider {loggerProviderResource}} _) = loggerProviderResource +instance IsReadableLogRecord ReadableLogRecord where + readLogRecord = readLogRecord . readableLogRecord + readLogRecordInstrumentationScope = readLogRecordInstrumentationScope . readableLogRecord + readLogRecordResource = readLogRecordResource . readableLogRecord + + +instance IsReadableLogRecord ReadWriteLogRecord where + readLogRecord (ReadWriteLogRecord _ ref) = readIORef ref + readLogRecordInstrumentationScope (ReadWriteLogRecord (Logger {loggerInstrumentationScope}) _) = loggerInstrumentationScope + readLogRecordResource (ReadWriteLogRecord Logger {loggerProvider = LoggerProvider {loggerProviderResource}} _) = loggerProviderResource -instance ReadWriteLogRecord LogRecord where - readLogRecordAttributeLimits (LogRecord Logger {loggerProvider = LoggerProvider {loggerProviderAttributeLimits}} _) = loggerProviderAttributeLimits - modifyLogRecord (LogRecord _ ref) = modifyIORef ref - atomicModifyLogRecord (LogRecord _ ref) = atomicModifyIORef ref +instance IsReadWriteLogRecord ReadWriteLogRecord where + readLogRecordAttributeLimits (ReadWriteLogRecord Logger {loggerProvider = LoggerProvider {loggerProviderAttributeLimits}} _) = loggerProviderAttributeLimits + modifyLogRecord (ReadWriteLogRecord _ ref) = modifyIORef ref + atomicModifyLogRecord (ReadWriteLogRecord _ ref) = atomicModifyIORef ref data ImmutableLogRecord = ImmutableLogRecord diff --git a/api/src/OpenTelemetry/Logging/Core.hs b/api/src/OpenTelemetry/Logging/Core.hs index 14143a17..6464d129 100644 --- a/api/src/OpenTelemetry/Logging/Core.hs +++ b/api/src/OpenTelemetry/Logging/Core.hs @@ -13,10 +13,13 @@ module OpenTelemetry.Logging.Core ( makeLogger, -- * @LogRecord@ operations - LogRecord, - ReadableLogRecord (..), - ReadWriteLogRecord (..), + ReadableLogRecord, + ReadWriteLogRecord, + IsReadableLogRecord (..), + IsReadWriteLogRecord (..), LogRecordArguments (..), + AnyValue (..), + ToValue (..), SeverityNumber (..), toShortName, emitLogRecord, From 9e0cd5ed302b4def2ad0d5251e2a3c7eb1420ab2 Mon Sep 17 00:00:00 2001 From: evanlauer1 Date: Mon, 15 Jul 2024 15:40:53 -0700 Subject: [PATCH 7/8] Changed mkReadableLogRecord to take in a ReadWriteLogRecord --- api/src/OpenTelemetry/Internal/Logging/Types.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/OpenTelemetry/Internal/Logging/Types.hs b/api/src/OpenTelemetry/Internal/Logging/Types.hs index f61c618d..54893cfd 100644 --- a/api/src/OpenTelemetry/Internal/Logging/Types.hs +++ b/api/src/OpenTelemetry/Internal/Logging/Types.hs @@ -67,8 +67,8 @@ mkReadWriteLogRecord l = fmap (ReadWriteLogRecord l) . newIORef newtype ReadableLogRecord = ReadableLogRecord {readableLogRecord :: ReadWriteLogRecord} -mkReadableLogRecord :: Logger -> ImmutableLogRecord -> IO ReadableLogRecord -mkReadableLogRecord l = fmap ReadableLogRecord . mkReadWriteLogRecord l +mkReadableLogRecord :: ReadWriteLogRecord -> ReadableLogRecord +mkReadableLogRecord = ReadableLogRecord {- | This is a typeclass representing @LogRecord@s that can be read from. From 5af2e9e40db39b0e80c2ce2712a2363a88a20853 Mon Sep 17 00:00:00 2001 From: evanlauer1 Date: Mon, 22 Jul 2024 11:29:13 -0700 Subject: [PATCH 8/8] Added NullValue to AnyValue and removed body parameter from emptyLogRecordArguments --- api/src/OpenTelemetry/Internal/Common/Types.hs | 1 + api/src/OpenTelemetry/Internal/Logging/Core.hs | 5 +++-- api/src/OpenTelemetry/Internal/Logging/Types.hs | 6 +++--- api/test/OpenTelemetry/Logging/CoreSpec.hs | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/api/src/OpenTelemetry/Internal/Common/Types.hs b/api/src/OpenTelemetry/Internal/Common/Types.hs index b78dfd18..0d80f8c7 100644 --- a/api/src/OpenTelemetry/Internal/Common/Types.hs +++ b/api/src/OpenTelemetry/Internal/Common/Types.hs @@ -89,6 +89,7 @@ data AnyValue | ByteStringValue ByteString | ArrayValue [AnyValue] | HashMapValue (H.HashMap Text AnyValue) + | NullValue deriving stock (Read, Show, Eq, Ord, Data, Generic) deriving anyclass (Hashable) diff --git a/api/src/OpenTelemetry/Internal/Logging/Core.hs b/api/src/OpenTelemetry/Internal/Logging/Core.hs index c283d065..181a3818 100644 --- a/api/src/OpenTelemetry/Internal/Logging/Core.hs +++ b/api/src/OpenTelemetry/Internal/Logging/Core.hs @@ -147,7 +147,7 @@ logDroppedAttributes = emitOTelLogRecord H.empty Warn "At least 1 attribute was -- | WARNING: this function should only be used to emit logs from the hs-opentelemetry-api library. DO NOT USE this function in any other context. emitOTelLogRecord :: (MonadIO m) => H.HashMap Text LA.AnyValue -> SeverityNumber -> Text -> m ReadWriteLogRecord -emitOTelLogRecord attrs severity body = do +emitOTelLogRecord attrs severity bodyText = do glp <- getGlobalLoggerProvider let gl = makeLogger glp $ @@ -159,8 +159,9 @@ emitOTelLogRecord attrs severity body = do } emitLogRecord gl $ - (emptyLogRecordArguments body) + emptyLogRecordArguments { severityNumber = Just severity + , body = toValue bodyText , attributes = attrs } diff --git a/api/src/OpenTelemetry/Internal/Logging/Types.hs b/api/src/OpenTelemetry/Internal/Logging/Types.hs index 54893cfd..09661e3e 100644 --- a/api/src/OpenTelemetry/Internal/Logging/Types.hs +++ b/api/src/OpenTelemetry/Internal/Logging/Types.hs @@ -218,15 +218,15 @@ data LogRecordArguments = LogRecordArguments } -emptyLogRecordArguments :: (ToValue body) => body -> LogRecordArguments -emptyLogRecordArguments body = +emptyLogRecordArguments :: LogRecordArguments +emptyLogRecordArguments = LogRecordArguments { timestamp = Nothing , observedTimestamp = Nothing , context = Nothing , severityText = Nothing , severityNumber = Nothing - , body = toValue body + , body = NullValue , attributes = H.empty } diff --git a/api/test/OpenTelemetry/Logging/CoreSpec.hs b/api/test/OpenTelemetry/Logging/CoreSpec.hs index 5bf7a919..11423447 100644 --- a/api/test/OpenTelemetry/Logging/CoreSpec.hs +++ b/api/test/OpenTelemetry/Logging/CoreSpec.hs @@ -50,7 +50,7 @@ spec = describe "Core" $ do it "works" $ do lp <- getGlobalLoggerProvider let l = makeLogger lp InstrumentationLibrary {libraryName = "exampleLibrary", libraryVersion = "", librarySchemaUrl = "", libraryAttributes = A.emptyAttributes} - lr <- emitLogRecord l $ (emptyLogRecordArguments ("" :: LA.AnyValue)) {attributes = H.fromList [("something", "a thing")]} + lr <- emitLogRecord l $ emptyLogRecordArguments {attributes = H.fromList [("something", "a thing")]} addAttribute lr "anotherThing" ("another thing" :: LA.AnyValue) @@ -64,7 +64,7 @@ spec = describe "Core" $ do it "works" $ do lp <- getGlobalLoggerProvider let l = makeLogger lp InstrumentationLibrary {libraryName = "exampleLibrary", libraryVersion = "", librarySchemaUrl = "", libraryAttributes = A.emptyAttributes} - lr <- emitLogRecord l $ (emptyLogRecordArguments ("" :: LA.AnyValue)) {attributes = H.fromList [("something", "a thing")]} + lr <- emitLogRecord l $ emptyLogRecordArguments {attributes = H.fromList [("something", "a thing")]} addAttributes lr $ H.fromList