mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +03:00
7b3ce7d927
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/10662 GitOrigin-RevId: abefec7649098cfb6e4cc906210bc709c25301b0
562 lines
20 KiB
Haskell
562 lines
20 KiB
Haskell
{-# LANGUAGE NumericUnderscores #-}
|
|
{-# LANGUAGE PatternSynonyms #-}
|
|
{-# LANGUAGE UndecidableInstances #-}
|
|
{-# LANGUAGE ViewPatterns #-}
|
|
|
|
module Hasura.Logging
|
|
( LoggerSettings (..),
|
|
defaultLoggerSettings,
|
|
EngineLogType (..),
|
|
Hasura,
|
|
InternalLogTypes (..),
|
|
EngineLog (..),
|
|
FormattedTime, -- N.B. opaque
|
|
toPOSIX_ns,
|
|
userAllowedLogTypes,
|
|
ToEngineLog (..),
|
|
debugT,
|
|
debugBS,
|
|
debugLBS,
|
|
UnstructuredLog (..),
|
|
Logger (.., Logger, unLogger),
|
|
LogLevel (..),
|
|
prettyLogLevel,
|
|
UnhandledInternalErrorLog (..),
|
|
mkLogger,
|
|
nullLogger,
|
|
|
|
-- ** LoggerCtx
|
|
LoggerCtx,
|
|
getLoggerSet,
|
|
getTimeGetter,
|
|
getLogLevel,
|
|
getEnabledLogTypes,
|
|
getLogsExporter,
|
|
mkLoggerCtx,
|
|
mkLoggerCtxOTLP,
|
|
cleanLoggerCtx,
|
|
|
|
-- ** etc
|
|
eventTriggerLogType,
|
|
eventTriggerProcessLogType,
|
|
scheduledTriggerLogType,
|
|
scheduledTriggerProcessLogType,
|
|
cronEventGeneratorProcessType,
|
|
sourceCatalogMigrationLogType,
|
|
EnabledLogTypes (..),
|
|
defaultEnabledEngineLogTypes,
|
|
isEngineLogTypeEnabled,
|
|
readLogTypes,
|
|
getFormattedTime,
|
|
|
|
-- * Debounced stats logger
|
|
createStatsLogger,
|
|
closeStatsLogger,
|
|
logStats,
|
|
|
|
-- * Other internal logs
|
|
StoredIntrospectionLog (..),
|
|
StoredIntrospectionStorageLog (..),
|
|
)
|
|
where
|
|
|
|
import Control.Exception (ErrorCall (ErrorCallWithLocation), catch)
|
|
import Control.FoldDebounce qualified as FDebounce
|
|
import Control.Monad.Trans.Control
|
|
import Control.Monad.Trans.Managed (ManagedT (..), allocate)
|
|
import Data.Aeson qualified as J
|
|
import Data.Aeson.Types qualified as J
|
|
import Data.ByteString qualified as B
|
|
import Data.ByteString.Lazy qualified as BL
|
|
import Data.ByteString.Lazy.Char8 qualified as BLC
|
|
import Data.HashSet qualified as Set
|
|
import Data.IORef
|
|
import Data.Map.Strict (Map)
|
|
import Data.Map.Strict qualified as Map
|
|
import Data.SerializableBlob qualified as SB
|
|
import Data.String (fromString)
|
|
import Data.Text qualified as T
|
|
import Data.Time.Clock qualified as Time
|
|
import Data.Time.Clock.POSIX qualified as Time
|
|
import Data.Time.Format qualified as Format
|
|
import Data.Time.LocalTime qualified as Time
|
|
import Hasura.Base.Error (QErr)
|
|
import Hasura.CachedTime
|
|
import Hasura.Prelude
|
|
import Hasura.Tracing.Class qualified as Tracing
|
|
import Hasura.Tracing.Context
|
|
import Hasura.Tracing.TraceId
|
|
import System.Log.FastLogger qualified as FL
|
|
import Witch qualified
|
|
|
|
-- | A zoned timestamp with defined serialized format (via the ToJSON instance)
|
|
--
|
|
-- Internals not exported. Construct with 'getFormattedTime'
|
|
data FormattedTime = FormattedTime Time.UTCTime Time.TimeZone
|
|
deriving (Show, Eq)
|
|
|
|
instance J.ToJSON FormattedTime where
|
|
toJSON (FormattedTime t tz) = J.toJSON $ T.pack $ formatTime $ Time.utcToZonedTime tz t
|
|
where
|
|
formatTime = Format.formatTime Format.defaultTimeLocale format
|
|
format = "%FT%H:%M:%S%3Q%z"
|
|
|
|
-- format = Format.iso8601DateFormat (Just "%H:%M:%S")
|
|
|
|
-- | Timestamp as uniz epoch time, in nanoseconds.
|
|
toPOSIX_ns :: FormattedTime -> Word64
|
|
toPOSIX_ns (FormattedTime t _) = floor . (* 1e9) . Time.utcTimeToPOSIXSeconds $ t
|
|
|
|
-- | Typeclass representing any type which can be parsed into a list of enabled log types, and has a @Set@
|
|
-- of default enabled log types, and can find out if a log type is enabled
|
|
class (Eq (EngineLogType impl), Hashable (EngineLogType impl)) => EnabledLogTypes impl where
|
|
parseEnabledLogTypes :: String -> Either String [EngineLogType impl]
|
|
defaultEnabledLogTypes :: Set.HashSet (EngineLogType impl)
|
|
isLogTypeEnabled :: Set.HashSet (EngineLogType impl) -> EngineLogType impl -> Bool
|
|
|
|
-- | A family of EngineLogType types
|
|
data family EngineLogType impl
|
|
|
|
data Hasura
|
|
|
|
-- log types emitted from the OSS core.
|
|
-- NOTE: however these are also emitted by e.g. graphql-engine-pro (in addition
|
|
-- to other log types)
|
|
data instance EngineLogType Hasura
|
|
= ELTHttpLog
|
|
| ELTWebsocketLog
|
|
| ELTWebhookLog
|
|
| ELTQueryLog
|
|
| ELTExecutionLog
|
|
| ELTStartup
|
|
| ELTLivequeryPollerLog
|
|
| ELTActionHandler
|
|
| ELTDataConnectorLog
|
|
| ELTJwkRefreshLog
|
|
| ELTValidateInputLog
|
|
| -- internal log types
|
|
ELTInternal !InternalLogTypes
|
|
deriving (Show, Eq, Generic)
|
|
|
|
instance Hashable (EngineLogType Hasura)
|
|
|
|
instance Witch.From (EngineLogType Hasura) Text where
|
|
from = \case
|
|
ELTHttpLog -> "http-log"
|
|
ELTWebsocketLog -> "websocket-log"
|
|
ELTWebhookLog -> "webhook-log"
|
|
ELTQueryLog -> "query-log"
|
|
ELTExecutionLog -> "execution-log"
|
|
ELTStartup -> "startup"
|
|
ELTLivequeryPollerLog -> "livequery-poller-log"
|
|
ELTActionHandler -> "action-handler-log"
|
|
ELTDataConnectorLog -> "data-connector-log"
|
|
ELTJwkRefreshLog -> "jwk-refresh-log"
|
|
ELTValidateInputLog -> "validate-insert-input-log"
|
|
ELTInternal t -> Witch.from t
|
|
|
|
instance J.ToJSON (EngineLogType Hasura) where
|
|
toJSON = J.String . Witch.into @Text
|
|
|
|
instance J.FromJSON (EngineLogType Hasura) where
|
|
parseJSON = J.withText "log-type" $ \s ->
|
|
let logTypeText = T.toLower $ T.strip s
|
|
logTypeMaybe = Map.lookup logTypeText allowedLogTypeMapping
|
|
in logTypeMaybe `onNothing` failure
|
|
where
|
|
allowedLogTypeMapping :: Map Text (EngineLogType Hasura)
|
|
allowedLogTypeMapping =
|
|
Map.fromList $ (\lt -> (Witch.into @Text lt, lt)) <$> userAllowedLogTypes
|
|
|
|
failure :: J.Parser (EngineLogType Hasura)
|
|
failure =
|
|
fail $ "Valid list of comma-separated log types: " <> BLC.unpack (J.encode userAllowedLogTypes)
|
|
|
|
data InternalLogTypes
|
|
= -- | mostly for debug logs - see @debugT@, @debugBS@ and @debugLBS@ functions
|
|
ILTUnstructured
|
|
| ILTUnhandledInternalError
|
|
| ILTEventTrigger
|
|
| ILTEventTriggerProcess
|
|
| ILTScheduledTrigger
|
|
| ILTScheduledTriggerProcess
|
|
| ILTCronEventGeneratorProcess
|
|
| -- | internal logs for the websocket server
|
|
ILTWsServer
|
|
| ILTPgClient
|
|
| -- | log type for logging metadata related actions; currently used in logging inconsistent metadata
|
|
ILTMetadata
|
|
| ILTTelemetry
|
|
| ILTSchemaSync
|
|
| ILTSourceCatalogMigration
|
|
| ILTStoredIntrospection
|
|
| ILTStoredIntrospectionStorage
|
|
| ILTModelInfo
|
|
deriving (Show, Eq, Generic)
|
|
|
|
instance Hashable InternalLogTypes
|
|
|
|
instance Witch.From InternalLogTypes Text where
|
|
from = \case
|
|
ILTUnstructured -> "unstructured"
|
|
ILTUnhandledInternalError -> "unhandled-internal-error"
|
|
ILTEventTrigger -> "event-trigger"
|
|
ILTEventTriggerProcess -> "event-trigger-process"
|
|
ILTScheduledTrigger -> "scheduled-trigger"
|
|
ILTScheduledTriggerProcess -> "scheduled-trigger-process"
|
|
ILTCronEventGeneratorProcess -> "cron-event-generator-process"
|
|
ILTWsServer -> "ws-server"
|
|
ILTPgClient -> "pg-client"
|
|
ILTMetadata -> "metadata"
|
|
ILTTelemetry -> "telemetry-log"
|
|
ILTSchemaSync -> "schema-sync"
|
|
ILTSourceCatalogMigration -> "source-catalog-migration"
|
|
ILTStoredIntrospection -> "stored-introspection"
|
|
ILTStoredIntrospectionStorage -> "stored-introspection-storage"
|
|
ILTModelInfo -> "model-info"
|
|
|
|
instance J.ToJSON InternalLogTypes where
|
|
toJSON = J.String . Witch.into @Text
|
|
|
|
-- the default enabled log-types
|
|
defaultEnabledEngineLogTypes :: Set.HashSet (EngineLogType Hasura)
|
|
defaultEnabledEngineLogTypes =
|
|
Set.fromList [ELTStartup, ELTHttpLog, ELTWebhookLog, ELTWebsocketLog, ELTJwkRefreshLog]
|
|
|
|
isEngineLogTypeEnabled :: Set.HashSet (EngineLogType Hasura) -> EngineLogType Hasura -> Bool
|
|
isEngineLogTypeEnabled enabledTypes logTy = case logTy of
|
|
ELTInternal _ -> True
|
|
_ -> logTy `Set.member` enabledTypes
|
|
|
|
readLogTypes :: String -> Either String [EngineLogType Hasura]
|
|
readLogTypes = mapM (J.eitherDecodeStrict' . quote . txtToBs) . T.splitOn "," . T.pack
|
|
where
|
|
quote x = "\"" <> x <> "\""
|
|
|
|
instance EnabledLogTypes Hasura where
|
|
parseEnabledLogTypes = readLogTypes
|
|
defaultEnabledLogTypes = defaultEnabledEngineLogTypes
|
|
isLogTypeEnabled = isEngineLogTypeEnabled
|
|
|
|
-- log types that can be set by the user
|
|
userAllowedLogTypes :: [EngineLogType Hasura]
|
|
userAllowedLogTypes =
|
|
[ ELTStartup,
|
|
ELTHttpLog,
|
|
ELTWebhookLog,
|
|
ELTWebsocketLog,
|
|
ELTQueryLog,
|
|
ELTExecutionLog,
|
|
ELTLivequeryPollerLog,
|
|
ELTActionHandler,
|
|
ELTDataConnectorLog,
|
|
ELTJwkRefreshLog,
|
|
ELTValidateInputLog
|
|
]
|
|
|
|
data LogLevel
|
|
= LevelDebug
|
|
| LevelInfo
|
|
| LevelWarn
|
|
| LevelError
|
|
| LevelOther Text
|
|
deriving (Show, Eq, Ord)
|
|
|
|
instance J.ToJSON LogLevel where
|
|
toJSON = J.toJSON . prettyLogLevel
|
|
|
|
-- | Human-readable LogLevel, as serialized for end-users
|
|
prettyLogLevel :: LogLevel -> Text
|
|
prettyLogLevel = \case
|
|
LevelDebug -> "debug"
|
|
LevelInfo -> "info"
|
|
LevelWarn -> "warn"
|
|
LevelError -> "error"
|
|
LevelOther t -> t
|
|
|
|
-- | This is the top-level log type emitted for OSS and on-prem enterprise. It
|
|
-- is built from the output of 'toEngineLog'
|
|
data EngineLog impl = EngineLog
|
|
{ _elTimestamp :: !FormattedTime,
|
|
_elLevel :: !LogLevel,
|
|
_elType :: !(EngineLogType impl),
|
|
_elDetail :: !J.Value,
|
|
-- | The trace context in which this log message was emitted, if any. See 'unLoggerTracing'.
|
|
_elTraceId :: !(Maybe TraceId),
|
|
-- | The span context in which this log message was emitted, if any. See 'unLoggerTracing'.
|
|
_elSpanId :: !(Maybe SpanId)
|
|
}
|
|
deriving stock (Generic)
|
|
|
|
deriving instance (Show (EngineLogType impl)) => Show (EngineLog impl)
|
|
|
|
deriving instance (Eq (EngineLogType impl)) => Eq (EngineLog impl)
|
|
|
|
instance (J.ToJSON (EngineLogType impl)) => J.ToJSON (EngineLog impl) where
|
|
toJSON = J.genericToJSON hasuraJSON {J.omitNothingFields = True}
|
|
|
|
-- | Typeclass representing any data type that can be converted to @EngineLog@ for the purpose of
|
|
-- logging
|
|
class (EnabledLogTypes impl) => ToEngineLog a impl where
|
|
toEngineLog :: a -> (LogLevel, EngineLogType impl, J.Value)
|
|
|
|
data UnstructuredLog = UnstructuredLog {_ulLevel :: !LogLevel, _ulPayload :: !SB.SerializableBlob}
|
|
deriving (Show)
|
|
|
|
debugT :: Text -> UnstructuredLog
|
|
debugT = UnstructuredLog LevelDebug . SB.fromText
|
|
|
|
debugBS :: B.ByteString -> UnstructuredLog
|
|
debugBS = UnstructuredLog LevelDebug . SB.fromBS
|
|
|
|
debugLBS :: BL.ByteString -> UnstructuredLog
|
|
debugLBS = UnstructuredLog LevelDebug . SB.fromLBS
|
|
|
|
instance ToEngineLog UnstructuredLog Hasura where
|
|
toEngineLog (UnstructuredLog level t) =
|
|
(level, ELTInternal ILTUnstructured, J.toJSON t)
|
|
|
|
-- | Abstract. Constructed with 'mkLoggerCtx'.
|
|
data LoggerCtx impl = LoggerCtx
|
|
{ _lcLoggerSet :: !FL.LoggerSet,
|
|
_lcLogLevel :: !LogLevel,
|
|
_lcTimeGetter :: !(IO FormattedTime),
|
|
_lcEnabledLogTypes :: !(Set.HashSet (EngineLogType impl)),
|
|
-- | @LogsExporter@ or a noop. Wrapped in readIORef to work around cycle at
|
|
-- callsite of @runOtlpLogsExporter@
|
|
_lcLogsExporter :: !(IO (EngineLog impl -> IO ()))
|
|
}
|
|
|
|
getLoggerSet :: LoggerCtx impl -> FL.LoggerSet
|
|
getLoggerSet = _lcLoggerSet
|
|
|
|
getLogLevel :: LoggerCtx impl -> LogLevel
|
|
getLogLevel = _lcLogLevel
|
|
|
|
getTimeGetter :: LoggerCtx impl -> IO FormattedTime
|
|
getTimeGetter = _lcTimeGetter
|
|
|
|
getEnabledLogTypes :: LoggerCtx impl -> Set.HashSet (EngineLogType impl)
|
|
getEnabledLogTypes = _lcEnabledLogTypes
|
|
|
|
getLogsExporter :: LoggerCtx impl -> IO (EngineLog impl -> IO ())
|
|
getLogsExporter = _lcLogsExporter
|
|
|
|
-- * Unhandled Internal Errors
|
|
|
|
-- | We expect situations where there are code paths that should not occur and we throw
|
|
-- an 'error' on this code paths. If our assumptions are incorrect and infact
|
|
-- these errors do occur, we want to log them.
|
|
newtype UnhandledInternalErrorLog = UnhandledInternalErrorLog ErrorCall
|
|
|
|
instance ToEngineLog UnhandledInternalErrorLog Hasura where
|
|
toEngineLog (UnhandledInternalErrorLog (ErrorCallWithLocation err loc)) =
|
|
( LevelError,
|
|
ELTInternal ILTUnhandledInternalError,
|
|
J.object [("error", fromString err), ("location", fromString loc)]
|
|
)
|
|
|
|
-- * LoggerSettings
|
|
|
|
data LoggerSettings = LoggerSettings
|
|
{ -- | should current time be cached (refreshed every sec)? For performance
|
|
-- impact, see benchmarks in: https://github.com/hasura/graphql-engine-mono/pull/10631
|
|
_lsCachedTimestamp :: !Bool,
|
|
_lsTimeZone :: !(Maybe Time.TimeZone),
|
|
_lsLevel :: !LogLevel
|
|
}
|
|
deriving (Show, Eq)
|
|
|
|
defaultLoggerSettings :: Bool -> LogLevel -> LoggerSettings
|
|
defaultLoggerSettings isCached =
|
|
LoggerSettings isCached Nothing
|
|
|
|
-- | Get the current time, formatted with the current or specified timezone
|
|
getFormattedTime :: Maybe Time.TimeZone -> IO FormattedTime
|
|
getFormattedTime tzM = do
|
|
tz <- onNothing tzM Time.getCurrentTimeZone
|
|
t <- Time.getCurrentTime
|
|
return $ FormattedTime t tz
|
|
|
|
-- | Get the current time, formatted with the current or specified timezone
|
|
getCachedFormattedTime :: Maybe Time.TimeZone -> IO FormattedTime
|
|
getCachedFormattedTime tzM = do
|
|
(t, tz, _) <- readIORef cachedRecentFormattedTimeAndZone
|
|
pure $ maybe (FormattedTime t tz) (FormattedTime t) tzM
|
|
|
|
-- | Creates a new 'LoggerCtx', optionally fanning out to an OTLP endpoint
|
|
-- (while enabled) as well.
|
|
--
|
|
-- The underlying 'LoggerSet' is bound to the 'ManagedT' context: when it exits,
|
|
-- the log will be flushed and cleared regardless of whether it was exited
|
|
-- properly or not ('ManagedT' uses 'bracket' underneath). This guarantees that
|
|
-- the logs will always be flushed, even in case of error, avoiding a repeat of
|
|
-- https://github.com/hasura/graphql-engine/issues/4772.
|
|
mkLoggerCtxOTLP ::
|
|
(MonadIO io, MonadBaseControl IO io) =>
|
|
-- | @LogsExporter@ or a noop. Wrapped in readIORef to work around cycle at
|
|
-- callsite of @runOtlpLogsExporter@
|
|
IO (EngineLog impl -> IO ()) ->
|
|
LoggerSettings ->
|
|
Set.HashSet (EngineLogType impl) ->
|
|
ManagedT io (LoggerCtx impl)
|
|
mkLoggerCtxOTLP logsExporter (LoggerSettings shouldCacheTime tzM logLevel) enabledLogs = do
|
|
loggerSet <- allocate acquire release
|
|
pure $ LoggerCtx loggerSet logLevel (timeGetter tzM) enabledLogs logsExporter
|
|
where
|
|
acquire = liftIO do
|
|
FL.newStdoutLoggerSet FL.defaultBufSize
|
|
release loggerSet = liftIO do
|
|
FL.flushLogStr loggerSet
|
|
FL.rmLoggerSet loggerSet
|
|
-- use either a slower time lookup per log line, or quick reference to not
|
|
-- very granular current-ish timestamp
|
|
timeGetter
|
|
| shouldCacheTime = getCachedFormattedTime
|
|
| otherwise = getFormattedTime
|
|
|
|
-- | 'mkLoggerCtxOTLP' but with no otlp log shipping, for compatibility
|
|
mkLoggerCtx ::
|
|
(MonadIO io, MonadBaseControl IO io) =>
|
|
LoggerSettings ->
|
|
Set.HashSet (EngineLogType impl) ->
|
|
ManagedT io (LoggerCtx impl)
|
|
mkLoggerCtx = mkLoggerCtxOTLP (pure (\_ -> pure ()))
|
|
|
|
cleanLoggerCtx :: LoggerCtx a -> IO ()
|
|
cleanLoggerCtx =
|
|
FL.rmLoggerSet . _lcLoggerSet
|
|
|
|
-- | A callback capable of actually emitting a log line (e.g. to stdout). If
|
|
-- not in a 'MonadTrace' context you can make use of the old API via 'Logger'
|
|
-- and 'unLogger'.
|
|
newtype Logger impl = LoggerTracing {unLoggerTracing :: forall a m. (ToEngineLog a impl, Tracing.MonadTraceContext m, MonadIO m) => a -> m ()}
|
|
|
|
-- | This is kept for compatibility with the old interface, which didn't
|
|
-- require a 'MonadTraceContext' environment
|
|
pattern Logger :: forall impl. (forall a m. (ToEngineLog a impl, MonadIO m) => a -> m ()) -> Logger impl
|
|
pattern Logger {unLogger} <- (newToOrig -> unLogger)
|
|
where
|
|
Logger f = LoggerTracing f
|
|
|
|
{-# COMPLETE Logger :: Logger #-}
|
|
|
|
-- Internal. To get 'pattern Logger' to typecheck
|
|
newToOrig :: Logger impl -> (forall a m. (ToEngineLog a impl, MonadIO m) => a -> m ())
|
|
newToOrig (LoggerTracing f) = fmap Tracing.runNoMonadTraceContext f
|
|
|
|
mkLogger :: (J.ToJSON (EngineLogType impl)) => LoggerCtx impl -> Logger impl
|
|
mkLogger (LoggerCtx loggerSet serverLogLevel timeGetter enabledLogTypes logsExporter) = LoggerTracing $ \l -> do
|
|
-- NOTE: This has us logging a trace and span id even in the OSS server,
|
|
-- where tracing isn't actually supported. We decided this was fine, and
|
|
-- actually might end up being useful as a way for OSS users to correlate
|
|
-- logs that are part of the same operation
|
|
cxt <- Tracing.currentContext
|
|
let mbCurrentSpan = tcCurrentSpan <$> cxt
|
|
mbCurrentTrace = tcCurrentTrace <$> cxt
|
|
localTime <- liftIO timeGetter
|
|
let (logLevel, logTy, logDet) = toEngineLog l
|
|
when (logLevel >= serverLogLevel && isLogTypeEnabled enabledLogTypes logTy) $ liftIO do
|
|
let logLine = EngineLog localTime logLevel logTy logDet mbCurrentTrace mbCurrentSpan
|
|
FL.pushLogStrLn loggerSet $ FL.toLogStr (J.encode logLine)
|
|
logsExporter >>= \f -> f logLine
|
|
|
|
nullLogger :: Logger Hasura
|
|
nullLogger = Logger \_ -> pure ()
|
|
|
|
eventTriggerLogType :: EngineLogType Hasura
|
|
eventTriggerLogType = ELTInternal ILTEventTrigger
|
|
|
|
eventTriggerProcessLogType :: EngineLogType Hasura
|
|
eventTriggerProcessLogType = ELTInternal ILTEventTriggerProcess
|
|
|
|
scheduledTriggerLogType :: EngineLogType Hasura
|
|
scheduledTriggerLogType = ELTInternal ILTScheduledTrigger
|
|
|
|
scheduledTriggerProcessLogType :: EngineLogType Hasura
|
|
scheduledTriggerProcessLogType = ELTInternal ILTScheduledTriggerProcess
|
|
|
|
cronEventGeneratorProcessType :: EngineLogType Hasura
|
|
cronEventGeneratorProcessType = ELTInternal ILTCronEventGeneratorProcess
|
|
|
|
sourceCatalogMigrationLogType :: EngineLogType Hasura
|
|
sourceCatalogMigrationLogType = ELTInternal ILTSourceCatalogMigration
|
|
|
|
-- | Emit when stored introspection is used
|
|
data StoredIntrospectionLog = StoredIntrospectionLog
|
|
{ silMessage :: Text,
|
|
-- | upstream data source errors
|
|
silSourceError :: QErr
|
|
}
|
|
deriving stock (Generic)
|
|
|
|
instance J.ToJSON StoredIntrospectionLog where
|
|
toJSON = J.genericToJSON hasuraJSON
|
|
|
|
instance ToEngineLog StoredIntrospectionLog Hasura where
|
|
toEngineLog siLog =
|
|
(LevelInfo, ELTInternal ILTStoredIntrospection, J.toJSON siLog)
|
|
|
|
-- | Logs related to errors while interacting with the stored introspection
|
|
-- storage
|
|
data StoredIntrospectionStorageLog = StoredIntrospectionStorageLog
|
|
{ sislMessage :: Text,
|
|
sislError :: QErr
|
|
}
|
|
deriving stock (Generic)
|
|
|
|
instance J.ToJSON StoredIntrospectionStorageLog where
|
|
toJSON = J.genericToJSON hasuraJSON
|
|
|
|
instance ToEngineLog StoredIntrospectionStorageLog Hasura where
|
|
toEngineLog sisLog =
|
|
(LevelInfo, ELTInternal ILTStoredIntrospectionStorage, J.toJSON sisLog)
|
|
|
|
-- | A logger useful for accumulating and logging stats, in tight polling loops. It also
|
|
-- debounces to not flood with excessive logs. Use @'logStats' to record statistics for logging.
|
|
createStatsLogger ::
|
|
forall m stats impl.
|
|
( MonadIO m,
|
|
ToEngineLog stats impl,
|
|
Monoid stats
|
|
) =>
|
|
Logger impl ->
|
|
m (FDebounce.Trigger stats stats)
|
|
createStatsLogger hasuraLogger =
|
|
liftIO $ FDebounce.new debounceArgs debounceOpts
|
|
where
|
|
logDelay :: Int
|
|
logDelay =
|
|
-- Accumulate stats occurred within 10 minutes and log once.
|
|
10 * 60 * 1000_000 -- 10 minutes
|
|
debounceArgs :: FDebounce.Args stats stats
|
|
debounceArgs =
|
|
FDebounce.Args
|
|
{ FDebounce.cb = unLogger hasuraLogger, -- Log using the Hasura logger
|
|
FDebounce.fold = (<>),
|
|
FDebounce.init = mempty
|
|
}
|
|
|
|
debounceOpts :: FDebounce.Opts stats stats
|
|
debounceOpts = FDebounce.def {FDebounce.delay = logDelay}
|
|
|
|
-- Orphan instance. Required for @'closeStatsLogger'.
|
|
instance (EnabledLogTypes impl) => ToEngineLog (FDebounce.OpException, EngineLogType impl) impl where
|
|
toEngineLog (opException, logType) =
|
|
let errorMessage :: Text
|
|
errorMessage = case opException of
|
|
FDebounce.AlreadyClosedException -> "already closed"
|
|
FDebounce.UnexpectedClosedException _someException -> "closed unexpectedly"
|
|
in (LevelWarn, logType, J.object ["message" J..= ("cannot close fetched events stats logger: " <> errorMessage)])
|
|
|
|
-- | Safely close the statistics logger. When occurred, exception is logged.
|
|
closeStatsLogger :: (MonadIO m, EnabledLogTypes impl) => EngineLogType impl -> Logger impl -> FDebounce.Trigger stats stats -> m ()
|
|
closeStatsLogger logType (Logger hasuraLogger) debounceLogger =
|
|
liftIO $ catch (FDebounce.close debounceLogger) $ \(e :: FDebounce.OpException) -> hasuraLogger (e, logType)
|
|
|
|
-- | This won't log the given stats immediately.
|
|
-- The stats are accumulated over the specific timeframe and logged only once.
|
|
-- See @'createStatsLogger' for more details.
|
|
logStats :: (MonadIO m) => FDebounce.Trigger stats stats -> stats -> m ()
|
|
logStats debounceTrigger = liftIO . FDebounce.send debounceTrigger
|