diff --git a/network-metrics.cabal b/network-metrics.cabal
--- a/network-metrics.cabal
+++ b/network-metrics.cabal
@@ -1,5 +1,5 @@
 name:               network-metrics
-version:            0.1.3
+version:            0.1.4
 synopsis:           Send metrics to Ganglia, Graphite, and statsd.
 license:            OtherLicense
 license-file:       LICENSE
@@ -32,8 +32,7 @@
   location: git://github.com/brendanhay/network-metrics.git
 
 library
-  exposed-modules:  Network.Metrics
-                  , Network.Metrics.Ganglia
+  exposed-modules:  Network.Metrics.Ganglia
                   , Network.Metrics.Graphite
                   , Network.Metrics.Statsd
   other-modules:    Network.Metrics.Internal
diff --git a/src/GMetric.hs b/src/GMetric.hs
--- a/src/GMetric.hs
+++ b/src/GMetric.hs
@@ -14,11 +14,13 @@
       main
     ) where
 
-import Control.Monad          (when)
-import Data.ByteString.Char8  (pack)
+import Control.Monad            (when)
+import Data.Binary.Put          (runPut)
+import Data.ByteString.Char8    (pack)
 import System.Console.CmdArgs
-import System.Environment     (getArgs, withArgs)
-import System.Exit            (ExitCode(..), exitWith)
+import System.Environment       (getArgs, withArgs)
+import System.Exit              (ExitCode(..), exitWith)
+import Network.Metrics.Internal
 
 import qualified Network.Metrics.Ganglia as G
 
@@ -27,7 +29,7 @@
     , optPort  :: String
     , optName  :: String
     , optValue :: String
-    , optType  :: G.MetricType
+    , optType  :: G.GangliaType
     , optGroup :: String
     , optUnits :: String
     , optSlope :: G.Slope
@@ -49,11 +51,13 @@
 
 emit :: Options -> IO ()
 emit Options{..} = do
-    handle <- G.open optHost optPort
-    G.emit metric handle
-    G.close handle
+    sink@(G.Ganglia hd) <- G.open optHost optPort
+    _ <- push' G.putMetaData hd
+    _ <- push' G.putValue hd
+    G.close sink
   where
-    metric = G.Metric
+    push' f = hPush . runPut $ f metric
+    metric  = G.GangliaMetric
         (pack optName)
         optType
         (pack optUnits)
diff --git a/src/Network/Metrics.hs b/src/Network/Metrics.hs
deleted file mode 100644
--- a/src/Network/Metrics.hs
+++ /dev/null
@@ -1,18 +0,0 @@
--- |
--- Module      : Network.Metrics
--- Copyright   : (c) 2012 Brendan Hay <brendan@soundcloud.com>
--- License     : This Source Code Form is subject to the terms of
---               the Mozilla Public License, v. 2.0.
---               A copy of the MPL can be found in the LICENSE file or
---               you can obtain it at http://mozilla.org/MPL/2.0/.
--- Maintainer  : Brendan Hay <brendan@soundcloud.com>
--- Stability   : experimental
--- Portability : non-portable (GHC extensions)
---
-
-module Network.Metrics (
-    -- * Re-exported types
-      Handle(..)
-    ) where
-
-import Network.Metrics.Internal
diff --git a/src/Network/Metrics/Ganglia.hs b/src/Network/Metrics/Ganglia.hs
--- a/src/Network/Metrics/Ganglia.hs
+++ b/src/Network/Metrics/Ganglia.hs
@@ -13,53 +13,61 @@
 module Network.Metrics.Ganglia (
     -- * Exported types
       Slope(..)
-    , MetricType(..)
-    , Metric(..)
+    , GangliaType(..)
+    , GangliaMetric(..)
+    , Ganglia(..)
 
     -- * Default constructors
     , defaultMetric
 
     -- * Socket Handle operations
     , open
-    , emit
 
-    -- * Development helpers
-    , test
+    -- * Binary encoding
+    , putMetaData
+    , putValue
 
-    -- * Re-exported from internal
-    , I.Handle
-    , I.close
+    -- -- * Network.Metrics.Internal re-exported types
+    , Group
+    , Bucket
+    , Value
+    , MetricType(..)
+    , Metric(..)
+    , MetricSink(push)
+
+    -- * Network.Metrics.Internal operations
+    , close
     ) where
 
-import Control.Concurrent (threadDelay)
-import Control.Monad      (unless, liftM)
+import Control.Monad            (liftM)
 import Data.Binary.Put
