diff --git a/core-telemetry.cabal b/core-telemetry.cabal
--- a/core-telemetry.cabal
+++ b/core-telemetry.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           core-telemetry
-version:        0.1.6.1
+version:        0.1.7.0
 synopsis:       Advanced telemetry
 description:    This is part of a library to help build command-line programs, both tools and
                 longer-running daemons.
@@ -37,9 +37,10 @@
 library
   exposed-modules:
       Core.Telemetry
-      Core.Telemetry.Observability
       Core.Telemetry.Console
       Core.Telemetry.Honeycomb
+      Core.Telemetry.Observability
+      Core.Telemetry.Structured
   hs-source-dirs:
       lib
   ghc-options: -Wall -Wwarn -fwarn-tabs
@@ -48,8 +49,8 @@
     , base >=4.11 && <5
     , bytestring
     , chronologique
-    , core-data
-    , core-program >=0.3.0.7
+    , core-data >=0.2.1.11
+    , core-program >=0.3.0.8
     , core-text >=0.3.5
     , exceptions
     , http-streams
diff --git a/lib/Core/Telemetry.hs b/lib/Core/Telemetry.hs
--- a/lib/Core/Telemetry.hs
+++ b/lib/Core/Telemetry.hs
@@ -27,8 +27,10 @@
     -- Processors to export telemetry to a backend.
     module Core.Telemetry.Console,
     module Core.Telemetry.Honeycomb,
+    module Core.Telemetry.Structured
 ) where
 
 import Core.Telemetry.Console
 import Core.Telemetry.Honeycomb
 import Core.Telemetry.Observability
+import Core.Telemetry.Structured
diff --git a/lib/Core/Telemetry/Console.hs b/lib/Core/Telemetry/Console.hs
--- a/lib/Core/Telemetry/Console.hs
+++ b/lib/Core/Telemetry/Console.hs
@@ -3,7 +3,19 @@
 {-# LANGUAGE OverloadedStrings #-}
 
 {- |
-Implementations of different backends that telemetry can be exported to.
+A simple exporter backend that prints your metrics to the terminal as they are
+submitted.
+
+Taking the example from 'Core.Telemetry.Observability.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 (
     consoleExporter,
@@ -22,8 +34,11 @@
 import Core.Text.Utilities
 import qualified Data.List as List
 
--- TODO convert this into a Render instance
-
+{-|
+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
 consoleExporter =
     Exporter
@@ -40,12 +55,12 @@
     let out = outputChannelFrom context
     pure
         ( Forwarder
-            { telemetryHandlerFrom = process out
+            { telemetryHandlerFrom = processConsoleOutput out
             }
         )
 
-process :: TQueue (Maybe Rope) -> [Datum] -> IO ()
-process out datums = do
+processConsoleOutput :: TQueue (Maybe Rope) -> [Datum] -> IO ()
+processConsoleOutput out datums = do
     mapM_ processOne datums
   where
     processOne :: Datum -> IO ()
diff --git a/lib/Core/Telemetry/Honeycomb.hs b/lib/Core/Telemetry/Honeycomb.hs
--- a/lib/Core/Telemetry/Honeycomb.hs
+++ b/lib/Core/Telemetry/Honeycomb.hs
@@ -4,7 +4,7 @@
 {-# OPTIONS_GHC -fno-warn-unused-imports #-}
 
 {- |
-A backend exporter that sends telemetry in the form of traces of your
+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.
 
@@ -187,8 +187,6 @@
                     (JsonNumber (fromRational (toRational duration / 1e6)))
                     meta5
 
-        -- start = show (fromRational (toRational (spanTimeFrom datum) / 1e9) :: Fixed E9)
-        -- meta7 = insertKeyValue "timestamp" (JsonString (intoRope (show (spanTimeFrom datum)))) meta6
         time = intoRope (show (spanTimeFrom datum))
         point =
             JsonObject
diff --git a/lib/Core/Telemetry/Observability.hs b/lib/Core/Telemetry/Observability.hs
--- a/lib/Core/Telemetry/Observability.hs
+++ b/lib/Core/Telemetry/Observability.hs
@@ -43,7 +43,7 @@
 Then when you run your program you can pick the exporter:
 
 @
-\$ burgerservice --telemetry=console
+\$ __burgerservice --telemetry=console__
 @
 
 to activate sending telemetry to, in this case, the console. Other exporters
@@ -295,7 +295,7 @@
 This will allow you to then select the appropriate backend at runtime:
 
 @
-\$ burgerservice --telemetry=console
+\$ __burgerservice --telemetry=structured__
 @
 
 which will result in it spitting out metrics as it goes,
@@ -521,11 +521,11 @@
 Add measurements to the current span.
 
 @
-            telemetry
-                [ metric "calories" (667 :: Int)
-                , metric "precise" measurement
-                , metric "meal_name" ("hamburger" :: Rope)
-                , metric "flavour" True
+            'telemetry'
+                [ 'metric' \"calories\" (667 :: 'Int')
+                , 'metric' \"precise\" measurement
+                , 'metric' \"meal_name\" ("hamburger" :: 'Rope')
+                , 'metric' \"flavour\" 'True'
                 ]
 @
 
@@ -575,9 +575,9 @@
 the in which it occured.
 
 @
-            sendEvent
+            'sendEvent'
                 "Make tea"
-                [ metric "sugar" False
+                [ 'metric' \"sugar\" 'False'
                 ]
 @
 -}
diff --git a/lib/Core/Telemetry/Structured.hs b/lib/Core/Telemetry/Structured.hs
new file mode 100644
--- /dev/null
+++ b/lib/Core/Telemetry/Structured.hs
@@ -0,0 +1,154 @@
+{-# 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)
