nri-observability-0.1.0.0: src/Reporter/File/Internal.hs
module Reporter.File.Internal where
import qualified Control.Concurrent.Async as Async
import qualified Control.Concurrent.STM as STM
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Encoding
import qualified Data.ByteString.Lazy as ByteString
import qualified Data.Foldable
import qualified Environment
import qualified Network.HostName
import qualified Platform
import qualified Platform.ReporterHelpers as Helpers
import qualified Platform.Timer as Timer
import qualified System.IO
import qualified System.Random as Random
import qualified Prelude
-- Log tracing information for a request to a file. Tracing information contains
-- nested spans but will appear flattend in the log. Each tracing span will
-- appear on its own line in the log, ordered by its start date.
--
-- Example usage:
--
-- > settings <- File.decode
-- > handler <- File.handler settings
-- > File.report handler "request-id" span
report :: Handler -> Text -> Platform.TracingSpan -> Prelude.IO ()
report Handler {skipLogging, writeQueue, logContext} requestId span = do
skip <- skipLogging span
if skip
then Prelude.pure ()
else
logItemRecursively logContext {requestId} span []
|> STM.writeTBQueue writeQueue
|> (`STM.orElse` Prelude.pure ()) -- Drop the log if the queue is full.
|> STM.atomically
logItemRecursively ::
LogContext ->
Platform.TracingSpan ->
[ByteString.ByteString] ->
[ByteString.ByteString]
logItemRecursively logContext span acc =
logItemRecursivelyHelper logContext span acc
|> List.reverse
logItemRecursivelyHelper ::
LogContext ->
Platform.TracingSpan ->
[ByteString.ByteString] ->
[ByteString.ByteString]
logItemRecursivelyHelper logContext span acc =
List.foldr -- The right-most child is the oldest, so that's where we start
( logItemRecursivelyHelper
logContext
{ namespace = Platform.name span : namespace logContext
}
)
(logItem logContext span : acc)
(Platform.children span)
logItem :: LogContext -> Platform.TracingSpan -> ByteString.ByteString
logItem LogContext {timer, namespace, environment, requestId, hostname} span =
(Aeson..=) "name" (Platform.name span)
++ (Aeson..=) "start_utc" (Timer.toUTC timer (Platform.started span))
++ (Aeson..=) "duration_ms" (Prelude.fromIntegral (Timer.durationInUs span) / 1000)
++ (Aeson..=) "env" environment
++ (Aeson..=) "namespace" namespace
++ (Aeson..=) "request-id" requestId
++ (Aeson..=) "hostname" hostname
++ ( case Platform.frame span of
Nothing -> Prelude.mempty
Just (_, srcLoc) ->
(Aeson..=) "src" (Helpers.srcString srcLoc)
)
++ (Aeson..=) "allocated_mb" (toFloat (Platform.allocated span) / 1e6)
++ (Aeson..=) "details" (Platform.details span)
|> Aeson.pairs
|> Data.Aeson.Encoding.encodingToLazyByteString
data Handler = Handler
{ fileHandle :: System.IO.Handle,
logContext :: LogContext,
skipLogging :: Platform.TracingSpan -> Prelude.IO Bool,
writeQueue :: STM.TBQueue [ByteString.ByteString],
loggingThread :: Async.Async ()
}
data LogContext = LogContext
{ timer :: Timer.Timer,
namespace :: [Text],
environment :: Text,
requestId :: Text,
hostname :: Text
}
-- | Create a 'Handler' for a specified set of 'Settings'. Do this once when
-- your application starts and reuse the 'Handler' you get.
handler :: Settings -> Prelude.IO Handler
handler settings = do
let skipLogging span =
case Platform.succeeded span of
Platform.Succeeded -> do
roll <- Random.randomRIO (0.0, 1.0)
Prelude.pure (roll > fractionOfSuccessRequestsLogged settings)
Platform.Failed -> Prelude.pure False
Platform.FailedWith _ -> Prelude.pure False
fileHandle <- System.IO.openFile (logFile settings) System.IO.AppendMode
hostname <- map Text.fromList Network.HostName.getHostName
writeQueue <- STM.atomically (STM.newTBQueue 100)
loggingThread <- Async.async (logLoop writeQueue fileHandle)
timer <- Timer.mkTimer
let logContext =
LogContext
{ timer,
hostname,
requestId = "", -- This changes per request and so is set later.
namespace = [appName settings],
environment = appEnvironment settings
}
Prelude.pure
Handler
{ fileHandle,
skipLogging,
writeQueue,
loggingThread,
logContext
}
logLoop ::
STM.TBQueue [ByteString.ByteString] ->
System.IO.Handle ->
Prelude.IO ()
logLoop writeQueue fileHandle = do
lines <- STM.atomically (STM.readTBQueue writeQueue)
Data.Foldable.for_ lines <| \line -> do
ByteString.hPut fileHandle line
ByteString.hPut fileHandle "\n"
logLoop writeQueue fileHandle
cleanup :: Handler -> Prelude.IO ()
cleanup Handler {loggingThread, fileHandle} = do
Async.cancel loggingThread
System.IO.hClose fileHandle
-- | Configuration settings for this reporter. A value of this type can be read
-- from the environment using the 'decoder' function.
data Settings = Settings
{ -- | The file to log too. On unix systems you can set this to @/dev/stdout@
-- in order to log to stdout.
--
-- [@environment variable@] LOG_FILE
-- [@default value@] app.log
logFile :: Prelude.FilePath,
-- | The name of this application. This will be attached to all logs.
--
-- [@environment variable@] LOG_ROOT_NAMESPACE
-- [@default value@] your-application-name-here
appName :: Text,
-- | The environment this application is running in. This will be attached
-- to all logs.
--
-- [@environment variable@] ENVIRONMENT
-- [@default value@] development
appEnvironment :: Text,
-- | The fraction of requests that should be logged. Set to 1 if you want to
-- log everything, and a lower value to save money.
--
-- [@environment variable@] FRACTION_OF_SUCCESS_REQUESTS_LOGGED
-- [@default value@] 1
fractionOfSuccessRequestsLogged :: Float
}
-- | 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 =
Prelude.pure Settings
|> andMap logFileDecoder
|> andMap appNameDecoder
|> andMap environmentDecoder
|> andMap fractionOfSuccessRequestsLoggedDecoder
logFileDecoder :: Environment.Decoder Prelude.FilePath
logFileDecoder =
Environment.variable
Environment.Variable
{ Environment.name = "LOG_FILE",
Environment.description = "File to log too.",
Environment.defaultValue = "app.log"
}
Environment.filePath
appNameDecoder :: Environment.Decoder Text
appNameDecoder =
Environment.variable
Environment.Variable
{ Environment.name = "LOG_ROOT_NAMESPACE",
Environment.description = "Root of the log namespace. This should be the name of the application.",
Environment.defaultValue = "your-application-name-here"
}
Environment.text
environmentDecoder :: Environment.Decoder Text
environmentDecoder =
Environment.variable
Environment.Variable
{ Environment.name = "ENVIRONMENT",
Environment.description = "Environment to display in logs.",
Environment.defaultValue = "development"
}
Environment.text
fractionOfSuccessRequestsLoggedDecoder :: Environment.Decoder Float
fractionOfSuccessRequestsLoggedDecoder =
Environment.variable
Environment.Variable
{ Environment.name = "FRACTION_OF_SUCCESS_REQUESTS_LOGGED",
Environment.description = "The fraction of successful requests logged. Defaults to logging all successful requests.",
Environment.defaultValue = "1"
}
Environment.float