-import Data.Bits          ((.&.))
-import Data.Char          (toLower)
-import Data.Data          (Data, Typeable)
-import Data.Default       (Default, def)
-import Data.Int           (Int32)
-import Data.Word          (Word32)
-import Network.Socket
-import System.Random      (randomRIO)
+import Data.Bits                ((.&.))
+import Data.Char                (toLower)
+import Data.Data                (Data, Typeable)
+import Data.Default             (Default, def)
+import Data.Int                 (Int32)
+import Data.Word                (Word32)
+import Network.Socket           (SocketType(..))
+import Network.Metrics.Internal
 
-import qualified Data.ByteString          as B
-import qualified Data.ByteString.Char8    as BS
-import qualified Network.Metrics.Internal as I
+import qualified Data.ByteString            as B
+import qualified Data.ByteString.Char8      as BS
+import qualified Data.ByteString.Lazy.Char8 as BL
 
+
 -- | Allows gmetad and the PHP webfrontend to efficiently separate
 -- constant data metrics from volatile ones
 data Slope = Zero | Positive | Negative | Both | Unspecified
       deriving (Data, Typeable, Show, Eq, Enum)
 
 -- | Metric types supported by Ganglia
-data MetricType = String | Int8 | UInt8 | Int16 | UInt16 | Int32 | UInt32 | Float | Double
+data GangliaType = String | Int8 | UInt8 | Int16 | UInt16 | Int32 | UInt32 | Float | Double
       deriving (Data, Typeable, Eq, Show)
 
--- | Concrete metric type used to emit metadata and value packets
-data Metric = Metric
+-- | concrete metric type used to emit metadata and value packets
+data GangliaMetric = GangliaMetric
     { name  :: BS.ByteString
-    , type' :: MetricType
+    , type' :: GangliaType
     , units :: BS.ByteString
     , value :: BS.ByteString
     , host  :: BS.ByteString
@@ -70,20 +78,26 @@
     , dmax  :: Word32
     } deriving (Show)
 
-instance Default Metric where
+instance Default GangliaMetric where
     def = defaultMetric
 
+data Ganglia = Ganglia Handle
+
+instance MetricSink Ganglia where
+    push m (Ganglia h) = hPush (encode m) h
+    close  (Ganglia h) = hClose h
+
 --
 -- API
 --
 
 -- | A default metric record
