-- Hoogle documentation, generated by Haddock -- See Hoogle, http://www.haskell.org/hoogle/ -- | Advanced telemetry -- -- This is part of a library to help build command-line programs, both -- tools and longer-running daemons. -- -- This package in particular adds helpers for recording telemetry for -- subsequent analysis. You can instrument your code with tracing and -- spans, and also emit events carrying arbitrary metadata. Backends are -- provided for structured logs, sending traces to observability -- services, or even just outputting the telemetry to console. -- -- See Core.Telemetry.Observability to get started. @package core-telemetry @version 0.2.3.4 -- | A simple exporter backend that prints your metrics to the terminal as -- they are submitted. -- -- Taking the example from telemetry, the output would be: -- --
--   09:58:54Z (03.755) Process order:
--     calories = 667.0
--     flavour = true
--     meal_name = "hamburger"
--     precise = 45.0
--   
module Core.Telemetry.Console -- | Output metrics to the terminal. This is mostly useful for debugging, -- but it can also be used as general output mechanism if your program is -- mostly concerned with gathering metrics and displaying them. consoleExporter :: Exporter -- | A generic exporter that may someday send metrics to an OpenTelemetry -- backend. -- -- unimplemented module Core.Telemetry.General generalExporter :: Exporter -- | A exporter backend that sends telemetry in the form of traces of your -- application's behaviour, or event data—accompanied either way by -- [conceivably very wide] additional metadata—to the Honeycomb -- observability service. -- -- When specifying the honeycombExporter you have to specify -- certain command-line options and environment variables to enable it: -- --
--   $ export HONEYCOMB_TEAM="62e3626a2cc34475adef4d799eca0407"
--   $ burger-service --telemetry=honeycomb --dataset=prod-restaurant-001
--   
-- -- If you annotate your program with spans, you can get a trace like -- this: -- -- -- Notice -- -- This library is Open Source but the Honeycomb service is not. -- Honeycomb offers a free tier which is quite suitable for individual -- use and small local applications. In the future you may be able to -- look at Core.Telemetry.General if you instead want to forward -- to a generic OpenTelemetry provider. module Core.Telemetry.Honeycomb -- | Indicate which "dataset" spans and events will be posted into type Dataset = Rope -- | Configure your application to send telemetry in the form of spans and -- traces to the Honeycomb observability service. -- --
--   context <- configure ...
--   context' <- initializeTelemetry [honeycombExporter] context
--   executeWith context' ...
--   
honeycombExporter :: Exporter -- | Machinery for generating identifiers to be used in traces and spans. -- Meets the requirements of the W3C Trace Context specification, -- specifically as relates to forming trace identifiers and span -- identifiers into traceparent headers. The key requirements -- are that traces be globally unique and that spans be unique within a -- trace. module Core.Telemetry.Identifiers -- | Get the identifier of the current trace, if you are within a trace -- started by beginTrace or usingTrace. getIdentifierTrace :: Program τ (Maybe Trace) -- | Get the identifier of the current span, if you are currently within a -- span created by encloseSpan. getIdentifierSpan :: Program τ (Maybe Span) -- | Override the identifier of the current span, if you are currently -- within a span created by encloseSpan. This is an unsafe action, -- specifically and only for the situation where you need create a parent -- span for an asynchronous process whose unique identifier has already -- been nominated. In this scenario all child spans would already have -- been created with this span identifier as their parent, leaving you -- with the final task of creating a "root" span within the trace with -- that parent identifier. setIdentifierSpan :: Span -> Program t () -- | Generate an identifier suitable for use in a trace context. Trace -- identifiers are 16 bytes. We incorporate the time to nanosecond -- precision, the host system's MAC address, and a random element. This -- is similar to a version 1 UUID, but we render the least significant -- bits of the time stamp ordered first so that visual distinctiveness is -- on the left. The MAC address in the lower 48 bits is not -- reversed, leaving the most distinctiveness [the actual host as opposed -- to manufacturer OIN] hanging on the right hand edge of the identifier. -- The two bytes of supplied randomness are put in the middle. createIdentifierTrace :: Time -> Word16 -> MAC -> Trace -- | Generate an identifier for a span. We only have 8 bytes to work with. -- We use the nanosecond prescision Time with the nibbles reversed, and -- then overwrite the last two bytes with the supplied random value. createIdentifierSpan :: Time -> Word16 -> Span -- | Get the MAC address of the first interface that's not the loopback -- device. If something goes weird then we return a valid but bogus -- address (in the locally administered addresses block). hostMachineIdentity :: MAC -- | Render the Trace and Span identifiers representing a -- span calling onward to another component in a distributed system. The -- W3C Trace Context recommendation specifies the HTTP header -- traceparent with a version sequence (currently hard coded at -- 00), the 16 byte trace identifier, the 8 byte span -- identifier, and a flag sequence (currently quite ignored), all -- formatted as follows: -- --
--   traceparent: 00-fd533dbf96ecdc610156482ae36c24f7-1d1e9dbf96ec4649-00
--   
createTraceParentHeader :: Trace -> Span -> Rope -- | Parse a traceparent header into a Trace and -- Span, assuming it was a valid pair according to the W3C Trace -- Context recommendation. The expectation is that, if present in an HTTP -- request, these values would be passed to usingTrace to allow -- the program to contribute spans to an existing trace started by -- another program or service. parseTraceParentHeader :: Rope -> Maybe (Trace, Span) -- | Traditional "monitoring" systems were concerned with gathering -- together obscene quantities of metrics and graphing them. This makes -- for very pretty billboard displays in Network Operations -- Centers which impress visitors tremendously, but (it turns out) are of -- limited use when actually trying to troubleshoot problems or improve -- the performance of our systems. We all put a lot of effort into trying -- to detect anamolies but really, despite person-centuries of effort, -- graphing raw system metrics doesn't get us as far as we would have -- liked. -- -- Experience with large-scale distributed systems has led to the insight -- that what you need is to be able to trace the path a request takes as -- it moves through a system, correlating and comparing this trace to -- others like it. This has led to the modern "observability" movement, -- more concerned with metrics which descirbe user-visible experience, -- service levels, error budgets, and being able to do ad-hoc analysis of -- evolving situations. -- -- This library aims to support both models of using telemetry, with the -- primary emphasis being on the traces and spans that can -- be connected together by an observability tool. -- --

