diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,13 @@
 # Changelog for hs-opentelemetry-exporter-handle
 
+## Unreleased
+
+## 1.0.0.0 - 2026-05-29
+
+- `OpenTelemetry.Exporter.Handle` barrel now re-exports all three signals (Span, Metric, LogRecord); deprecated annotation removed
+- New `OpenTelemetry.Exporter.Handle.LogRecord` module (console log exporter with default formatter)
+- New `OpenTelemetry.Exporter.Handle.Metric` module (console metric exporter)
+
 ## 0.0.1.3
 
 - Relax `hs-opentelemetry-api` bounds to support 0.3.x
diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright Ian Duncan (c) 2021
+Copyright Ian Duncan (c) 2021-2026
 
 All rights reserved.
 
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,1 +1,3 @@
 # hs-opentelemetry-exporter-handle
+
+[![hs-opentelemetry-exporter-handle](https://img.shields.io/hackage/v/hs-opentelemetry-exporter-handle?style=flat-square&logo=haskell&label=hs-opentelemetry-exporter-handle&labelColor=5D4F85)](https://hackage.haskell.org/package/hs-opentelemetry-exporter-handle)
diff --git a/hs-opentelemetry-exporter-handle.cabal b/hs-opentelemetry-exporter-handle.cabal
--- a/hs-opentelemetry-exporter-handle.cabal
+++ b/hs-opentelemetry-exporter-handle.cabal
@@ -1,12 +1,14 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.37.0.
+-- This file has been generated from package.yaml by hpack version 0.38.3.
 --
 -- see: https://github.com/sol/hpack
 
 name:           hs-opentelemetry-exporter-handle
-version:        0.0.1.3
+version:        1.0.0.0
+synopsis:       Handle-based exporter for OpenTelemetry (e.g. stdout)
 description:    Please see the README on GitHub at <https://github.com/iand675/hs-opentelemetry/tree/main/exporters/handle#readme>
+category:       OpenTelemetry, Telemetry, Monitoring
 homepage:       https://github.com/iand675/hs-opentelemetry#readme
 bug-reports:    https://github.com/iand675/hs-opentelemetry/issues
 author:         Ian Duncan
@@ -27,6 +29,7 @@
   exposed-modules:
       OpenTelemetry.Exporter.Handle
       OpenTelemetry.Exporter.Handle.LogRecord
+      OpenTelemetry.Exporter.Handle.Metric
       OpenTelemetry.Exporter.Handle.Span
   other-modules:
       Paths_hs_opentelemetry_exporter_handle
@@ -34,6 +37,24 @@
       src
   build-depends:
       base >=4.7 && <5
-    , hs-opentelemetry-api ==0.3.*
+    , hs-opentelemetry-api ==1.0.*
     , text
+    , vector
+  default-language: Haskell2010
+
+test-suite hs-opentelemetry-exporter-handle-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Paths_hs_opentelemetry_exporter_handle
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      base >=4.7 && <5
+    , hs-opentelemetry-api ==1.0.*
+    , hs-opentelemetry-exporter-handle ==1.0.*
+    , hspec
+    , unordered-containers
+    , vector
   default-language: Haskell2010
diff --git a/src/OpenTelemetry/Exporter/Handle.hs b/src/OpenTelemetry/Exporter/Handle.hs
--- a/src/OpenTelemetry/Exporter/Handle.hs
+++ b/src/OpenTelemetry/Exporter/Handle.hs
@@ -1,7 +1,15 @@
-module OpenTelemetry.Exporter.Handle
-  {-# DEPRECATED "use OpenTelemetry.Exporter.Handle.Span instead" #-} (
+{- |
+Module      : OpenTelemetry.Exporter.Handle
+Description : Re-exports for handle-based (stdout/stderr) exporters.
+Stability   : experimental
+-}
+module OpenTelemetry.Exporter.Handle (
   module OpenTelemetry.Exporter.Handle.Span,
+  module OpenTelemetry.Exporter.Handle.Metric,
+  module OpenTelemetry.Exporter.Handle.LogRecord,
 ) where
 
+import OpenTelemetry.Exporter.Handle.LogRecord
+import OpenTelemetry.Exporter.Handle.Metric
 import OpenTelemetry.Exporter.Handle.Span
 
diff --git a/src/OpenTelemetry/Exporter/Handle/LogRecord.hs b/src/OpenTelemetry/Exporter/Handle/LogRecord.hs
--- a/src/OpenTelemetry/Exporter/Handle/LogRecord.hs
+++ b/src/OpenTelemetry/Exporter/Handle/LogRecord.hs
@@ -1,2 +1,94 @@
-module OpenTelemetry.Exporter.Handle.LogRecord () where
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
 
+module OpenTelemetry.Exporter.Handle.LogRecord (
+  makeHandleLogRecordExporter,
+  stdoutLogRecordExporter,
+  stderrLogRecordExporter,
+  defaultLogRecordFormatter,
+) where
+
+import qualified Data.Text as T
+import qualified Data.Text.IO as T
+import qualified Data.Text.Lazy as LT
+import qualified Data.Text.Lazy.Builder as LB
+import Data.Text.Lazy.Builder.Int (decimal)
+import Data.Text.Lazy.Builder.RealFloat (realFloat)
+import qualified Data.Vector as V
+import OpenTelemetry.Internal.Common.Types (ExportResult (..), FlushResult (..), InstrumentationLibrary (..))
+import OpenTelemetry.Internal.Log.Types
+import OpenTelemetry.LogAttributes (AnyValue (..), LogAttributes (..))
+import System.IO (Handle, hFlush, stderr, stdout)
+
+
+builderText :: LB.Builder -> T.Text
+builderText = LT.toStrict . LB.toLazyText
+
+
+textIntegral :: (Integral n) => n -> T.Text
+textIntegral = builderText . decimal
+
+
+textDouble :: Double -> T.Text
+textDouble = builderText . realFloat
+
+
+makeHandleLogRecordExporter :: Handle -> (ReadableLogRecord -> IO T.Text) -> IO LogRecordExporter
+makeHandleLogRecordExporter h formatter =
+  mkLogRecordExporter
+    LogRecordExporterArguments
+      { logRecordExporterArgumentsExport = \lrs -> do
+          V.mapM_ (\lr -> formatter lr >>= T.hPutStrLn h >> hFlush h) lrs
+          pure Success
+      , logRecordExporterArgumentsForceFlush = hFlush h >> pure FlushSuccess
+      , logRecordExporterArgumentsShutdown = hFlush h
+      }
+
+
+stdoutLogRecordExporter :: IO LogRecordExporter
+stdoutLogRecordExporter = makeHandleLogRecordExporter stdout defaultLogRecordFormatter
+
+
+stderrLogRecordExporter :: IO LogRecordExporter
+stderrLogRecordExporter = makeHandleLogRecordExporter stderr defaultLogRecordFormatter
+
+
+defaultLogRecordFormatter :: ReadableLogRecord -> IO T.Text
+defaultLogRecordFormatter lr = do
+  ImmutableLogRecord {..} <- readLogRecord lr
+  let scope_ = readLogRecordInstrumentationScope lr
+  let sevText = case logRecordSeverityText of
+        UJust s -> s
+        UNothing -> "UNSET"
+  let bodyText = case logRecordBody of
+        TextValue t -> t
+        IntValue i -> textIntegral i
+        DoubleValue d -> textDouble d
+        BoolValue b -> if b then "true" else "false"
+        NullValue -> ""
+        _ -> T.pack (show logRecordBody)
+  let traceInfo = case logRecordTracingDetails of
+        TracingDetails tid sid _flags -> " trace=" <> T.pack (show tid) <> " span=" <> T.pack (show sid)
+        NoTracingDetails -> ""
+  let LogAttributes {attributesCount = attrCount, attributesDropped = droppedCount} = logRecordAttributes
+  let attrInfo =
+        if attrCount > 0
+          then " attrs=" <> textIntegral attrCount <> if droppedCount > 0 then " dropped=" <> textIntegral droppedCount else ""
+          else ""
+  let eventInfo = case logRecordEventName of
+        UJust en -> " event=" <> en
+        UNothing -> ""
+  pure $
+    T.concat
+      [ T.pack (show logRecordObservedTimestamp)
+      , " "
+      , sevText
+      , " ["
+      , libraryName scope_
+      , "]"
+      , eventInfo
+      , " "
+      , bodyText
+      , traceInfo
+      , attrInfo
+      ]
diff --git a/src/OpenTelemetry/Exporter/Handle/Metric.hs b/src/OpenTelemetry/Exporter/Handle/Metric.hs
new file mode 100644
--- /dev/null
+++ b/src/OpenTelemetry/Exporter/Handle/Metric.hs
@@ -0,0 +1,53 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+{- |
+Module      :  OpenTelemetry.Exporter.Handle.Metric
+Copyright   :  (c) Ian Duncan, 2024-2026
+License     :  BSD-3
+Description :  Console \/ handle metric exporter (@OTEL_METRICS_EXPORTER=console@).
+Stability   :  experimental
+
+Renders each export batch using 'OpenTelemetry.Debug.MetricExport.renderResourceMetricsExportDebug'
+and writes the text to the given 'Handle'.
+-}
+module OpenTelemetry.Exporter.Handle.Metric (
+  makeHandleMetricExporter,
+  stdoutMetricExporter,
+  stderrMetricExporter,
+) where
+
+import Control.Monad.IO.Class (MonadIO)
+import qualified Data.Text.IO as TIO
+import qualified Data.Vector as V
+import OpenTelemetry.Debug.MetricExport (renderResourceMetricsExportDebug)
+import OpenTelemetry.Exporter.Metric (
+  MetricExporter (..),
+ )
+import OpenTelemetry.Internal.Common.Types (ExportResult (..), FlushResult (..), ShutdownResult (..))
+import System.IO (Handle, hFlush, hPutChar, stderr, stdout)
+
+
+makeHandleMetricExporter :: Handle -> MetricExporter
+makeHandleMetricExporter h =
+  MetricExporter
+    { metricExporterExport = \batches -> do
+        TIO.hPutStr h (renderResourceMetricsExportDebug (V.toList batches))
+        hPutChar h '\n'
+        hFlush h
+        pure Success
+    , metricExporterShutdown = do
+        hFlush h
+        pure ShutdownSuccess
+    , metricExporterForceFlush = do
+        hFlush h
+        pure FlushSuccess
+    }
+
+
+stdoutMetricExporter :: (MonadIO m) => m MetricExporter
+stdoutMetricExporter = pure (makeHandleMetricExporter stdout)
+
+
+stderrMetricExporter :: (MonadIO m) => m MetricExporter
+stderrMetricExporter = pure (makeHandleMetricExporter stderr)
diff --git a/src/OpenTelemetry/Exporter/Handle/Span.hs b/src/OpenTelemetry/Exporter/Handle/Span.hs
--- a/src/OpenTelemetry/Exporter/Handle/Span.hs
+++ b/src/OpenTelemetry/Exporter/Handle/Span.hs
@@ -2,6 +2,27 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE RecordWildCards #-}
 
+{- |
+Module      : OpenTelemetry.Exporter.Handle.Span
+Copyright   : (c) Ian Duncan, 2021-2026
+License     : BSD-3
+Description : Export spans as text to a file handle (stdout/stderr)
+Stability   : experimental
+
+= Overview
+
+Writes spans as human-readable text to a file handle. Useful for local
+development and debugging.
+
+= Quick example
+
+@
+import OpenTelemetry.Exporter.Handle.Span (stdoutExporter')
+
+exporter <- stdoutExporter'
+-- Spans will be printed to stdout as they complete
+@
+-}
 module OpenTelemetry.Exporter.Handle.Span (
   makeHandleExporter,
   -- $
@@ -13,9 +34,12 @@
 
 import Data.IORef
 import qualified Data.Text.Lazy as L
+import Data.Text.Lazy.Builder (fromString, fromText, toLazyText)
 import qualified Data.Text.Lazy.IO as L
 import OpenTelemetry.Exporter.Span
+import OpenTelemetry.Internal.Common.Types (FlushResult (..), ShutdownResult (..))
 import OpenTelemetry.Trace.Core
+import OpenTelemetry.Trace.Id (Base (..), spanIdBaseEncodedText, traceIdBaseEncodedText)
 import System.IO (Handle, hFlush, stderr, stdout)
 
 
@@ -25,7 +49,8 @@
     { spanExporterExport = \fs -> do
         mapM_ (mapM_ (\s -> f s >>= L.hPutStrLn h >> hFlush h)) fs
         pure Success
-    , spanExporterShutdown = hFlush h
+    , spanExporterShutdown = hFlush h >> pure ShutdownSuccess
+    , spanExporterForceFlush = hFlush h >> pure FlushSuccess
     }
 
 
@@ -37,12 +62,16 @@
 stderrExporter' = makeHandleExporter stderr
 
 
-defaultFormatter :: ImmutableSpan -> L.Text
-defaultFormatter ImmutableSpan {..} =
-  L.intercalate
-    " "
-    [ L.pack $ show $ traceId spanContext
-    , L.pack $ show $ spanId spanContext
-    , L.pack $ show spanStart
-    , L.fromStrict spanName
-    ]
+defaultFormatter :: ImmutableSpan -> IO L.Text
+defaultFormatter imm = do
+  hot <- readIORef (spanHot imm)
+  let ctx = spanContext imm
+  pure $!
+    toLazyText $
+      fromText (traceIdBaseEncodedText Base16 (traceId ctx))
+        <> " "
+        <> fromText (spanIdBaseEncodedText Base16 (spanId ctx))
+        <> " "
+        <> fromString (show (spanStart imm))
+        <> " "
+        <> fromText (hotName hot)
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,67 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main (main) where
+
+import qualified Data.HashMap.Strict as HM
+import qualified Data.Vector as V
+import OpenTelemetry.Exporter.Handle.LogRecord (makeHandleLogRecordExporter)
+import OpenTelemetry.Exporter.Handle.Span (makeHandleExporter)
+import OpenTelemetry.Exporter.LogRecord (
+  logRecordExporterExport,
+  logRecordExporterForceFlush,
+  logRecordExporterShutdown,
+ )
+import OpenTelemetry.Exporter.Span (SpanExporter (..))
+import OpenTelemetry.Internal.Common.Types (ExportResult (..))
+import System.IO (stdout)
+import Test.Hspec
+
+
+main :: IO ()
+main = hspec spec
+
+
+spec :: Spec
+spec =
+  -- Handle-based exporters (implementation-specific; SpanExporter / LogRecordExporter shape)
+  -- https://opentelemetry.io/docs/specs/otel/trace/sdk/#span-exporter
+  -- https://opentelemetry.io/docs/specs/otel/logs/sdk/#logrecordexporter
+  do
+    describe "Handle Span exporter" $ do
+      -- SpanExporter.Export must accept empty batches (SDK contract)
+      -- https://opentelemetry.io/docs/specs/otel/trace/sdk/#export-batch
+      it "returns Success for empty batch" $ do
+        let formatter _span = pure "test"
+            exporter = makeHandleExporter stdout formatter
+        result <- spanExporterExport exporter HM.empty
+        case result of
+          Success -> pure ()
+          Failure _ -> expectationFailure "expected Success"
+
+      it "forceFlush does not throw" $ do
+        let exporter = makeHandleExporter stdout (\_ -> pure "test")
+        _ <- spanExporterForceFlush exporter
+        pure ()
+
+      it "shutdown does not throw" $ do
+        let exporter = makeHandleExporter stdout (\_ -> pure "test")
+        _ <- spanExporterShutdown exporter
+        pure ()
+
+    describe "Handle LogRecord exporter" $ do
+      it "returns Success for empty batch" $ do
+        exporter <- makeHandleLogRecordExporter stdout (\_ -> pure "test")
+        result <- logRecordExporterExport exporter V.empty
+        case result of
+          Success -> pure ()
+          Failure _ -> expectationFailure "expected Success"
+
+      it "forceFlush does not throw" $ do
+        exporter <- makeHandleLogRecordExporter stdout (\_ -> pure "test")
+        _ <- logRecordExporterForceFlush exporter
+        pure ()
+
+      it "shutdown does not throw" $ do
+        exporter <- makeHandleLogRecordExporter stdout (\_ -> pure "test")
+        _ <- logRecordExporterShutdown exporter
+        pure ()
