diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2015 Sharif Olorin
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,31 @@
+# nagios-plugin-ekg
+
+This package provides `check_ekg`, a generic plugin for Nagios (and
+compatible) monitoring systems which translates metrics exposed by
+[ekg](https://hackage.haskell.org/package/ekg) into perfdata suitable
+for use with pnp4nagios and similar tools.
+
+There is currently no support for threshold checks; the plugin will
+always return an `OK` result if it can obtain and parse output from
+the EKG endpoint. Its primary function is to allow gathering
+performance data over time from EKG-enabled Haskell applications;
+there are no current plans to add support for thresholds/alerting,
+though I'd consider it in the future if there was demand.
+
+# Usage
+
+The plugin can either be executed directly on the Nagios host or
+invoked via NRPE.
+
+Example invocation:
+
+```
+$ check_ekg -e http://haskell-app.example.com:8000
+OK: perfdata only | iterations=60614c;;;; rts_gc_bytes_allocated=0c;;;; rts_gc_bytes_copied=0c;;;; rts_gc_cpu_ms=0c;;;; rts_gc_cumulative_bytes_used=0c;;;; rts_gc_current_bytes_slop=0.0;;;; rts_gc_current_bytes_used=0.0;;;; rts_gc_gc_cpu_ms=0c;;;; rts_gc_gc_wall_ms=0c;;;; rts_gc_max_bytes_slop=0.0;;;; rts_gc_max_bytes_used=0.0;;;; rts_gc_mutator_cpu_ms=0c;;;; rts_gc_mutator_wall_ms=0c;;;; rts_gc_num_bytes_usage_samples=0c;;;; rts_gc_num_gcs=0c;;;; rts_gc_par_avg_bytes_copied=0.0;;;; rts_gc_par_max_bytes_copied=0.0;;;; rts_gc_par_tot_bytes_copied=0.0;;;; rts_gc_peak_megabytes_allocated=0.0;;;; rts_gc_wall_ms=0c;;;; ekg_server_timestamp_ms=1435214338224c;;;;
+$ echo $?
+0
+```
+
+# Requirements
+
+`nagios-plugin-ekg` has been tested with GHC 7.8.4 and GHC 7.10.1.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/lib/System/Nagios/Plugin/Ekg.hs b/lib/System/Nagios/Plugin/Ekg.hs
new file mode 100644
--- /dev/null
+++ b/lib/System/Nagios/Plugin/Ekg.hs
@@ -0,0 +1,7 @@
+module System.Nagios.Plugin.Ekg (
+    module System.Nagios.Plugin.Ekg.Check,
+    module System.Nagios.Plugin.Ekg.Types
+) where
+
+import System.Nagios.Plugin.Ekg.Check
+import System.Nagios.Plugin.Ekg.Types
diff --git a/lib/System/Nagios/Plugin/Ekg/Check.hs b/lib/System/Nagios/Plugin/Ekg/Check.hs
new file mode 100644
--- /dev/null
+++ b/lib/System/Nagios/Plugin/Ekg/Check.hs
@@ -0,0 +1,47 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module System.Nagios.Plugin.Ekg.Check where
+
+import Control.Applicative
+import Control.Lens
+import Control.Monad.IO.Class
+import Data.Aeson
+import Data.ByteString.Lazy (ByteString)
+import qualified Data.ByteString.Lazy.Char8 as BL
+import qualified Data.Text as T
+import Network.Wreq hiding (header)
+import qualified Network.Wreq as W
+import Options.Applicative
+import System.Nagios.Plugin
+
+import System.Nagios.Plugin.Ekg.Types
+
+pluginOptParser :: ParserInfo PluginOpts
+pluginOptParser =  info (helper <*> opts)
+    (   fullDesc
+     <> progDesc "Nagios plugin exposing perfdata from EKG."
+     <> header   "check_ekg"
+    )
+  where
+    opts = PluginOpts <$> strOption (   long "ekg-endpoint"
+                                     <> short 'e'
+                                     <> metavar "EKG-ENDPOINT"
+                                     <> value "http://localhost:8888"
+                                     <> help "URL of the EKG endpoint."
+                                    )
+
+checkEkg :: NagiosPlugin ()
+checkEkg = do
+    opts <- liftIO $ execParser pluginOptParser
+    let reqOpts = W.header "Accept" .~ ["application/json"] $ defaults
+    resp <- liftIO . getWith reqOpts $ optsEndpoint opts
+    case resp ^. responseStatus . statusCode of
+        200 -> checkEkg' $ resp ^. responseBody
+        code -> addResult Critical . T.pack $ "EKG endpoint failed with status " <> show code
+
+checkEkg' :: ByteString -> NagiosPlugin ()
+checkEkg' bs = case (eitherDecode' bs :: Either String MetricTree) of
+    Left err -> addResult Critical $ "failed to parse EKG output: " <> T.pack err
+    Right meters -> do
+        addPerfData meters
+        addResult OK "perfdata only"
diff --git a/lib/System/Nagios/Plugin/Ekg/Types.hs b/lib/System/Nagios/Plugin/Ekg/Types.hs
new file mode 100644
--- /dev/null
+++ b/lib/System/Nagios/Plugin/Ekg/Types.hs
@@ -0,0 +1,113 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module System.Nagios.Plugin.Ekg.Types where
+
+import Control.Applicative
+import Control.Monad
+import Data.Aeson
+import Data.Aeson.Types
+import Data.Int
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HM
+import Data.Map (Map)
+import qualified Data.Map as M
+import Data.Monoid
+import Data.Text (Text)
+import qualified Data.Text as T
+import System.Nagios.Plugin
+
+data PluginOpts = PluginOpts
+  { optsEndpoint :: String }
+
+data EkgMetric =
+     -- * Nondecreasing counter, e.g., all-time number of requests.
+      EkgCounter Int64
+     -- * Measure of a quantity over time, e.g., number of requests per minute.
+    | EkgGauge Double
+    -- * Can't meaningfully turn labels into perfdata, this is a placeholder.
+    | EkgLabel
+    -- * Can't meaningfully turn distributions into perfdata, this is a placeholder.
+    | EkgDistribution
+  deriving (Eq, Show)
+
+instance FromJSON EkgMetric where
+    parseJSON (Object o) = do
+        metric_type <- o .: "type"
+        case metric_type of
+            "c" -> EkgCounter <$> o .: "val"
+            "g" -> EkgGauge <$> o .: "val"
+            "l" -> return EkgLabel
+            "d" -> return EkgDistribution
+            x   -> fail $ "Invalid metric type " <> T.unpack x
+    parseJSON _          = fail "EkgMetric must be an object"
+
+-- | A node in the 'MetricTree'; a Leaf is a single metric.
+data MetricNode =
+      Leaf EkgMetric
+    | Branch (Map Text MetricNode)
+
+instance FromJSON MetricNode where
+    parseJSON (Object o) = do
+        leaf <- isLeaf <$> parseJSON (Object o)
+        if leaf
+            then Leaf <$> parseJSON (Object o)
+            else Branch <$> parseJSON (Object o)
+      where
+        -- Educated guess as to whether this object is a leaf. It'll
+        -- definitely have "type"; it'll have "val" if it's a counter,
+        -- gauge or label and it'll have "variance" and "mean" if it's
+        -- a distribution.
+        --
+        -- My kingdom for a schema.
+        isLeaf :: HashMap Text Value -> Bool
+        isLeaf m = HM.member "type" m &&
+            (HM.member "val" m ||
+                (HM.member "variance" m && HM.member "mean" m)
+            )
+    parseJSON x          = fail $ "MetricNode must be an object, not " <> show x
+
+newtype MetricTree = MetricTree
+    { unMetricTree :: Map Text MetricNode }
+
+instance FromJSON MetricTree where
+    parseJSON (Object o) = MetricTree <$> parseJSON (Object o)
+    parseJSON _          = fail "MetricTree must be an object"
+
+instance ToPerfData MetricTree where
+    toPerfData (MetricTree m) = M.foldrWithKey (renderValue Nothing) [] m
+
+-- | Build perfdata from a single metric. The Nagios perfdata format
+--   doesn't allow us to sensibly represent the EKG 'Distribution' or
+--   'Label' types so we don't try.
+renderMetric :: Text
+             -> EkgMetric
+             -> Maybe PerfDatum
+renderMetric lbl (EkgCounter n) = 
+    Just $ barePerfDatum lbl (IntegralValue n) Counter
+renderMetric lbl (EkgGauge n) =
+    Just $ barePerfDatum lbl (RealValue n) NullUnit
+renderMetric _ EkgLabel = Nothing
+renderMetric _ EkgDistribution = Nothing
+
+-- | Build perfdata from a node in the metric tree. Produce a
+--   'PerfDatum' from a 'Leaf', recursively walk a 'Branch' and
+--   mappend the leaves.
+renderValue :: Maybe Text
+            -> Text
+            -> MetricNode
+            -> [PerfDatum]
+            -> [PerfDatum]
+renderValue prefix lbl (Leaf val) acc =
+    case renderMetric (withPrefix prefix lbl) val of
+        Nothing -> acc
+        Just pd -> pd : acc
+renderValue prefix lbl (Branch branch) acc = acc
+    <> M.foldrWithKey (renderValue (Just $ withPrefix prefix lbl)) [] branch
+
+-- | Construct a metric name, optionally prepended with a prefix (we
+--   want a prefix for every component of the name except the first one).
+withPrefix :: Maybe Text
+           -> Text
+           -> Text
+withPrefix Nothing suff = suff
+withPrefix (Just prefix) suff = prefix <> "_" <> suff
diff --git a/nagios-plugin-ekg.cabal b/nagios-plugin-ekg.cabal
new file mode 100644
--- /dev/null
+++ b/nagios-plugin-ekg.cabal
@@ -0,0 +1,61 @@
+name:                nagios-plugin-ekg
+version:             0.1.0.0
+synopsis:            Monitor ekg metrics via Nagios
+description:         A generic Nagios plugin which retrieves metrics
+                     from an application which uses
+                     <http://hackage.haskell.org/package/ekg ekg>.
+homepage:            https://github.com/fractalcat/nagios-plugin-ekg
+license:             MIT
+license-file:        LICENSE
+author:              Sharif Olorin
+maintainer:          sio@tesser.org
+copyright:           2015 Sharif Olorin and Anchor Systems
+category:            System
+build-type:          Simple
+extra-source-files:  README.md
+cabal-version:       >=1.10
+
+source-repository    head
+  type:              git
+  location:          git@github.com:fractalcat/nagios-plugin-ekg.git
+
+library
+  exposed-modules:     System.Nagios.Plugin.Ekg
+                       System.Nagios.Plugin.Ekg.Types
+                       System.Nagios.Plugin.Ekg.Check
+  build-depends:       base >= 4.7 && <5,
+                       nagios-check >= 0.3.1,
+                       transformers,
+                       bytestring,
+                       optparse-applicative,
+                       lens,
+                       aeson,
+                       wreq,
+                       text,
+                       unordered-containers,
+                       containers
+  hs-source-dirs:      lib
+  default-language:    Haskell2010
+
+executable check_ekg
+  main-is:             Main.hs
+  build-depends:       base >=4.7 && <5,
+                       nagios-check >= 0.3,
+                       text,
+                       nagios-plugin-ekg
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+
+test-suite sample-data
+  hs-source-dirs:      tests
+  main-is:             SampleData.hs
+  type:                exitcode-stdio-1.0
+  default-language:    Haskell2010
+  build-depends:       base >=4.5 && <5,
+                       HUnit,
+                       bytestring,
+                       hspec,
+                       nagios-check,
+                       nagios-plugin-ekg,
+                       text,
+                       transformers
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -0,0 +1,7 @@
+module Main where
+
+import System.Nagios.Plugin (runNagiosPlugin)
+import System.Nagios.Plugin.Ekg (checkEkg)
+
+main :: IO ()
+main = runNagiosPlugin checkEkg
diff --git a/tests/SampleData.hs b/tests/SampleData.hs
new file mode 100644
--- /dev/null
+++ b/tests/SampleData.hs
@@ -0,0 +1,48 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import           Control.Monad.IO.Class
+import qualified Data.ByteString.Lazy         as BS
+import qualified Data.Text                    as T
+import           Test.Hspec
+import           Test.HUnit.Base
+
+import           System.Nagios.Plugin
+import           System.Nagios.Plugin.Ekg
+
+main :: IO ()
+main = hspec suite
+
+suite :: Spec
+suite =
+    describe "checkEkg'" $ do
+        validJson <- runIO $ BS.readFile "tests/data/ekg_sample_output.json"
+        (_, validState) <- runIO . runNagiosPlugin' $ checkEkg' validJson
+        let (validStatus, validOutput) = finishState validState
+
+        let invalidJson = "{ \"iamnotarealmetric\": \"noreallyimnot\" }"
+        (_, borkedState) <- runIO . runNagiosPlugin' $ checkEkg' invalidJson
+        let (borkedStatus, borkedOutput) = finishState borkedState
+        
+        let emptyString = ""
+        (_, emptyState) <- runIO . runNagiosPlugin' $ checkEkg' emptyString
+        let (emptyStatus, emptyOutput) = finishState emptyState
+
+        it "exits with OK status given valid input" $
+            validStatus @?= OK
+
+        it "outputs the correct perfdata" $
+            validOutput @?= "OK: perfdata only | iterations=41734c;;;; rts_gc_bytes_allocated=0c;;;; rts_gc_bytes_copied=0c;;;; rts_gc_cpu_ms=0c;;;; rts_gc_cumulative_bytes_used=0c;;;; rts_gc_current_bytes_slop=0.0;;;; rts_gc_current_bytes_used=0.0;;;; rts_gc_gc_cpu_ms=0c;;;; rts_gc_gc_wall_ms=0c;;;; rts_gc_max_bytes_slop=0.0;;;; rts_gc_max_bytes_used=0.0;;;; rts_gc_mutator_cpu_ms=0c;;;; rts_gc_mutator_wall_ms=0c;;;; rts_gc_num_bytes_usage_samples=0c;;;; rts_gc_num_gcs=0c;;;; rts_gc_par_avg_bytes_copied=0.0;;;; rts_gc_par_max_bytes_copied=0.0;;;; rts_gc_par_tot_bytes_copied=0.0;;;; rts_gc_peak_megabytes_allocated=0.0;;;; rts_gc_wall_ms=0c;;;; ekg_server_timestamp_ms=1435212142955c;;;;"
+
+        it "exits with CRITICAL status given invalid JSON input" $
+           borkedStatus @?= Critical
+
+        it "outputs correct failure message on invalid JSON input" $
+           borkedOutput @?= "CRITICAL: failed to parse EKG output: MetricNode must be an object, not String \"noreallyimnot\""
+
+        it "exits with CRITICAL status given empty input" $
+           emptyStatus @?= Critical
+
+        it "outputs correct failure message on empty input" $
+           emptyOutput @?= "CRITICAL: failed to parse EKG output: not enough input"