Usage

-- -- To use this capability, first you need to initialize the telemetry -- subsystem with an appropriate exporter: -- --
--   import Core.Program
--   import Core.Telemetry
--   
--   main :: IO ()
--   main = do
--       context <- configure "1.0" None (simpleConfig [])
--       context' <- initializeTelemetry [consoleExporter, structuredExporter, honeycombExporter] context
--       executeWith context' program
--   
-- -- Then when you run your program you can pick the exporter: -- --
--   $ burgerservice --telemetry=structured
--   
-- -- to activate sending telemetry, in this case, to the console in the -- form of structured JSON logs. Other exporters add additional -- command-line options with which to configure how and where the metrics -- will be sent. -- --

Traces and Spans

-- -- At the top of your program or request loop you need to start a new -- trace (with beginTrace) or continue one inherited from another -- service (with usingTrace): -- --
--   program :: Program None ()
--   program = do
--       beginTrace $ do
--           encloseSpan "Service request" $ do
--   
--               -- do stuff!
--   
--               ...
--   
--               obs <- currentSkyObservation
--               temp <- currentAirTemperature
--   
--               ...
--   
--               -- add appropriate telemetry values to the span
--               telemetry
--                   [ metric "sky_colour" (colourFrom obs)
--                   , metric "temperature" temp
--                   ]
--   
-- -- will result in sky_colour="Blue" and -- temperature=26.1 or whatever being sent by the telemetry -- system to the observability service that's been activated. -- -- The real magic here is that spans nest. As you go into each -- subcomponent on your request path you can again call -- encloseSpan creating a new span, which can have its own -- telemetry: -- --
--   currentSkyObservation :: Program None Observation
--   currentSkyObservation = do
--       encloseSpan "Observe sky" $ do
--           ...
--   
--           telemetry
--               [ metric "radar_frequency" freq
--               , metric "cloud_cover" blockageLevel
--               ]
--   
--           pure result
--   
-- -- Any metrics added before entering the new span will be inherited by -- the subspan and sent when it finishes so you don't have to keep -- re-attaching data if it's common across all the spans in your trace. -- --

Events