-defaultMetric :: Metric
-defaultMetric = Metric
-    { name  = "magical_metric"
-    , type' = UInt16
+defaultMetric :: GangliaMetric
+defaultMetric = GangliaMetric
+    { name  = ""
+    , type' = Int32
     , units = ""
-    , value = "0"
+    , value = ""
     , host  = ""
     , spoof = ""
     , group = ""
@@ -92,41 +106,17 @@
     , dmax  = 0
     }
 
--- | Create a new unconnected socket handle for UDP communication
-open :: String -> String -> IO I.Handle
-open = I.open Datagram
-
--- | Emit a metric's metadata and value on the specified socket handle
-emit :: Metric -> I.Handle -> IO ()
-emit metric handle@(I.Handle sock addr) = do
-    sIsConnected sock >>= \b -> unless b $ connect sock addr
-    _ <- push putMetaData handle
-    _ <- push putMetric handle
-    return ()
-  where
-    push fn = I.emit . runPut $ fn metric
-
---
--- Private
---
-
--- TODO: enforce max buffer size length checks.
--- Magic number is per libgmond.c
-bufferSize :: Integer
-bufferSize = 1500
-
---
--- Binary Encoding
---
+open :: String -> String -> IO Ganglia
+open host port = liftM Ganglia (hOpen Datagram host port)
 
 -- | Metric metadata
 --
 -- The format for this can be found in either:
 -- * gm_protocol.x in the Ganglia 3.1 sources
 -- * https://github.com/lookfirst/jmxtrans
-putMetaData :: Metric -> Put
-putMetaData metric@Metric{..} = do
-    putHeader 128 metric -- 128 = metadata_msg
+putMetaData :: GangliaMetric -> Put
+putMetaData m@GangliaMetric{..} = do
+    putHeader 128 m -- 128 = metadata_msg
     putType type'
     putString name
     putString units
@@ -136,15 +126,30 @@
     putGroup group
 
 -- | Metric value
-putMetric :: Metric -> Put
-putMetric metric@Metric{..} = do
-    putHeader 133 metric -- 133 = string_msg
+putValue :: GangliaMetric -> Put
+putValue m@GangliaMetric{..} = do
+    putHeader 133 m -- 133 = string_msg
     putString "%s"
     putString value
 
+--
+-- Private
+--
+
+-- TODO: enforce max buffer size length checks.
+-- Magic number is per libgmond.c
+bufferSize :: Integer
+bufferSize = 1500
+
+encode :: Metric -> BL.ByteString
+encode (Metric _ g b v) = BL.concat $ map put [putMetaData, putValue]
+  where
+    metric = defaultMetric { name  = b, group = g, value = v }
+    put f  = runPut $ f metric
+
 -- | Common headers for the metadata and value
-putHeader :: Int32 -> Metric -> Put
-putHeader code Metric{..} = do
+putHeader :: Int32 -> GangliaMetric -> Put
+putHeader code GangliaMetric{..} = do
     putInt code
     putString host
     putString name
@@ -178,32 +183,5 @@
   where
     len = BS.length bstr
 
-putType :: MetricType -> Put
+putType :: GangliaType -> Put
 putType = putString . BS.pack . map toLower . show
-
---
--- Development
---
-
--- | Exactly one second!
-oneSecond :: Int
-oneSecond = 1000000
-
--- | The min, max variance of test metrics
-variance :: (Int, Int)
-variance = (0, 1000)
-
--- | Sample a random element from a list
-sample :: [a] -> IO a
-sample xs = liftM (xs !!) (randomRIO (0, length xs - 1))
-
--- | Used to emit a default metric via GHCi
-test :: String -> String -> IO a
-test host port = open host port >>= loop
-  where
-    loop h = do
-        r <- randomRIO variance :: IO Int
-        n <- sample ["magic", "candy", "unicorns"]
-        emit defaultMetric { name = n, value = BS.pack $ show r } h
-        threadDelay oneSecond
-        loop h
diff --git a/src/Network/Metrics/Graphite.hs b/src/Network/Metrics/Graphite.hs
--- a/src/Network/Metrics/Graphite.hs
+++ b/src/Network/Metrics/Graphite.hs
@@ -11,43 +11,50 @@
 --
 
 module Network.Metrics.Graphite (
-    -- * Exported types
-      Metric(..)
-
     -- * Socket Handle operations
-    , open
-    , emit
+      open
 
-    -- * Re-exported from internal
-    , I.Handle
-    , I.close
+    -- * Network.Metrics.Internal re-exported types
+    , Group
+    , Bucket
+    , Value
+    , MetricType(..)
+    , Metric(..)
+    , MetricSink(push)
+
+    -- * Network.Metrics.Internal operations
+    , close
     ) where
 
+import Control.Monad            (liftM)
 import Network.Socket
 import Data.Time.Clock.POSIX
+import Network.Metrics.Internal
 
 import qualified Data.ByteString.Char8      as BS
 import qualified Data.ByteString.Lazy.Char8 as BL
-import qualified Network.Metrics.Internal   as I
 
-data Metric = Metric
-    { bucket :: BS.ByteString
-    , value  :: BS.ByteString
-    } deriving (Show)
+data Graphite = Graphite Handle
 
+instance MetricSink Graphite where
+    push m (Graphite h) = encode m >>= flip hPush h
+    close  (Graphite h) = hClose h
 --
 -- API
 --
 
 -- | Create a new disconnected socket handle for TCP communication
-open :: String -> String -> IO I.Handle
-open = I.open Stream
+open :: String -> String -> IO Graphite
+open host port = liftM Graphite (hOpen Stream host port)
 
--- | Emit a metric's metadata and value on the specified socket handle
-emit :: Metric -> I.Handle -> IO ()
-emit Metric{..} handle = do
-    time <- getPOSIXTime
-    I.emit (encoded time) handle
+--
+-- Private
+--
+
+encode :: Metric -> IO BL.ByteString
+encode (Metric _ g b v) = liftM bstr getPOSIXTime
   where
-    encoded n   = BL.fromChunks [bucket, value, timestamp n]
+    bucket      = BS.concat [g, ".", b]
     timestamp n = BS.pack $ show (truncate n :: Integer)
+    bstr n      = BL.fromChunks [bucket, v, timestamp n]
+
diff --git a/src/Network/Metrics/Internal.hs b/src/Network/Metrics/Internal.hs
--- a/src/Network/Metrics/Internal.hs
+++ b/src/Network/Metrics/Internal.hs
@@ -13,41 +13,68 @@
 module Network.Metrics.Internal (
     -- * Exported types
       Handle(..)
+    , Group
+    , Bucket
+    , Value
+    , MetricType(..)
+    , Metric(..)
+    , MetricSink(..)
 
     -- * Socket Handle operations
-    , open
-    , close
-    , emit
+    , hOpen
+    , hClose
+    , hPush
     ) where
 
 import Control.Monad                  (unless)
 import Network.Socket                 hiding (send)
 import Network.Socket.ByteString.Lazy (send)
 
+import qualified Data.ByteString.Char8      as BS
 import qualified Data.ByteString.Lazy.Char8 as BL
 
 -- | Socket handle
 data Handle = Handle Socket SockAddr deriving (Show)
 
+-- | Metric group
+type Group = BS.ByteString
+
+-- | Metric bucket
+type Bucket = BS.ByteString
+
+-- | Metric value
+type Value = BS.ByteString
+
+-- | Metric type
+data MetricType = Counter | Gauge | Timer deriving (Show)
+
+-- | Concrete metric data type
+data Metric = Metric MetricType Group Bucket Value deriving (Show)
+
+-- | Describes a sink resource which is held open for metric emission
+class MetricSink a where
+    push  :: Metric -> a -> IO ()
+    close :: a -> IO ()
+
 --
 -- API
 --
 
 -- | Create a new unconnected socket handle for UDP communication
-open :: SocketType -> String -> String -> IO Handle
-open typ host port = do
+hOpen :: SocketType -> String -> String -> IO Handle
+hOpen typ host port = do
     (addr:_) <- getAddrInfo Nothing (Just host) (Just port)
     sock     <- socket (addrFamily addr) typ defaultProtocol
     return $ Handle sock (addrAddress addr)
 
 -- | Close a socket handle
-close :: Handle -> IO ()
-close (Handle sock _) = sClose sock
+hClose :: Handle -> IO ()
+hClose (Handle sock _) = sClose sock
 
--- | Push an encoded metric to the specified socket handle
-emit :: BL.ByteString -> Handle -> IO ()
-emit bstr (Handle sock addr) | BL.null bstr = return ()
-                             | otherwise    = do
+-- | Direct access for writing a bytestring to the socket handle
+hPush :: BL.ByteString -> Handle -> IO ()
+hPush bstr (Handle sock addr) | BL.null bstr = return ()
+                              | otherwise    = do
     sIsConnected sock >>= \b -> unless b $ connect sock addr
     _ <- send sock bstr
     return ()
diff --git a/src/Network/Metrics/Statsd.hs b/src/Network/Metrics/Statsd.hs
--- a/src/Network/Metrics/Statsd.hs
+++ b/src/Network/Metrics/Statsd.hs
@@ -11,29 +11,30 @@
 --
 
 module Network.Metrics.Statsd (
-    -- * Exported types
-      MetricType(..)
-    , Metric(..)
-
     -- * Socket Handle operations
-    , open
-    , emit
+      open
 
-    -- * Re-exported from internal
-    , I.Handle
-    , I.close
+    -- * Network.Metrics.Internal re-exported types
+    , Group
+    , Bucket
+    , Value
+    , MetricType(..)
+    , Metric(..)
+    , MetricSink(push)
+
+    -- * Network.Metrics.Internal operations
+    , close
     ) where
 
-import Network.Socket
-import System.Random (randomRIO)
+import Control.Monad  (liftM)
+import Network.Socket (SocketType(..))
+import System.Random  (randomRIO)
+import Network.Metrics.Internal
 
 import qualified Data.ByteString.Char8      as BS
 import qualified Data.ByteString.Lazy.Char8 as BL
-import qualified Network.Metrics.Internal   as I
 
-data MetricType = Counter | Timer | Gauge deriving (Show)
-
-data Metric = Metric
+data StatsdMetric = StatsdMetric
     { type'  :: MetricType
     , bucket :: BS.ByteString
     , value  :: BS.ByteString
@@ -42,33 +43,37 @@
 
 data Sampled = Sampled | Exact | Ignore
 
+data Statsd = Statsd Handle
+
+instance MetricSink Statsd where
+    push m (Statsd h) = encode m >>= flip hPush h
+    close  (Statsd h) = hClose h
+
 --
 -- API
 --
 
 -- | Create a new disconnected socket handle for UDP communication
-open :: String -> String -> IO I.Handle
-open = I.open Datagram
-
--- | Emit a metric's metadata and value on the specified socket handle
-emit :: Metric -> I.Handle -> IO ()
-emit metric handle = do
-    rand <- randomRIO (0.0, 1.0)
-    I.emit (encoded rand) handle
-  where
-    encoded = BL.fromChunks . components metric . sample (rate metric)
+open :: String -> String -> IO Statsd
+open host port = liftM Statsd (hOpen Datagram host port)
 
 --
--- Sampling
+-- Private
 --
 
+encode :: Metric -> IO BL.ByteString
+encode (Metric t g b v) = liftM bstr (randomRIO (0.0, 1.0))
+  where
+    metric = StatsdMetric t (BS.concat [g, ".", b]) v 1.0
+    bstr   = BL.fromChunks . chunks metric . sample (rate metric)
+
 sample :: Double -> Double -> Sampled
 sample rate rand | rate < 1.0 && rand <= rate = Sampled
                  | rate == 1.0                = Exact
                  | otherwise                  = Ignore
 
-components :: Metric -> Sampled -> [BS.ByteString]
-components Metric{..} sampled = case sampled of
+chunks :: StatsdMetric -> Sampled -> [BS.ByteString]
+chunks StatsdMetric{..} sampled = case sampled of
     Sampled -> base ++ ["@", BS.pack $ show rate]
     Exact   -> base
     Ignore  -> []
