diff --git a/collectd_ceilometer/gnocchi/__init__.py b/collectd_ceilometer/gnocchi/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/collectd_ceilometer/gnocchi/plugin.py b/collectd_ceilometer/gnocchi/plugin.py
new file mode 100644
index 0000000..0a60e79
--- /dev/null
+++ b/collectd_ceilometer/gnocchi/plugin.py
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+"""Gnocchi collectd plugin"""
+
+import logging
+
+try:
+    # pylint: disable=import-error
+    import collectd
+    # pylint: enable=import-error
+except ImportError:
+    collectd = None  # when running unit tests collectd is not avaliable
+
+import collectd_ceilometer
+from collectd_ceilometer.common.logger import CollectdLogHandler
+from collectd_ceilometer.common.meters import MeterStorage
+from collectd_ceilometer.common.settings import Config
+from collectd_ceilometer.gnocchi.writer import Writer
+
+LOGGER = logging.getLogger(__name__)
+ROOT_LOGGER = logging.getLogger(collectd_ceilometer.__name__)
+
+
+def register_plugin(collectd):
+    "Bind plugin hooks to collectd and viceversa"
+
+    config = Config.instance()
+
+    # Setup loggging
+    log_handler = CollectdLogHandler(collectd=collectd)
+    log_handler.cfg = config
+    ROOT_LOGGER.addHandler(log_handler)
+    ROOT_LOGGER.setLevel(logging.NOTSET)
+
+    # Creates collectd plugin instance
+    instance = Plugin(collectd=collectd, config=config)
+
+    # Register plugin callbacks
+    collectd.register_init(instance.init)
+    collectd.register_config(instance.config)
+    collectd.register_write(instance.write)
+    collectd.register_shutdown(instance.shutdown)
+
+
+class Plugin(object):
+    """Gnocchi plugin with collectd callbacks"""
+    # NOTE: this is multithreaded class
+
+    def __init__(self, collectd, config):
+        self._config = config
+        self._meters = MeterStorage(collectd=collectd)
+        self._writer = Writer(self._meters, config=config)
+
+    def config(self, cfg):
+        """Configuration callback
+
+        @param cfg configuration node provided by collectd
+        """
+        # pylint: disable=no-self-use
+        self._config.read(cfg)
+
+    def init(self):
+        """Initialization callback"""
+
+        collectd.info('Initializing the collectd OpenStack python plugin')
+        self._meters = MeterStorage(collectd=collectd)
+        # self._writer = Writer(self._meters)
+
+    def write(self, vl, data=None):
+        """Collectd write callback"""
+        self._writer.write(vl, data)
+
+    def shutdown(self):
+        """Shutdown callback"""
+        LOGGER.info("SHUTDOWN")
+        self._writer.flush()
+
+
+if collectd:
+    register_plugin(collectd=collectd)
diff --git a/collectd_ceilometer/gnocchi/sender.py b/collectd_ceilometer/gnocchi/sender.py
new file mode 100644
index 0000000..e48f824
--- /dev/null
+++ b/collectd_ceilometer/gnocchi/sender.py
@@ -0,0 +1,242 @@
+# -*- coding: utf-8 -*-
+
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+"""Gnocchi collectd plugin implementation"""
+
+from __future__ import division
+from __future__ import unicode_literals
+
+import collectd_ceilometer
+from collectd_ceilometer.common.keystone_light import ClientV3
+from collectd_ceilometer.common.keystone_light import KeystoneException
+from collectd_ceilometer.common.settings import Config
+
+import json
+import logging
+import requests
+from requests.exceptions import RequestException
+import six
+import threading
+
+LOGGER = logging.getLogger(__name__)
+ROOT_LOGGER = logging.getLogger(collectd_ceilometer.__name__)
+
+
+# HTTP status codes
+HTTP_CREATED = 201
+HTTP_UNAUTHORIZED = 401
+HTTP_NOT_FOUND = 404
+
+
+class Sender(object):
+    """Sends the JSON serialized data to Gnocchi"""
+
+    def __init__(self):
+        """Create the Sender instance
+
+        The cofinguration must be initialized before the object is created.
+        """
+        self._url_base = None
+        self._keystone = None
+        self._auth_token = None
+        self._auth_lock = threading.Lock()
+        self._failed_auth = False
+        self._meter_ids = {}
+
+    def _authenticate(self):
+        """Authenticate and renew the authentication token"""
+
+        # if auth_token is available, just return it
+        if self._auth_token is not None:
+            return self._auth_token
+
+        # aquire the authentication lock
+        with self._auth_lock:
+            # re-check the auth_token as another thread could set it
+            if self._auth_token is not None:
+                return self._auth_token
+
+            LOGGER.debug('Authenticating request')
+            # pylint: disable=broad-except
+            try:
+                # create a keystone client if it doesn't exist
+                if self._keystone is None:
+                    cfg = Config.instance()
+                    self._keystone = ClientV3(
+                        auth_url=cfg.OS_AUTH_URL,
+                        username=cfg.OS_USERNAME,
+                        password=cfg.OS_PASSWORD,
+                        tenant_name=cfg.OS_TENANT_NAME
+                    )
+                # store the authentication token
+                self._auth_token = self._keystone.auth_token
+
+                # get the uri of service endpoint
+                endpoint = self._get_endpoint("gnocchi")
+
+                self._url_base = "{}/v1/metric/%s/measures".format(endpoint)
+
+                LOGGER.info('Authenticating request - success')
+                self._failed_auth = False
+
+            except KeystoneException as exc:
+                log_level = logging.DEBUG
+
+                if not self._failed_auth:
+                    log_level = logging.ERROR
+                    LOGGER.error(
+                        'Suspending error logs until successful auth'
+                    )
+
+                LOGGER.log(log_level, 'Authentication error: %s',
+                           six.text_type(exc),
+                           exc_info=0)
+
+                if exc.response:
+                    LOGGER.debug('Response: %s', exc.response)
+
+                self._auth_token = None
+                self._failed_auth = True
+
+        return self._auth_token
+
+    def send(self, metername, payload, unit):
+        """Send the payload to Gnocchi"""
+
+        # get the auth_token
+        auth_token = self._authenticate()
+        LOGGER.info('Auth_token: %s',
+                    auth_token,
+                    )
+        # if auth_token is not set, there is nothing to do
+        if auth_token is None:
+            LOGGER.debug('Unable to send data. Not authenticated')
+            return
+
+        if self._url_base is None:
+            LOGGER.debug(
+                'Unable to send data. Missing endpoint from ident server')
+            return
+
+        # create request URL
+        metric_id = self._get_metric_id(metername, unit)
+        url = self._url_base % (metric_id)
+
+        # send the POST request
+        result = self._perform_request(url, payload, auth_token)
+
+        if result is None:
+            return
+
+        LOGGER.info('Result: %s %s',
+                    six.text_type(result.status_code),
+                    result.text)
+
+        # if the request failed due to an auth error
+        if result.status_code == HTTP_UNAUTHORIZED:
+            # reset the auth token in order to force the subsequent
+            # _authenticate() call to renew it
+            # Here, it can happen that the token is reset right after
+            # another thread has finished the authentication and thus
+            # the authentication may be performed twice
+            self._auth_token = None
+
+            # renew the authentication token
+            auth_token = self._authenticate()
+
+            if auth_token is not None:
+                # and try to repost
+                result = self._perform_request(url, payload, auth_token)
+
+        if result.status_code == HTTP_NOT_FOUND:
+            LOGGER.debug("Received 404 error when submitting %s sample, \
+                         creating a new metric",
+                         metername)
+
+            # create metric (endpoint, metername)
+            metric_id = self._get_metric_id(metername, unit)
+
+            LOGGER.info('metername: %s, meter_id: %s', metername, metric_id)
+            # Set a new url for the request
+            url = self._url_base % (metric_id)
+            # TODO(emma-l-foley): Add error checking
+            # Submit the sample
+            result = self._perform_request(url, payload, auth_token)
+
+        if result.status_code == HTTP_CREATED:
+            LOGGER.debug('Result: %s', HTTP_CREATED)
+        else:
+            LOGGER.info('Result: %s %s',
+                        result.status_code,
+                        result.text)
+
+    def _get_endpoint(self, service):
+        # get the uri of service endpoint
+        endpoint = self._keystone.get_service_endpoint(
+            service,
+            Config.instance().CEILOMETER_URL_TYPE)
+        return endpoint
+
+    def _get_metric_id(self, metername, unit):
+
+        try:
+            return self._meter_ids[metername]
+        except KeyError as ke:
+            LOGGER.warn(ke)
+            LOGGER.warn('No known ID for %s', metername)
+
+            endpoint = self._get_endpoint("gnocchi")
+            self._meter_ids[metername] = \
+                self._create_metric(metername, endpoint, unit)
+
+        return self._meter_ids[metername]
+
+    def _create_metric(self, metername, endpoint, unit):
+        url = "{}/v1/metric/".format(endpoint)
+        payload = json.dumps({"archive_policy_name": "high",
+                              "name": metername,
+                              "unit": unit,
+                              })
+        result = self._perform_request(url, payload, self._auth_token)
+        metric_id = json.loads(result.text)['id']
+        LOGGER.debug("metric_id=%s", metric_id)
+        return metric_id
+
+    @classmethod
+    def _perform_request(cls, url, payload, auth_token):
+        """Perform the POST request"""
+
+        LOGGER.debug('Performing request to %s', url)
+
+        # request headers
+        headers = {'X-Auth-Token': auth_token,
+                   'Content-type': 'application/json'}
+        # perform request and return its result
+        response = None
+        try:
+            LOGGER.debug(
+                "Performing request to: %s with data=%s and headers=%s",
+                url, payload, headers)
+
+            response = requests.post(
+                url, data=payload, headers=headers,
+                timeout=(Config.instance().CEILOMETER_TIMEOUT / 1000.))
+            LOGGER.info('Response: %s: %s',
+                        response.status_code, response.text
+                        )
+        except RequestException as exc:
+            LOGGER.error('gnocchi request error: %s', six.text_type(exc))
+        finally:
+            LOGGER.debug('Returning response from _perform_request(): %s',
+                         response.status_code)
+            return response
diff --git a/collectd_ceilometer/gnocchi/writer.py b/collectd_ceilometer/gnocchi/writer.py
new file mode 100644
index 0000000..07fe344
--- /dev/null
+++ b/collectd_ceilometer/gnocchi/writer.py
@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+"""Gnocchi collectd plugin implementation"""
+
+from __future__ import unicode_literals
+
+from collectd_ceilometer.gnocchi.sender import Sender
+from collections import defaultdict
+from collections import namedtuple
+import datetime
+import json
+import logging
+import six
+import threading
+
+LOGGER = logging.getLogger(__name__)
+
+
+class Sample(namedtuple('Sample', ['value', 'timestamp', 'meta',
+                                   'unit', 'metername'])):
+    """Sample data"""
+
+    def to_payload(self):
+        """Return a payload dictionary"""
+        return {
+            'value': self.value,
+            'timestamp': self.timestamp,
+        }
+
+
+class SampleContainer(object):
+    """Sample storage"""
+
+    def __init__(self):
+        self._lock = threading.Lock()
+        self._data = defaultdict(list)
+
+    def add(self, key, samples, limit):
+        """Store list of samples under the key
+
+        Store the list of samples under the given key. If numer of stored
+        samples is greater than the given limit, all the samples are returned
+        and the stored samples are dropped. Otherwise None is returned.
+
+        @param key      key of the samples
+        @param samples  list of samples
+        @param limit    sample list limit
+        """
+        with self._lock:
+            current = self._data[key]
+            current += samples
+            if len(current) >= limit:
+                self._data[key] = []
+                return current
+        return None
+
+    def reset(self):
+        """Reset stored samples
+
+        Returns all samples and removes them from the container.
+        """
+        with self._lock:
+            retval = self._data
+            self._data = defaultdict(list)
+        return retval
+
+
+class Writer(object):
+    """Data collector"""
+
+    def __init__(self, meters, config):
+        self._meters = meters
+        self._samples = SampleContainer()
+        self._sender = Sender()
+        self._config = config
+
+    def write(self, vl, data):
+        """Collect data from collectd
+
+        example of vl: collectd.Values(type='vmpage_action',
+        type_instance='interleave_hit',plugin='numa',plugin_instance='node0',
+        host='localhost',time=1443092594.625033,interval=10.0,values=[21383])
+        """
+        # take the plugin (specialized or default) for parsing the data
+        plugin = self._meters.get(vl.plugin)
+        # prepare all data related to the sample
+        metername = plugin.meter_name(vl)
+        unit = plugin.unit(vl)
+        timestamp = datetime.datetime.utcfromtimestamp(vl.time).isoformat()
+
+        LOGGER.debug(
+            'Writing: plugin="%s", metername="%s"', vl.plugin, metername)
+
+        # store sample for every value
+        data = [
+            Sample(
+                value=value, timestamp=timestamp, meta=vl.meta,
+                unit=unit, metername=metername
+                )
+            for value in vl.values
+        ]
+
+        # add data to cache and get the samples to send
+        to_send = self._samples.add(metername, data,
+                                    self._config.BATCH_SIZE)
+        if to_send:
+            self._send_data(metername, to_send, unit)
+
+    def flush(self):
+        """Flush all pending samples"""
+
+        # get all stored samples
+        to_send = self._samples.reset()
+
+        # send all cached samples
+        for key, samples in six.iteritems(to_send):
+            if samples:
+                self._send_data(key, samples)
+
+    def _send_data(self, metername, to_send, unit=None):
+        """Send data to gnocchi"""
+
+        LOGGER.debug('Sending %d samples of %s',
+                     len(to_send), metername)
+
+        # gnocchi samples
+        payload = json.dumps([sample.to_payload() for sample in to_send])
+        self._sender.send(metername, payload, unit)
diff --git a/collectd_ceilometer/tests/gnocchi/__init__.py b/collectd_ceilometer/tests/gnocchi/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/collectd_ceilometer/tests/gnocchi/test_plugin.py b/collectd_ceilometer/tests/gnocchi/test_plugin.py
new file mode 100644
index 0000000..0a46f34
--- /dev/null
+++ b/collectd_ceilometer/tests/gnocchi/test_plugin.py
@@ -0,0 +1,365 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2010-2011 OpenStack Foundation
+# Copyright (c) 2015 Intel Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Plugin tests"""
+
+import logging
+import mock
+import requests
+import unittest
+
+from collectd_ceilometer.common.keystone_light import KeystoneException
+from collectd_ceilometer.gnocchi import plugin
+from collectd_ceilometer.gnocchi import sender
+
+from collectd_ceilometer.tests import match
+
+Logger = logging.getLoggerClass()
+
+
+def mock_collectd(**kwargs):
+    "Returns collecd module with collecd logging hooks."
+    return mock.patch(
+        __name__ + '.' + MockedCollectd.__name__, specs=True,
+        get_dataset=mock.MagicMock(side_effect=Exception), **kwargs)
+
+
+class MockedCollectd(object):
+    "Mocked collectd module specifications."
+
+    def debug(self, record):
+        "Hook for debug messages"
+
+    def info(self, record):
+        "Hook for info messages"
+
+    def warning(self, record):
+        "Hook for warning messages"
+
+    def error(self, record):
+        "Hook for error messages"
+
+    def register_init(self, hook):
+        "Register an hook for init."
+
+    def register_config(self, hook):
+        "Register an hook for config."
+
+    def register_write(self, hook):
+        "Register an hook for write."
+
+    def register_shutdown(self, hook):
+        "Register an hook for shutdown."
+
+    def get_dataset(self, s):
+        "Gets a dataset."
+
+
+def mock_config(BATCH_SIZE=1, **kwargs):
+    "Returns collecd module with collecd logging hooks."
+    return mock.patch(
+        __name__ + '.' + MockedConfig.__name__, specs=True,
+        BATCH_SIZE=BATCH_SIZE, **kwargs)
+
+
+class MockedConfig(object):
+    "Mocked config class."
+
+    BATCH_SIZE = 1
+
+
+def mock_value(
+        host='localhost', plugin='cpu', plugin_instance='0',
+        _type='freq', type_instance=None, time=123456789, values=(1234,),
+        **kwargs):
+    """Create a mock value"""
+
+    return mock.patch(
+        __name__ + '.' + MockedValue.__name__, specs=True,
+        host=host, plugin=plugin, plugin_instance=plugin_instance, type=_type,
+        type_instance=type_instance, time=time, values=list(values), meta=None,
+        **kwargs)
+
+
+class MockedValue(object):
+    """Value used for testing"""
+
+    host = 'localhost'
+    plugin = None
+    plugin_instance = None
+    type = None
+    type_instance = None
+    time = 123456789
+    values = []
+    meta = None
+
+
+class TestPlugin(unittest.TestCase):
+    """Test the collectd plugin"""
+
+    @mock.patch.object(plugin, 'Plugin', autospec=True)
+    @mock.patch.object(plugin, 'Config', autospec=True)
+    @mock.patch.object(plugin, 'CollectdLogHandler', autospec=True)
+    @mock.patch.object(plugin, 'ROOT_LOGGER', autospec=True)
+    @mock_collectd()
+    def test_callbacks(
+            self, collectd, ROOT_LOGGER, CollectdLogHandler, Config, Plugin):
+        """Verify that the callbacks are registered properly"""
+
+        # When plugin function is called
+        plugin.register_plugin(collectd=collectd)
+
+        # Logger handler is set up
+        ROOT_LOGGER.addHandler.assert_called_once_with(
+            CollectdLogHandler.return_value)
+        ROOT_LOGGER.setLevel.assert_called_once_with(logging.NOTSET)
+
+        # It create a plugin
+        Plugin.assert_called_once_with(
+            collectd=collectd, config=Config.instance.return_value)
+
+        # callbacks are registered to collectd
+        instance = Plugin.return_value
+        collectd.register_config.assert_called_once_with(instance.config)
+        collectd.register_write.assert_called_once_with(instance.write)
+        collectd.register_shutdown.assert_called_once_with(instance.shutdown)
+
+    @mock.patch.object(sender.Sender, '_get_metric_id', autospec=True)
+    @mock.patch.object(requests, 'post', spec=callable)
+    @mock.patch.object(sender, 'ClientV3', autospec=True)
+    @mock_collectd()
+    @mock_config(BATCH_SIZE=2)
+    @mock_value()
+    def test_write(self, data, config, collectd, ClientV3, post, get_metric_id):
+        """Test collectd data writing"""
+
+        auth_client = ClientV3.return_value
+        auth_client.get_service_endpoint.return_value = \
+            'https://test-gnocchi.tld'
+
+        post.return_value.status_code = sender.HTTP_CREATED
+        post.return_value.text = 'Created'
+
+        get_metric_id.return_value = 'my-metric-id'
+
+        # init instance
+        instance = plugin.Plugin(collectd=collectd, config=config)
+
+        # write the first value
+        instance.write(data)
+        collectd.error.assert_not_called()
+
+        # no value has been sent to ceilometer
+        post.assert_not_called()
+
+        # send the second value
+        instance.write(data)
+        collectd.error.assert_not_called()
+
+        # authentication client has been created
+        ClientV3.assert_called_once()
+
+        # and values has been sent
+        post.assert_called_once_with(
+            'https://test-gnocchi.tld' +
+            '/v1/metric/my-metric-id/measures',
+            data=match.json([{
+                "value": 1234,
+                "timestamp": "1973-11-29T21:33:09",
+                }, {
+                "value": 1234,
+                "timestamp": "1973-11-29T21:33:09",
+                }]),
+            headers={'Content-type': 'application/json',
+                     'X-Auth-Token': auth_client.auth_token},
+            timeout=1.0)
+
+        # reset post method
+        post.reset_mock()
+
+        # write another values
+        instance.write(data)
+        collectd.error.assert_not_called()
+
+        # nothing has been sent
+        post.assert_not_called()
+
+        # call shutdown
+        instance.shutdown()
+
+        # no errors
+        collectd.error.assert_not_called()
+
+        # previously written value has been sent
+        post.assert_called_once_with(
+            'https://test-gnocchi.tld' +
+            '/v1/metric/my-metric-id/measures',
+            data=match.json([{
+                "value": 1234,
+                "timestamp": "1973-11-29T21:33:09",
+                }]),
+            headers={
+                'Content-type': 'application/json',
+                'X-Auth-Token': auth_client.auth_token},
+            timeout=1.0)
+
+    @mock.patch.object(requests, 'post', spec=callable)
+    @mock.patch.object(sender, 'ClientV3', autospec=True)
+    @mock.patch.object(sender, 'LOGGER', autospec=True)
+    @mock_collectd()
+    @mock_config()
+    @mock_value()
+    def test_write_auth_failed(
+            self, data, config, collectd, LOGGER, ClientV3, post):
+        """Test authentication failure"""
+
+        # tell the auth client to rise an exception
+        ClientV3.side_effect = KeystoneException(
+            "Missing name 'xxx' in received services",
+            "exception",
+            "services list")
+
+        # init instance
+        instance = plugin.Plugin(collectd=collectd, config=config)
+
+        # write the value
+        instance.write(data)
+
+        LOGGER.error.assert_called_once_with(
+            "Suspending error logs until successful auth")
+        LOGGER.log.assert_called_once_with(
+            logging.ERROR, "Authentication error: %s",
+            "Missing name 'xxx' in received services\nReason: exception",
+            exc_info=0)
+
+        # no requests method has been called
+        post.assert_not_called()
+
+    @mock.patch.object(sender.Sender, '_perform_request', spec=callable)
+    @mock.patch.object(sender, 'ClientV3', autospec=True)
+    @mock_collectd()
+    @mock_config()
+    @mock_value()
+    def test_request_error(
+            self, data, config, collectd, ClientV3, perf_req):
+        """Test error raised by underlying requests module"""
+
+        # tell POST request to raise an exception
+        perf_req.side_effect = requests.RequestException('Test POST exception')
+
+        # ieit instance
+        instance = plugin.Plugin(collectd=collectd, config=config)
+
+        # write the value
+        self.assertRaises(requests.RequestException, instance.write, data)
+
+    @mock.patch.object(sender.Sender, '_get_metric_id', autospec=True)
+    @mock.patch.object(requests, 'post', spec=callable)
+    @mock.patch.object(sender, 'ClientV3', autospec=True)
+    @mock_collectd()
+    @mock_config()
+    @mock_value()
+    def test_reauthentication(self, data, config, collectd,
+                              ClientV3, post, get_metric_id):
+        """Test re-authentication"""
+        # init instance
+        instance = plugin.Plugin(collectd=collectd, config=config)
+
+        # the sender used by the instance
+
+        get_metric_id.return_value = 'my-metric-id'
+
+        # response returned on success
+        response_ok = requests.Response()
+        response_ok.status_code = requests.codes["OK"]
+
+        # response returned on failure
+        response_unauthorized = requests.Response()
+        response_unauthorized.status_code = requests.codes["UNAUTHORIZED"]
+
+        post.return_value = response_ok
+
+        client = ClientV3.return_value
+        client.auth_token = 'Test auth token'
+
+        # write the value
+        instance.write(data)
+
+        # verify the auth token
+        post.assert_called_once_with(
+            mock.ANY, data=mock.ANY,
+            headers={u'Content-type': mock.ANY,
+                     u'X-Auth-Token': 'Test auth token'},
+            timeout=1.0)
+
+        # POST response is unauthorized -> new token needs to be acquired
+        post.side_effect = [response_unauthorized, response_ok]
+
+        # set a new auth token
+        client.auth_token = 'New test auth token'
+
+        instance.write(data)
+
+        # verify the auth token:
+        call_list = post.call_args_list
+        # POST called three times
+        self.assertEqual(len(call_list), 3)
+
+        # the second call contains the old token
+        token = call_list[1][1]['headers']['X-Auth-Token']
+        self.assertEqual(token, 'Test auth token')
+        # the third call contains the new token
+        token = call_list[2][1]['headers']['X-Auth-Token']
+        self.assertEqual(token, 'New test auth token')
+
+    @mock.patch.object(requests, 'post', spec=callable)
+    @mock.patch.object(sender, 'ClientV3', autospec=True)
+    @mock.patch.object(plugin, 'Writer', autospec=True)
+    @mock.patch.object(plugin, 'LOGGER', autospec=True)
+    @mock_collectd()
+    @mock_config()
+    @mock_value()
+    def test_exception_value_error(self, data, config, collectd,
+                                   LOGGER, Writer, ClientV3, post):
+        """Test exception raised during write and shutdown"""
+
+        writer = Writer.return_value
+        writer.write.side_effect = ValueError('Test write error')
+
+        # init instance
+        instance = plugin.Plugin(collectd=collectd, config=config)
+
+        self.assertRaises(ValueError, instance.write, data)
+
+    @mock.patch.object(requests, 'post', spec=callable)
+    @mock.patch.object(sender, 'ClientV3', autospec=True)
+    @mock.patch.object(plugin, 'Writer', autospec=True)
+    @mock.patch.object(plugin, 'LOGGER', autospec=True)
+    @mock_collectd()
+    @mock_config()
+    @mock_value()
+    def test_exception_runtime_error(self, data, config, collectd,
+                                     LOGGER, Writer, ClientV3, post):
+        """Test exception raised during write and shutdown"""
+
+        writer = Writer.return_value
+        writer.flush.side_effect = RuntimeError('Test shutdown error')
+
+        # init instance
+        instance = plugin.Plugin(collectd=collectd, config=config)
+
+        self.assertRaises(RuntimeError, instance.shutdown)
diff --git a/doc/source/devstackGSG.rst b/doc/source/devstackGSG.rst
index ed96eac..fd2bca9 100644
--- a/doc/source/devstackGSG.rst
+++ b/doc/source/devstackGSG.rst
@@ -77,3 +77,21 @@ To enable this feature execute the following instructions:
   and seperate each meter and its unit with a comma, as shown below.
 
 | COLLECTD_CUSTOM_UNITS="<meter> <unit>,<meter> <unit>"
+
+Gnocchi
+=======
+
+To deploy with gnocchi using devstack, add the following to you local.conf:
+
+    enable_plugin collectd-ceilometer-plugin http://github.com/openstack/collectd-ceilometer-plugin
+
+    COLLECTD_INSTALL=True
+    COLLECTD_CONF_DIR=/etc/collectd/collectd.conf.d/
+
+    # GNOCCHI
+    enable_plugin gnocchi https://github.com/openstack/gnocchi master
+    enable_service gnocchi-api,gnocchi-metricd,gnocchi-statsd
+    GNOCCHI_USE_KEYSTONE=True
+
+Once deployment is complete, edit collectd-ceilometer-plugin.conf to point at
+the collectd_ceiloemter.gnocchi.plugin module.
diff --git a/setup.cfg b/setup.cfg
index af43a20..02d895b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -24,6 +24,7 @@ test_suite=collectd_ceilometer.tests
 [files]
 packages =
     collectd_ceilometer
+    collectd_gnocchi
 
 [build_sphinx]
 source-dir = doc/source