core-telemetry-0.2.7.2: lib/Core/Telemetry/Structured.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralisedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{- |
An exporter backend that outputs \"structured\" logs to your terminal as they are
submitted.
Most traditional programs' logs were textual, single lines, sometimes with a
reasonably well-known internal layout, but regardless very difficult to
actually perform analysis on. Engineers attempting to diagnose problems are
largely limited to doing text searches across masses of logs. It's hard to
correlate between diffent subsystems let alone perform any sort of statistical
analysis.
Other systems in the past gathered copious amounts of metrics but having done
so, left us with the hard problem of actually doing anything useful with them
other than providing fodder for pretty graphs.
Structured logging was a significant step forward for large-scale systems
administration; by combining metrics together with context in the form of
key/value pairs it allows us to perform more detailed investigation and
analysis that this was largely done by emitting copious amounts of enormously
wasteful JSON is astonishing and goes some way to explain why structured
logging took so long to catch on).
Taking the example from 'Core.Telemetry.Observability.telemetry', the output
would be:
@
\$ __burgerservice --telemetry=structured__
{"calories":667.0,"flavour":true,"meal_name":"hamburger","precise":45.0,"timestamp":"2021-10-22T11:12:53.674399531Z"}
...
@
which if pretty printed would have been more recognizable as
@
{
"calories": 667.0,
"flavour": true,
"meal_name": "hamburger",
"precise": 45.0,
"timestamp": "2021-10-22T11:12:53.674399531Z",
}
@
but all that whitespace would be wasteful, right?
While more advanced observability systems will directly ingest this data and
assemble it into traces of nested spans, in other situations having
straight-forward metrics output as JSON may be sufficient for your needs. If
you /do/ use this exporter in a program embellished with traces and spans, the
relevant contextual information will be added to the output:
@
{
"calories": 667.0,
"flavour": true,
"meal_name": "hamburger",
"precise": 45.0,
"timestamp": "2021-10-22T11:12:53.674399531Z",
"duration": 3.756717001,
"span_id": "o7ucNqCeSJBzeviL",
"span_name": "Process order",
"trace_id": "order-11430185",
"service_name": "burger-service"
}
@
-}
module Core.Telemetry.Structured
( structuredExporter
) where
import Control.Concurrent.STM (atomically)
import Control.Concurrent.STM.TQueue (TQueue, writeTQueue)
import Core.Data.Structures (insertKeyValue)
import Core.Encoding.Json
import Core.Program.Arguments
import Core.Program.Context
import Core.Text.Rope
{- |
Output metrics to @stdout@ in the form of a raw JSON object.
-}
structuredExporter :: Exporter
structuredExporter =
Exporter
{ codenameFrom = "structured"
, setupConfigFrom = setupStructuredConfig
, setupActionFrom = setupStructuredAction
}
setupStructuredConfig :: Config -> Config
setupStructuredConfig = id
setupStructuredAction :: Context τ -> IO Forwarder
setupStructuredAction context = do
let out = outputChannelFrom context
pure
( Forwarder
{ telemetryHandlerFrom = processStructuredOutput out
}
)
-- almost exact copy of what we use to send to Honeycomb
convertDatumToJson :: Datum -> JsonValue
convertDatumToJson datum =
let spani = spanIdentifierFrom datum
trace = traceIdentifierFrom datum
parent = parentIdentifierFrom datum
meta0 = attachedMetadataFrom datum
meta1 = insertKeyValue "span_name" (JsonString (spanNameFrom datum)) meta0
meta2 = case spani of
Nothing -> meta1
Just value -> insertKeyValue "span_id" (JsonString (unSpan value)) meta1
meta3 = case parent of
Nothing -> meta2
Just value -> insertKeyValue "parent_id" (JsonString (unSpan value)) meta2
meta4 = case trace of
Nothing -> meta3
Just value -> insertKeyValue "trace_id" (JsonString (unTrace value)) meta3
meta5 = case serviceNameFrom datum of
Nothing -> meta4
Just service -> insertKeyValue "service_name" (JsonString service) meta4
meta6 = case durationFrom datum of
Nothing -> meta5
Just duration ->
insertKeyValue
"duration"
(JsonNumber (fromRational (toRational duration / 1e9)))
meta5
time = intoRope (show (spanTimeFrom datum))
meta7 = insertKeyValue "timestamp" (JsonString time) meta6
in JsonObject meta7
processStructuredOutput :: TQueue (Maybe Rope) -> [Datum] -> IO ()
processStructuredOutput out datums = do
mapM_ processOne datums
where
processOne :: Datum -> IO ()
processOne datum = do
let object = convertDatumToJson datum
text = intoRope (encodeToUTF8 object)
atomically $ do
writeTQueue out (Just text)