-- -- In other circumstances you will just want to send metrics: -- --
--   -- not again!
--   sendEvent "Cat meowed"
--       [ metric "room" ("living room" :: Rope)
--       , metric "volume" (127.44 :: Float) -- decibels
--       , metric "apparently_hungry" True
--       ]
--   
-- -- will result in room="living room", volume=127.44, -- and apparently_hungry=true being sent as you'd expect. -- Ordinarily when you call metric you are passing in a variable -- that already has a type, but when hardcoding literals like in this -- example (less common but not unheard of) you'll need to add a type -- annotation. -- -- You do not have to call sendEvent from within a span, -- but if you do appropriate metadata will be added to help the -- observability system link the event to the context of the span it -- occured during. -- -- Either way, explicitly sending an event, or upon exiting a span, the -- telemetry will be gathered up and sent via the chosen exporter and -- forwarded to the observability or monitoring service you have chosen. module Core.Telemetry.Observability -- | Activate the telemetry subsystem for use within the Program -- monad. -- -- Each exporter specified here will add setup and configuration to the -- context, including command-line options and environment variables -- needed as approrpiate: -- --
--   context' <- initializeTelemetry [consoleExporter] context
--   
-- -- This will allow you to then select the appropriate backend at runtime: -- --
--   $ burgerservice --telemetry=console
--   
-- -- which will result in it spitting out metrics as it goes, -- --
--   calories = 667.0
--   flavour = true
--   meal_name = "hamburger"
--   precise = 45.0
--   
-- -- and so on. initializeTelemetry :: [Exporter] -> Context τ -> IO (Context τ) -- | Unique identifier for a trace. If your program is the top of an -- service stack then you can use beginTrace to generate a new -- idenfifier for this request or iteration. More commonly, however, you -- will inherit the trace identifier from the application or service -- which invokes this program or request handler, and you can specify it -- by using usingTrace. newtype Trace Trace :: Rope -> Trace -- | Unique identifier for a span. This will be generated by -- encloseSpan but for the case where you are continuing an -- inherited trace and passed the identifier of the parent span you can -- specify it using this constructor. newtype Span Span :: Rope -> Span -- | Send a span value up by hand. -- -- This handles a number of convenient things for you, and takes care of -- a few edge cases. -- -- Start a new trace. A random identifier will be generated. -- -- You must have a single "root span" immediately below starting a -- new trace. -- --
--   program :: Program None ()
--   program = do
--       beginTrace $ do
--           encloseSpan "Service Request" $ do
--               ...
--   
beginTrace :: Program τ α -> Program τ α -- | Continue an existing trace using a Trace identifier and parent -- Span identifier sourced externally. This is the most common -- case. Internal services that play a part of a larger request will -- inherit a job identifier, sequence number, or other externally -- supplied unique code. Even an internet-facing web service might have a -- correlation ID provided by the outside load balancers. -- --
--   program :: Program None ()
--   program = do
--   
--       -- do something that gets the trace ID
--       trace <- ...
--   
--       -- and something to get the parent span ID
--       parent <- ...
--   
--       usingTrace (Trace trace) (Span parent) $ do
--           encloseSpan "Internal processing" $ do
--               ...
--   
usingTrace :: Trace -> Span -> Program τ α -> Program τ α -- | Create a new trace with the specified Trace identifier. Unlike -- usingTrace this does not set the parent Span -- identifier, thereby marking this as a new trace and causing the first -- span enclosed within this trace to be considered the "root" span of -- the trace. This is unusual and should only expected to be used in -- concert with the setIdentifierSpan override to create a root -- spans in asynchronous processes after all the child spans have -- already been composed and sent. -- -- Most times, you don't need this. You're much better off using -- beginTrace to create a root span. However, life is not kind, -- and sometimes bad things happen to good abstractions. Maybe you're -- tracing your build system, which isn't obliging enough to be all -- contained in one Haskell process, but is a half-dozen steps shotgunned -- across several different processes. In situations like this, it's -- useful to be able to generate a Trace identifier and -- Span identifier, use that as the parent across several -- different process executions, hanging children spans off of this as -- you go, then manually send up the root span at the end of it all. -- --
--   trace <- ...
--   unique <- ...
--   
--   -- many child spans in other processes have used these as trace
--   -- identifiers and parent span identifier. Now form the root span thereby
--   -- finishing the trace.
--   
--   usingTrace' trace $ do
--       encloseSpan "Launch Missiles" $ do
--           setStartTime start
--           setIdentifierSpan unique
--           telemetry
--               [ metric ...
--               ]
--   
usingTrace' :: Trace -> Program τ α -> Program τ α -- | Record the name of the service that this span and its children are a -- part of. A reasonable default is the name of the binary that's -- running, but frequently you'll want to put something a bit more -- nuanced or specific to your application. This is the overall name of -- the independent service, component, or program complimenting the -- label set when calling encloseSpan, which by contrast -- descibes the name of the current phase, step, or even function name -- within the overall scope of the "service". -- -- This will end up as the service.name parameter when exported. setServiceName :: Rope -> Program τ () -- | Begin a span. -- -- You need to call this from within the context of a trace, which is -- established either by calling beginTrace or usingTrace -- somewhere above this point in the program. -- -- You can nest spans as you make your way through your program, which -- means each span has a parent (except for the first one, which is the -- root span) In the context of a trace, allows an observability tool to -- reconstruct the sequence of events and to display them as a nested -- tree correspoding to your program flow. -- -- The current time will be noted when entering the Program this -- span encloses, and its duration recorded when the sub Program -- exits. Start time, duration, the unique identifier of the span -- (generated for you), the identifier of the parent, and the unique -- identifier of the overall trace will be appended as metadata points -- and then sent to the telemetry channel. encloseSpan :: Label -> Program z a -> Program z a -- | Override the start time of the current span. -- -- Under normal circumstances this shouldn't be necessary. The start and -- end of a span are recorded automatically when calling -- encloseSpan. Observabilty tools are designed to be used live; -- traces and spans should be created in real time in your code. setStartTime :: Time -> Program τ () -- | A telemetry value that can be sent over the wire. This is a wrapper -- around JSON values of type string, number, or boolean. You create -- these using the metric method provided by a Telemetry -- instance and passing them to the telemetry function in a span -- or sendEvent if noting an event. data MetricValue -- | Add measurements to the current span. -- --
--   telemetry
--       [ metric "calories" (667 :: Int)
--       , metric "precise" measurement
--       , metric "meal_name" ("hamburger" :: Rope)
--       , metric "flavour" True
--       ]
--   
-- -- The metric function is a method provided by instances of the -- Telemtetry typeclass which is mostly a wrapper around -- constructing key/value pairs suitable to be sent as measurements up to -- an observability service. telemetry :: [MetricValue] -> Program τ () -- | Record telemetry about an event. Specify a label for the event and -- then whichever metrics you wish to record. -- -- The emphasis of this package is to create traces and spans. There are, -- however, times when you just want to send telemetry about an event. -- You can use sendEvent to accomplish this. -- -- If you do call sendEvent within an enclosing span created with -- encloseSpan (the usual and expected use case) then this event -- will be "linked" to this span so that the observability tool can -- display it attached to the span in the in which it occured. -- --
--   sendEvent
--       "Make tea"
--       [ metric "sugar" False
--       ]
--   
sendEvent :: Label -> [MetricValue] -> Program τ () -- | Reset the accumulated metadata metrics to the emtpy set. -- -- This isn't something you'd need in normal circumstances, as inheriting -- contextual metrics from surrounding code is usually what you want. But -- if you have a significant change of setting then clearing the attached -- metadata may be appropriate; after all, observability tools -- visualizing a trace will show you the context an event was encountered -- in. clearMetrics :: Program τ () instance GHC.Show.Show Core.Telemetry.Observability.MetricValue instance Core.Telemetry.Observability.Telemetry GHC.Types.Int instance Core.Telemetry.Observability.Telemetry GHC.Int.Int32 instance Core.Telemetry.Observability.Telemetry GHC.Int.Int64 instance Core.Telemetry.Observability.Telemetry GHC.Word.Word32 instance Core.Telemetry.Observability.Telemetry GHC.Word.Word64 instance Core.Telemetry.Observability.Telemetry GHC.Integer.Type.Integer instance Core.Telemetry.Observability.Telemetry GHC.Types.Float instance Core.Telemetry.Observability.Telemetry GHC.Types.Double instance Core.Telemetry.Observability.Telemetry Data.Scientific.Scientific instance Core.Telemetry.Observability.Telemetry Core.Text.Rope.Rope instance Core.Telemetry.Observability.Telemetry GHC.Base.String instance Core.Telemetry.Observability.Telemetry () instance Core.Telemetry.Observability.Telemetry Data.ByteString.Internal.ByteString instance Core.Telemetry.Observability.Telemetry Data.ByteString.Lazy.Internal.ByteString instance Core.Telemetry.Observability.Telemetry Data.Text.Internal.Text instance Core.Telemetry.Observability.Telemetry Data.Text.Internal.Lazy.Text instance Core.Telemetry.Observability.Telemetry GHC.Types.Bool instance Core.Telemetry.Observability.Telemetry Core.Encoding.Json.JsonValue -- | 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 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 -- | Output metrics to stdout in the form of a raw JSON object. structuredExporter :: Exporter -- | Support for building observability into your program by marking spans -- and sub-spans, forming them into traces, and sending them to a backend -- capable of ingesting such telemetry and performing analysis on it. -- -- This is intended to be used directly: -- --
--   import Core.Telemetry
--   
-- -- the submodules are mostly there to group documentation, along with -- grouping the implementations for each of the different supported -- backends. module Core.Telemetry