diff --git a/README.rst b/README.rst
index 66676fa..6d1b6e2 100644
--- a/README.rst
+++ b/README.rst
@@ -33,40 +33,39 @@ and exposed via a HTTP endpoint.
 .. code-block:: python
 
     #!/usr/bin/env python
-    '''
+    """
     This example demonstrates how a single Counter metric collector can be created
     and exposed via a HTTP endpoint.
-    '''
+    """
     import asyncio
     import socket
     from aioprometheus import Counter, Service
 
 
-    if __name__ == '__main__':
+    if __name__ == "__main__":
 
-        loop = asyncio.get_event_loop()
-
-        svr = Service()
+        async def main(svr: Service) -> None:
 
-        events_counter = Counter(
-            "events",
-            "Number of events.",
-            const_labels={'host': socket.gethostname()})
+            events_counter = Counter(
+                "events", "Number of events.", const_labels={"host": socket.gethostname()}
+            )
+            svr.register(events_counter)
+            await svr.start(addr="127.0.0.1", port=5000)
+            print(f"Serving prometheus metrics on: {svr.metrics_url}")
 
-        svr.register(events_counter)
+            # Now start another coroutine to periodically update a metric to
+            # simulate the application making some progress.
+            async def updater(c: Counter):
+                while True:
+                    c.inc({"kind": "timer_expiry"})
+                    await asyncio.sleep(1.0)
 
-        loop.run_until_complete(svr.start(addr="127.0.0.1"))
-        print(f'Serving prometheus metrics on: {svr.metrics_url}')
-
-        async def updater(m: Counter):
-            # Periodically update the metric to simulate some progress
-            # happening in a real application.
-            while True:
-                m.inc({'kind': 'timer_expiry'})
-                await asyncio.sleep(1.0)
+            await updater(events_counter)
 
+        loop = asyncio.get_event_loop()
+        svr = Service()
         try:
-            loop.run_until_complete(updater(events_counter))
+            loop.run_until_complete(main(svr))
         except KeyboardInterrupt:
             pass
         finally:
@@ -96,7 +95,7 @@ The example script can be run using:
 
     (venv) $ cd examples
     (venv) $ python simple-example.py
-    Serving prometheus metrics on: http://127.0.0.1:50624/metrics
+    Serving prometheus metrics on: http://127.0.0.1:5000/metrics
 
 In another terminal fetch the metrics using the ``curl`` command line tool
 to verify they can be retrieved by Prometheus server.
@@ -105,7 +104,7 @@ By default metrics will be returned in plan text format.
 
 .. code-block:: console
 
-    $ curl http://127.0.0.1:50624/metrics
+    $ curl http://127.0.0.1:5000/metrics
     # HELP events Number of events.
     # TYPE events counter
     events{host="alpha",kind="timer_expiry"} 33
@@ -115,7 +114,7 @@ to read on the command line.
 
 .. code-block:: console
 
-    $ curl http://127.0.0.1:50624/metrics -H "ACCEPT: application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited"
+    $ curl http://127.0.0.1:5000/metrics -H "ACCEPT: application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited"
 
 The metrics service also responds to requests sent to its ``/`` route. The
 response is simple HTML. This route can be useful as a Kubernetes ``/healthz``
@@ -124,15 +123,64 @@ to serialize a full metrics response.
 
 .. code-block:: console
 
-    $ curl http://127.0.0.1:50624/
+    $ curl http://127.0.0.1:5000/
     <html><body><a href='/metrics'>metrics</a></body></html>
 
-A number of convenience decorator functions are also available to assist with
-updating metrics.
+The aioprometheus package provides a number of convenience decorator
+functions that can assist with updating metrics.
+
+There ``examples`` directory contains many examples showing how to use the
+aioprometheus package. The ``app-example.py`` file will likely be of interest
+as it provides a more representative application example that the simple
+example shown above.
+
+Examples in the ``examples/frameworks`` directory show how aioprometheus can
+be used within existing aiohttp, quart and vibora applications instead of
+creating a separate aioprometheus.Service endpoint to handle metrics. The
+vibora example is shown below.
+
+.. code-block:: python
+
+    #!/usr/bin/env python
+    """
+    Sometimes you want to expose Prometheus metrics from within an existing web
+    service and don't want to start a separate Prometheus metrics server.
+
+    This example uses the aioprometheus package to add Prometheus instrumentation
+    to a Vibora application. In this example a registry and a counter metric is
+    instantiated. A '/metrics' route is added to the application and the render
+    function from aioprometheus is called to format the metrics into the
+    appropriate format.
+    """
+
+    from aioprometheus import render, Counter, Registry
+    from vibora import Vibora, Request, Response
+
+
+    app = Vibora(__name__)
+    app.registry = Registry()
+    app.events_counter = Counter("events", "Number of events.")
+    app.registry.register(app.events_counter)
+
+
+    @app.route("/")
+    async def hello(request: Request):
+        app.events_counter.inc({"path": "/"})
+        return Response(b"hello")
+
+
+    @app.route("/metrics")
+    async def handle_metrics(request: Request):
+        """
+        Negotiate a response format by inspecting the ACCEPTS headers and selecting
+        the most efficient format. Render metrics in the registry into the chosen
+        format and return a response.
+        """
+        content, http_headers = render(app.registry, [request.headers.get("accept")])
+        return Response(content, headers=http_headers)
+
 
-There are more examples in the ``examples`` directory. The ``app-example.py``
-file will likely be of interest as it provides a more representative
-application example.
+    app.run()
 
 
 License
diff --git a/docs/index.rst b/docs/index.rst
index 919115b..92626ec 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -64,7 +64,7 @@ The example script can be run using:
 
     (venv) $ cd examples
     (venv) $ python simple-example.py
-    Serving prometheus metrics on: http://127.0.0.1:50624/metrics
+    Serving prometheus metrics on: http://127.0.0.1:5000/metrics
 
 In another terminal fetch the metrics using the ``curl`` command line tool
 to verify they can be retrieved by Prometheus server.
@@ -73,12 +73,12 @@ By default metrics will be returned in plan text format.
 
 .. code-block:: console
 
-    $ curl http://127.0.0.1:50624/metrics
+    $ curl http://127.0.0.1:5000/metrics
     # HELP events Number of events.
     # TYPE events counter
     events{host="alpha",kind="timer_expiry"} 33
 
-    $ curl http://127.0.0.1:50624/metrics -H 'Accept: text/plain; version=0.0.4'
+    $ curl http://127.0.0.1:5000/metrics -H 'Accept: text/plain; version=0.0.4'
     # HELP events Number of events.
     # TYPE events counter
     events{host="alpha",kind="timer_expiry"} 36
@@ -88,7 +88,7 @@ to read on the command line.
 
 .. code-block:: console
 
-    $ curl http://127.0.0.1:50624/metrics -H "ACCEPT: application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited"
+    $ curl http://127.0.0.1:5000/metrics -H "ACCEPT: application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited"
 
 The metrics service also responds to requests sent to its ``/`` route. The
 response is simple HTML. This route can be useful as a Kubernetes health
@@ -97,15 +97,24 @@ serialize a full metrics response.
 
 .. code-block:: console
 
-    $ curl http://127.0.0.1:50624/
+    $ curl http://127.0.0.1:5000/
     <html><body><a href='/metrics'>metrics</a></body></html>
 
-A number of convenience decorator functions are also available to assist with
-updating metrics.
+The aioprometheus package provides a number of convenience decorator
+functions that can assist with updating metrics.
 
-There are more examples in the ``examples`` directory. The ``app-example.py``
-file will likely be of interest as it provides a more representative
-application example.
+There ``examples`` directory contains many examples showing how to use the
+aioprometheus package. The ``app-example.py`` file will likely be of interest
+as it provides a more representative application example that the simple
+example shown above.
+
+Examples in the ``examples/frameworks`` directory show how aioprometheus can
+be used within existing aiohttp, quart and vibora applications instead of
+creating a separate aioprometheus.Service endpoint to handle metrics. The
+vibora example is shown below.
+
+.. literalinclude:: ../examples/frameworks/vibora-example.py
+    :language: python3
 
 
 License
diff --git a/docs/user/index.rst b/docs/user/index.rst
index f9030f1..5ac57aa 100644
--- a/docs/user/index.rst
+++ b/docs/user/index.rst
@@ -222,7 +222,7 @@ The example script can be run using:
 
     (venv) $ cd examples
     (venv) $ python simple-example.py
-    Serving prometheus metrics on: http://127.0.0.1:50624/metrics
+    Serving prometheus metrics on: http://127.0.0.1:5000/metrics
 
 In another terminal fetch the metrics using the ``curl`` command line tool
 to verify they can be retrieved by Prometheus server.
@@ -231,12 +231,12 @@ By default metrics will be returned in plan text format.
 
 .. code-block:: console
 
-    $ curl http://127.0.0.1:50624/metrics
+    $ curl http://127.0.0.1:5000/metrics
     # HELP events Number of events.
     # TYPE events counter
     events{host="alpha",kind="timer_expiry"} 33
 
-    $ curl http://127.0.0.1:50624/metrics -H 'Accept: text/plain; version=0.0.4'
+    $ curl http://127.0.0.1:5000/metrics -H 'Accept: text/plain; version=0.0.4'
     # HELP events Number of events.
     # TYPE events counter
     events{host="alpha",kind="timer_expiry"} 36
@@ -246,7 +246,7 @@ to read on the command line.
 
 .. code-block:: console
 
-    $ curl http://127.0.0.1:50624/metrics -H "ACCEPT: application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited"
+    $ curl http://127.0.0.1:5000/metrics -H "ACCEPT: application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited"
 
 The metrics service also responds to requests sent to its ``/`` route. The
 response is simple HTML. This route can be useful as a Kubernetes ``/healthz``
@@ -255,7 +255,7 @@ to serialize a full metrics response.
 
 .. code-block:: console
 
-    $ curl http://127.0.0.1:50624/
+    $ curl http://127.0.0.1:5000/
     <html><body><a href='/metrics'>metrics</a></body></html>
 
 
@@ -274,11 +274,22 @@ The example can be run using
 .. code-block:: console
 
     (env) $ python app-example.py
-    Serving prometheus metrics on: http://127.0.0.1:50624/metrics
+    Serving prometheus metrics on: http://127.0.0.1:5000/metrics
 
 You can use the ``curl`` command line tool to fetch metrics manually or use
 the helper script described in the next section.
 
+Frameworks Example
+++++++++++++++++++
+
+The aioprometheus package can also be used within other web framework based
+applications such as ``aiohttp``, ``quart`` and ``vibora`` applications.
+This usage approach removes the need to create a separate server endpoint
+to handle metrics. The vibora example is shown below.
+
+.. literalinclude:: ../../examples/frameworks/vibora-example.py
+    :language: python3
+
 
 Checking examples using helper script
 -------------------------------------
@@ -307,7 +318,7 @@ Example:
 
 .. code-block:: console
 
-    $ python metrics-fetcher.py --url=http://127.0.0.1:50624/metrics --format=text --interval=2.0
+    $ python metrics-fetcher.py --url=http://127.0.0.1:5000/metrics --format=text --interval=2.0
 
 
 Checking Example using Prometheus
@@ -334,7 +345,7 @@ we can create a minimal configuration file to scrape the example application.
         scrape_timeout: 10s
 
         target_groups:
-          - targets: ['localhost:50624']
+          - targets: ['localhost:5000']
             labels:
               group: 'dev'
 
diff --git a/examples/app-example.py b/examples/app-example.py
index 230092e..e560758 100644
--- a/examples/app-example.py
+++ b/examples/app-example.py
@@ -22,13 +22,25 @@ from asyncio.base_events import BaseEventLoop
 class ExampleApp(object):
     """
     An example application that demonstrates how ``aioprometheus`` can be
-    used within a Python async application.
+    integrated and used within a Python application built upon asyncio.
+
+    This application attempts to simulate a long running distributed system
+    process, say a socket relay or some kind of message adapter. It is
+    intentionally not hosting an existing web service in the application.
+
+    In this case the aioprometheus.Service object is used to provide a
+    new HTTP endpoint that can be used to expose Prometheus metrics on.
+
+    If this application was a web service (i.e. already had an existing web
+    interface) then the aioprometheus.Service object could be used as before
+    to add another web interface or a different approach could be used that
+    provides a metrics handler function for use with the existing web service.
     """
 
     def __init__(
         self,
         metrics_host="127.0.0.1",
-        metrics_port: int = 0,
+        metrics_port: int = 5000,
         loop: BaseEventLoop = None,
     ):
 
diff --git a/examples/frameworks/aiohttp-example.py b/examples/frameworks/aiohttp-example.py
new file mode 100644
index 0000000..5a468fc
--- /dev/null
+++ b/examples/frameworks/aiohttp-example.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+"""
+Sometimes you want to expose Prometheus metrics from within an existing web
+service and don't want to start a separate Prometheus metrics server.
+
+This example uses the aioprometheus package to add Prometheus instrumentation
+to an aiohttp application. In this example a registry and a counter metric is
+instantiated. A '/metrics' route is added to the application and the render
+function from aioprometheus is called to format the metrics into the
+appropriate format.
+"""
+
+from aiohttp import web
+from aiohttp.hdrs import ACCEPT
+from aioprometheus import render, Counter, Registry
+
+
+app = web.Application()
+app.registry = Registry()
+app.events_counter = Counter("events", "Number of events.")
+app.registry.register(app.events_counter)
+
+
+async def handle_root(request):
+    app.events_counter.inc({"path": "/"})
+    text = "Hello aiohttp"
+    return web.Response(text=text)
+
+
+async def handle_metrics(request):
+    content, http_headers = render(app.registry, request.headers.getall(ACCEPT, []))
+    return web.Response(body=content, headers=http_headers)
+
+
+app.add_routes([web.get("/", handle_root), web.get("/metrics", handle_metrics)])
+
+
+web.run_app(app)
diff --git a/examples/frameworks/quart-example.py b/examples/frameworks/quart-example.py
new file mode 100644
index 0000000..b45873c
--- /dev/null
+++ b/examples/frameworks/quart-example.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+"""
+Sometimes you want to expose Prometheus metrics from within an existing web
+service and don't want to start a separate Prometheus metrics server.
+
+This example uses the aioprometheus package to add Prometheus instrumentation
+to a Quart application. In this example a registry and a counter metric is
+instantiated. A '/metrics' route is added to the application and the render
+function from aioprometheus is called to format the metrics into the
+appropriate format.
+"""
+
+from aioprometheus import render, Counter, Registry
+from quart import Quart, request
+
+
+app = Quart(__name__)
+app.registry = Registry()
+app.events_counter = Counter("events", "Number of events.")
+app.registry.register(app.events_counter)
+
+
+@app.route("/")
+async def hello():
+    app.events_counter.inc({"path": "/"})
+    return "hello"
+
+
+@app.route("/metrics")
+async def handle_metrics():
+    content, http_headers = render(app.registry, request.headers.getlist("accept"))
+    return content, http_headers
+
+
+app.run()
diff --git a/examples/frameworks/vibora-example.py b/examples/frameworks/vibora-example.py
new file mode 100644
index 0000000..f42e5d0
--- /dev/null
+++ b/examples/frameworks/vibora-example.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+"""
+Sometimes you want to expose Prometheus metrics from within an existing web
+service and don't want to start a separate Prometheus metrics server.
+
+This example uses the aioprometheus package to add Prometheus instrumentation
+to a Vibora application. In this example a registry and a counter metric is
+instantiated. A '/metrics' route is added to the application and the render
+function from aioprometheus is called to format the metrics into the
+appropriate format.
+"""
+
+from aioprometheus import render, Counter, Registry
+from vibora import Vibora, Request, Response
+
+
+app = Vibora(__name__)
+app.registry = Registry()
+app.events_counter = Counter("events", "Number of events.")
+app.registry.register(app.events_counter)
+
+
+@app.route("/")
+async def hello(request: Request):
+    app.events_counter.inc({"path": "/"})
+    return Response(b"hello")
+
+
+@app.route("/metrics")
+async def handle_metrics(request: Request):
+    """
+    Negotiate a response format by inspecting the ACCEPTS headers and selecting
+    the most efficient format. Render metrics in the registry into the chosen
+    format and return a response.
+    """
+    content, http_headers = render(app.registry, [request.headers.get("accept")])
+    return Response(content, headers=http_headers)
+
+
+app.run()
diff --git a/examples/simple-example.py b/examples/simple-example.py
index 83d66df..ed41712 100644
--- a/examples/simple-example.py
+++ b/examples/simple-example.py
@@ -1,16 +1,17 @@
 #!/usr/bin/env python
 """
-This example demonstrates how a single Counter metric collector can be created
-and exposed via a HTTP endpoint.
+This example demonstrates how aioprometheus can be used to expose metrics on
+a HTTP endpoint that is provided by the aioprometheus.Service object.
 
 .. code-block:: console
 
     (env) $ python simple-example.py
-    Serving prometheus metrics on: http://127.0.0.1:50624/metrics
+    Serving prometheus metrics on: http://127.0.0.1:5000/metrics
 
 In another terminal fetch the metrics using the ``curl`` command line tool
 to verify they can be retrieved by Prometheus server.
 """
+
 import asyncio
 import socket
 from aioprometheus import Counter, Service
@@ -18,28 +19,28 @@ from aioprometheus import Counter, Service
 
 if __name__ == "__main__":
 
-    loop = asyncio.get_event_loop()
-
-    svr = Service()
+    async def main(svr: Service) -> None:
 
-    events_counter = Counter(
-        "events", "Number of events.", const_labels={"host": socket.gethostname()}
-    )
+        events_counter = Counter(
+            "events", "Number of events.", const_labels={"host": socket.gethostname()}
+        )
+        svr.register(events_counter)
+        await svr.start(addr="127.0.0.1", port=5000)
+        print(f"Serving prometheus metrics on: {svr.metrics_url}")
 
-    svr.register(events_counter)
+        # Now start another coroutine to periodically update a metric to
+        # simulate the application making some progress.
+        async def updater(c: Counter):
+            while True:
+                c.inc({"kind": "timer_expiry"})
+                await asyncio.sleep(1.0)
 
-    loop.run_until_complete(svr.start(addr="127.0.0.1"))
-    print(f"Serving prometheus metrics on: {svr.metrics_url}")
-
-    async def updater(m: Counter):
-        # Periodically update the metric to simulate some progress
-        # happening in a real application.
-        while True:
-            m.inc({"kind": "timer_expiry"})
-            await asyncio.sleep(1.0)
+        await updater(events_counter)
 
+    loop = asyncio.get_event_loop()
+    svr = Service()
     try:
-        loop.run_until_complete(updater(events_counter))
+        loop.run_until_complete(main(svr))
     except KeyboardInterrupt:
         pass
     finally:
diff --git a/requirements.txt b/requirements.txt
index 93afa4e..408a37c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-aiohttp >= 3.2.1
+aiohttp >= 3.3.2
 asynctest
 prometheus-metrics-proto >= 18.1.1
 quantile-python >= 1.1
diff --git a/src/aioprometheus/__init__.py b/src/aioprometheus/__init__.py
index 2a12977..acbe1bb 100644
--- a/src/aioprometheus/__init__.py
+++ b/src/aioprometheus/__init__.py
@@ -3,9 +3,10 @@ from .collectors import Collector, Counter, Gauge, Summary, Histogram
 from .decorators import count_exceptions, inprogress, timer
 from . import formats
 from . import pusher
-from . import negotiator
+from .negotiator import negotiate
 from .registry import Registry, CollectorRegistry
 from .service import Service
+from .renderer import render
 
 
-__version__ = "18.01.04"
+__version__ = "18.7.1"
diff --git a/src/aioprometheus/formats/__init__.py b/src/aioprometheus/formats/__init__.py
index e521e6c..b4c191c 100644
--- a/src/aioprometheus/formats/__init__.py
+++ b/src/aioprometheus/formats/__init__.py
@@ -1,3 +1,3 @@
 from .base import IFormatter
-from .text import TextFormatter, TEXT_CONTENT_TYPE
-from .binary import BinaryFormatter, BINARY_CONTENT_TYPE
+from .text import TextFormatter, TEXT_CONTENT_TYPE, TEXT_ACCEPTS
+from .binary import BinaryFormatter, BINARY_CONTENT_TYPE, BINARY_ACCEPTS
diff --git a/src/aioprometheus/formats/binary.py b/src/aioprometheus/formats/binary.py
index 4b447f3..b5ae25d 100644
--- a/src/aioprometheus/formats/binary.py
+++ b/src/aioprometheus/formats/binary.py
@@ -30,6 +30,7 @@ BINARY_CONTENT_TYPE = (
     "proto=io.prometheus.client.MetricFamily; "
     "encoding=delimited"
 )
+BINARY_ACCEPTS = set(BINARY_CONTENT_TYPE.split("; "))
 
 
 class BinaryFormatter(IFormatter):
diff --git a/src/aioprometheus/formats/text.py b/src/aioprometheus/formats/text.py
index 52cd93a..02e75d2 100644
--- a/src/aioprometheus/formats/text.py
+++ b/src/aioprometheus/formats/text.py
@@ -35,6 +35,7 @@ NEG_INF = float("-inf")
 
 
 TEXT_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8"
+TEXT_ACCEPTS = set(TEXT_CONTENT_TYPE.split("; "))
 
 
 class TextFormatter(IFormatter):
diff --git a/src/aioprometheus/negotiator.py b/src/aioprometheus/negotiator.py
index 2284873..de96926 100644
--- a/src/aioprometheus/negotiator.py
+++ b/src/aioprometheus/negotiator.py
@@ -2,7 +2,7 @@
 import logging
 
 from . import formats
-from typing import Callable, Set
+from typing import Callable, Sequence, Set
 
 
 logger = logging.getLogger(__name__)
@@ -11,29 +11,40 @@ logger = logging.getLogger(__name__)
 FormatterType = Callable[[bool], formats.IFormatter]
 
 
-ProtobufAccepts = set(formats.BINARY_CONTENT_TYPE.split("; "))
-TextAccepts = set(formats.TEXT_CONTENT_TYPE.split("; "))
+ProtobufAccepts = formats.BINARY_ACCEPTS
+TextAccepts = formats.TEXT_ACCEPTS
 
 
-def negotiate(accepts: Set[str]) -> FormatterType:
-    """ Negotiate a response format by scanning through the ACCEPTS
-    header and selecting the most efficient format.
+def negotiate(accepts_headers: Sequence[str]) -> FormatterType:
+    """ Negotiate a response format by scanning through a list of ACCEPTS
+    headers and selecting the most efficient format.
 
     The formatter returned by this function is used to render a response.
 
-    :param accepts: a set of ACCEPT headers fields extracted from a request.
+    :param accepts_headers: a list of ACCEPT headers fields extracted from a request.
 
     :returns: a formatter class to form up the response into the
       appropriate representation.
     """
-    if not isinstance(accepts, set):
-        raise TypeError("Expected a set but got {}".format(type(accepts)))
+    accepts = parse_accepts(accepts_headers)
 
     formatter = formats.TextFormatter  # type: FormatterType
 
     if ProtobufAccepts.issubset(accepts):
         formatter = formats.BinaryFormatter  # type: ignore
 
-    logger.debug("negotiating %s resulted in choosing %s", accepts, formatter.__name__)
+    logger.debug(f"negotiating {accepts} resulted in choosing {formatter.__name__}")
 
     return formatter
+
+
+def parse_accepts(accept_headers: Sequence[str]) -> Set[str]:
+    """ Return a sequence of accepts items in the request headers """
+    accepts = set()  # type: Set[str]
+    for accept_items in accept_headers:
+        if ";" in accept_items:
+            accept_items = [i.strip() for i in accept_items.split(";")]
+        else:
+            accept_items = [accept_items]
+        accepts.update(accept_items)
+    return accepts
diff --git a/src/aioprometheus/renderer.py b/src/aioprometheus/renderer.py
new file mode 100644
index 0000000..07b0246
--- /dev/null
+++ b/src/aioprometheus/renderer.py
@@ -0,0 +1,37 @@
+
+from .formats import TextFormatter, BinaryFormatter
+from .registry import Registry
+from .negotiator import negotiate
+from typing import Sequence, Tuple, Union
+
+
+def render(registry: Registry, accepts_headers: Sequence[str]) -> Tuple[bytes, dict]:
+    """ Render the metrics in this registry to a specific format.
+
+    The format chosen is determined by scanning through the ACCEPTS headers
+    and selecting the most efficient format. If no accepts headers
+    information is provided then Text format is used as the default.
+
+    :param registry: A collector registry that contains the metrics to be
+      rendered into a specific format.
+
+    :param accepts_headers: a list of ACCEPT headers fields extracted from a request.
+
+    :returns: a 2-tuple where the first item is a bytes object that
+        represents the formatted metrics and the second item is a dict of
+        header fields that can be added to a HTTP response.
+    """
+    if not isinstance(registry, Registry):
+        raise Exception(f"registry must be a Registry, got: {type(registry)}")
+
+    if not isinstance(accepts_headers, (set, list, tuple)):
+        raise Exception(
+            f"accepts_headers must be a sequence, got: {type(accepts_headers)}"
+        )
+
+    Formatter = negotiate(accepts_headers)
+    formatter = Formatter()  # type: Union[TextFormatter, BinaryFormatter]
+
+    http_headers = formatter.get_headers()
+    content = formatter.marshall(registry)
+    return content, http_headers
diff --git a/src/aioprometheus/service.py b/src/aioprometheus/service.py
index 67bcf04..29860d3 100644
--- a/src/aioprometheus/service.py
+++ b/src/aioprometheus/service.py
@@ -9,7 +9,7 @@ import aiohttp.web
 
 from aiohttp.hdrs import METH_GET as GET, ACCEPT
 
-from .negotiator import negotiate
+from .renderer import render
 from .registry import Registry, CollectorsType
 from typing import Optional, Set
 
@@ -68,17 +68,11 @@ class Service(object):
                 "No URL available, Prometheus metrics server is not running"
             )
 
-        # IPv4 returns 2-tuple, IPv6 returns 4-tuple
-        host, port, *_ = self._site._server.sockets[0].getsockname()
+        # IPv4 address returns a 2-tuple, IPv6 returns a 4-tuple
+        host, port, *_ = self._runner.addresses[0]
         scheme = "http{}".format("s" if self._https else "")
-        url = "{scheme}://{host}:{port}".format(
-            scheme=scheme,
-            host=host if ":" not in host else "[{}]".format(host),
-            port=port,
-        )
-        #
-        # TODO: replace the above with self._site.name when aiohttp issue
-        # #3018 is resolved.
+        host = host if ":" not in host else f"[{host}]"
+        url = f"{scheme}://{host}:{port}"
         return url
 
     @property
@@ -107,7 +101,6 @@ class Service(object):
         port: int = 0,
         ssl: SSLContext = None,
         metrics_url: str = DEFAULT_METRICS_PATH,
-        discovery_agent=None,
     ) -> None:
         """ Start the prometheus metrics HTTP(S) server.
 
@@ -125,9 +118,6 @@ class Service(object):
         :param metrics_url: The name of the endpoint route to expose
           prometheus metrics on. Defaults to '/metrics'.
 
-        :param discovery_agent: an agent that can register the metrics
-          service with a service discovery mechanism.
-
         :raises: Exception if the server could not be started.
         """
         logger.debug(
@@ -159,19 +149,11 @@ class Service(object):
 
         logger.debug("Prometheus metrics server started on %s", self.metrics_url)
 
-        # register service with service discovery
-        if discovery_agent:
-            await discovery_agent.register(self)
-
-    async def stop(self, wait_duration: float = 1.0, discovery_agent=None) -> None:
+    async def stop(self, wait_duration: float = 1.0) -> None:
         """ Stop the prometheus metrics HTTP(S) server.
 
         :param wait_duration: the number of seconds to wait for connections to
           finish.
-
-        :param discovery_agent: an agent that can deregister the metrics
-          service from a service discovery mechanism.
-
         """
         logger.debug("Prometheus metrics server stopping")
 
@@ -179,10 +161,6 @@ class Service(object):
             logger.warning("Prometheus metrics server is already stopped")
             return
 
-        # de-register service with service discovery
-        if discovery_agent:
-            await discovery_agent.deregister(self)
-
         await self._runner.cleanup()
         self._site = None
         self._app = None
@@ -213,19 +191,15 @@ class Service(object):
         The request is inspected and the most efficient response data format
         is chosen.
         """
-        Formatter = negotiate(self.accepts(request))
-        formatter = Formatter(False)
-
-        resp = aiohttp.web.Response()
-        resp.headers.update(formatter.get_headers())
-        resp.body = formatter.marshall(self.registry)
-        return resp
+        content, http_headers = render(
+            self.registry, request.headers.getall(ACCEPT, [])
+        )
+        return aiohttp.web.Response(body=content, headers=http_headers)
 
     def accepts(self, request: aiohttp.web.Request) -> Set[str]:
         """ Return a sequence of accepts items in the request headers """
         accepts = set()  # type: Set[str]
         accept_headers = request.headers.getall(ACCEPT, [])
-        logger.debug("accept: {}".format(accept_headers))
         for accept_items in accept_headers:
             if ";" in accept_items:
                 accept_items = [i.strip() for i in accept_items.split(";")]
@@ -240,11 +214,10 @@ class Service(object):
         Serves a trivial page with a link to the metrics.  Use this if ever
         you need to point a health check at your the service.
         """
+        metrics_url = request.app["metrics_url"]
         return aiohttp.web.Response(
             content_type="text/html",
-            text="<html><body><a href='{}'>metrics</a></body></html>".format(
-                request.app["metrics_url"]
-            ),
+            text=f"<html><body><a href='{metrics_url}'>metrics</a></body></html>",
         )
 
     async def handle_robots(self, request: aiohttp.web.Request) -> aiohttp.web.Response:

metrics handler to existing server
hello, would be nice to include some documentation on how to do something like:

```
from aiohttp import web
from aioprometheus import Service

prometheus_service = Service()

async def handle(request):
    text = "Hello aiohttp"
    return web.Response(text=text)

app = web.Application()
app.router.add_get('/', handle)
app.router.add_get('/metrics', prometheus_service.handle_metrics)

web.run_app(app)
```
diff --git a/tests/test_aiohttp.py b/tests/test_aiohttp.py
new file mode 100644
index 0000000..195752d
--- /dev/null
+++ b/tests/test_aiohttp.py
@@ -0,0 +1,91 @@
+import asynctest
+import aiohttp
+import aiohttp.hdrs
+import aiohttp.web
+import unittest
+import aioprometheus
+
+
+class TestAiohttpRender(asynctest.TestCase):
+    """
+    Test exposing Prometheus metrics from within a aiohttp existing web
+    service without starting a separate Prometheus metrics server.
+    """
+
+    async def test_render_in_aiohttp_app(self):
+        """ check render usage in aiohttp app """
+
+        app = aiohttp.web.Application()
+        app.registry = aioprometheus.Registry()
+        app.events_counter = aioprometheus.Counter("events", "Number of events.")
+        app.registry.register(app.events_counter)
+
+        async def index(request):
+            app.events_counter.inc({"path": "/"})
+            return aiohttp.web.Response(text="hello")
+
+        async def handle_metrics(request):
+            content, http_headers = aioprometheus.render(
+                app.registry, request.headers.getall(aiohttp.hdrs.ACCEPT, [])
+            )
+            return aiohttp.web.Response(body=content, headers=http_headers)
+
+        app.add_routes(
+            [aiohttp.web.get("/", index), aiohttp.web.get("/metrics", handle_metrics)]
+        )
+
+        runner = aiohttp.web.AppRunner(app)
+        await runner.setup()
+
+        site = aiohttp.web.TCPSite(runner, "127.0.0.1", 0, shutdown_timeout=1.0)
+        await site.start()
+
+        # Fetch ephemeral port that was bound.
+        # IPv4 address returns a 2-tuple, IPv6 returns a 4-tuple
+        host, port, *_ = runner.addresses[0]
+        host = host if ":" not in host else f"[{host}]"
+        url = f"http://{host}:{port}"
+        root_url = f"{url}/"
+        metrics_url = f"{url}/metrics"
+
+        async with aiohttp.ClientSession() as session:
+
+            # Access root to increment metric counter
+            async with session.get(root_url) as response:
+                self.assertEqual(response.status, 200)
+
+            # Get default format
+            async with session.get(
+                metrics_url, headers={aiohttp.hdrs.ACCEPT: "*/*"}
+            ) as response:
+                self.assertEqual(response.status, 200)
+                self.assertIn(
+                    aioprometheus.formats.TEXT_CONTENT_TYPE,
+                    response.headers.get("content-type"),
+                )
+                # content = await response.read()
+
+            # Get text format
+            async with session.get(
+                metrics_url, headers={aiohttp.hdrs.ACCEPT: "text/plain;"}
+            ) as response:
+                self.assertEqual(response.status, 200)
+                self.assertIn(
+                    aioprometheus.formats.TEXT_CONTENT_TYPE,
+                    response.headers.get("content-type"),
+                )
+
+            # Get binary format
+            async with session.get(
+                metrics_url,
+                headers={
+                    aiohttp.hdrs.ACCEPT: aioprometheus.formats.BINARY_CONTENT_TYPE
+                },
+            ) as response:
+                self.assertEqual(response.status, 200)
+                self.assertIn(
+                    aioprometheus.formats.BINARY_CONTENT_TYPE,
+                    response.headers.get("content-type"),
+                )
+
+        await runner.cleanup()
diff --git a/tests/test_quart.py b/tests/test_quart.py
new file mode 100644
index 0000000..4a85361
--- /dev/null
+++ b/tests/test_quart.py
@@ -0,0 +1,72 @@
+import asynctest
+import unittest
+import aioprometheus
+
+try:
+    from quart import Quart, request
+
+    have_quart = True
+except ImportError:
+    have_quart = False
+
+
+@unittest.skipUnless(have_quart, "Quart library is not available")
+class TestQuartRender(asynctest.TestCase):
+    """
+    Test exposing Prometheus metrics from within an Quart existing web
+    service without starting a separate Prometheus metrics server.
+    """
+
+    async def test_render_in_quart_app(self):
+        """ check render usage in Quart app """
+
+        app = Quart(__name__)
+        app.registry = aioprometheus.Registry()
+        app.events_counter = aioprometheus.Counter("events", "Number of events.")
+        app.registry.register(app.events_counter)
+
+        @app.route("/")
+        async def index():
+            app.events_counter.inc({"path": "/"})
+            return "hello"
+
+        @app.route("/metrics")
+        async def handle_metrics():
+            content, http_headers = aioprometheus.render(
+                app.registry, request.headers.getlist("accept")
+            )
+            return content, http_headers
+
+        # The test client also starts the web service
+        test_client = app.test_client()
+
+        # Access root to increment metric counter
+        response = await test_client.get("/")
+        self.assertEqual(response.status_code, 200)
+
+        # Get default format
+        response = await test_client.get("/metrics", headers={"accept": "*/*"})
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(
+            aioprometheus.formats.TEXT_CONTENT_TYPE,
+            response.headers.get("content-type"),
+        )
+        # payload = await response.get_data()
+
+        # Get text format
+        response = await test_client.get("/metrics", headers={"accept": "text/plain;"})
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(
+            aioprometheus.formats.TEXT_CONTENT_TYPE,
+            response.headers.get("content-type"),
+        )
+
+        # Get binary format
+        response = await test_client.get(
+            "/metrics", headers={"accept": aioprometheus.formats.BINARY_CONTENT_TYPE}
+        )
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(
+            aioprometheus.formats.BINARY_CONTENT_TYPE,
+            response.headers.get("content-type"),
+        )
diff --git a/tests/test_renderer.py b/tests/test_renderer.py
new file mode 100644
index 0000000..a829018
--- /dev/null
+++ b/tests/test_renderer.py
@@ -0,0 +1,46 @@
+import asynctest
+import aioprometheus
+
+
+class TestRenderer(asynctest.TestCase):
+    async def test_invalid_registry(self):
+        """ check only valid registry can be provided """
+        for invalid_registry in ["nope", dict(), list()]:
+            with self.assertRaises(Exception) as cm:
+                aioprometheus.render(invalid_registry, [])
+            self.assertIn("registry must be a Registry, got:", str(cm.exception))
+
+    async def test_invalid_accepts_headers(self):
+        """ check only valid accepts_headers types can be provided """
+        registry = aioprometheus.Registry()
+        for accepts_headers in ["nope", None, 42, dict()]:
+            with self.assertRaises(Exception) as cm:
+                aioprometheus.render(registry, accepts_headers)
+            self.assertIn("accepts_headers must be a sequence, got:", str(cm.exception))
+
+    async def test_render_default(self):
+        """ check metrics can be rendered using default format """
+        accepts_headers = ("application/json", "*/*", "application/nothing")
+        registry = aioprometheus.Registry()
+        content, http_headers = aioprometheus.render(registry, accepts_headers)
+        self.assertEqual(
+            http_headers["Content-Type"], aioprometheus.formats.TEXT_CONTENT_TYPE
+        )
+
+    async def test_render_text(self):
+        """ check metrics can be rendered using text format """
+        accepts_headers = ("text/plain;",)
+        registry = aioprometheus.Registry()
+        content, http_headers = aioprometheus.render(registry, accepts_headers)
+        self.assertEqual(
+            http_headers["Content-Type"], aioprometheus.formats.TEXT_CONTENT_TYPE
+        )
+
+    async def test_render_binary(self):
+        """ check metrics can be rendered using binary format """
+        accepts_headers = (aioprometheus.formats.BINARY_CONTENT_TYPE,)
+        registry = aioprometheus.Registry()
+        content, http_headers = aioprometheus.render(registry, accepts_headers)
+        self.assertEqual(
+            http_headers["Content-Type"], aioprometheus.formats.BINARY_CONTENT_TYPE
+        )
diff --git a/tests/test_vibora.py b/tests/test_vibora.py
new file mode 100644
index 0000000..0fad923
--- /dev/null
+++ b/tests/test_vibora.py
@@ -0,0 +1,81 @@
+import asynctest
+import unittest
+import aioprometheus
+
+try:
+    from vibora import Vibora, Request, Response
+
+    have_vibora = True
+except ImportError:
+    have_vibora = False
+
+
+@unittest.skipUnless(have_vibora, "Vibora library is not available")
+class TestViboraRender(asynctest.TestCase):
+    """
+    Test exposing Prometheus metrics from within an Vibora existing web
+    service without starting a separate Prometheus metrics server.
+    """
+
+    async def test_render_in_vibora_app(self):
+        """ check render usage in Vibora app """
+
+        app = Vibora(__name__)
+        app.registry = aioprometheus.Registry()
+        app.events_counter = aioprometheus.Counter("events", "Number of events.")
+        app.registry.register(app.events_counter)
+
+        @app.route("/")
+        async def index(request: Request):
+            app.events_counter.inc({"path": "/"})
+            return Response(b"hello")
+
+        @app.route("/metrics")
+        async def handle_metrics(request: Request):
+            """
+            Negotiate a response format by inspecting the ACCEPTS headers and selecting
+            the most efficient format. Render metrics in the registry into the chosen
+            format and return a response.
+            """
+            content, http_headers = aioprometheus.render(
+                app.registry, [request.headers.get("accept")]
+            )
+            return Response(content, headers=http_headers)
+
+        # NOTE: Vibora client.get HTTP headers handling seem to expect case-sensitive.
+        # Must use Accept and not accept or ACCEPT! Where as response handling of
+        # requests doesn't seem to care.
+        # Until Vibora #139 is resolved we must use "Accept".
+
+        # The test client also starts the web service
+        client = app.test_client()
+
+        # Access root to increment metric counter
+        response = await client.get("/")
+        self.assertEqual(response.status_code, 200)
+
+        # Get default format
+        response = await client.get("/metrics", headers={"Accept": "*/*"})
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(
+            aioprometheus.formats.TEXT_CONTENT_TYPE,
+            [response.headers.get("Content-Type")],
+        )
+
+        # Get text format
+        response = await client.get("/metrics", headers={"Accept": "text/plain;"})
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(
+            aioprometheus.formats.TEXT_CONTENT_TYPE,
+            [response.headers.get("content-type")],
+        )
+
+        # # Get binary format
+        response = await client.get(
+            "/metrics", headers={"Accept": aioprometheus.formats.BINARY_CONTENT_TYPE}
+        )
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(
+            aioprometheus.formats.BINARY_CONTENT_TYPE,
+            [response.headers.get("content-type")],
+        )

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-asyncio black sphinx coverage mypy twine wheel
apt-get update apt-get install -y gcc
3.6
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
aiohttp==3.8.6
-e git+https://github.com/claws/aioprometheus.git@bc9975761a1b938e91ec1aa81120d8a78c911d21#egg=aioprometheus
aiosignal==1.2.0
alabaster==0.7.13
async-timeout==4.0.2
asynctest==0.13.0
attrs==22.2.0
Babel==2.11.0
black==22.8.0
bleach==4.1.0
certifi==2021.5.30
cffi==1.15.1
charset-normalizer==2.0.12
click==8.0.4
colorama==0.4.5
coverage==6.2
cryptography==40.0.2
dataclasses==0.8
docutils==0.18.1
frozenlist==1.2.0
idna==3.10
idna-ssl==1.1.0
imagesize==1.4.1
importlib-metadata==4.8.3
importlib-resources==5.4.0
iniconfig==1.1.1
jeepney==0.7.1
Jinja2==3.0.3
keyring==23.4.1
MarkupSafe==2.0.1
multidict==5.2.0
mypy==0.971
mypy-extensions==1.0.0
packaging==21.3
pathspec==0.9.0
pkginfo==1.10.0
platformdirs==2.4.0
pluggy==1.0.0
prometheus-metrics-proto==18.1.2
protobuf==3.19.6
py==1.11.0
pycparser==2.21
Pygments==2.14.0
pyparsing==3.1.4
pytest==7.0.1
pytest-asyncio==0.16.0
pytz==2025.2
quantile-python==1.1
readme-renderer==34.0
requests==2.27.1
requests-toolbelt==1.0.0
rfc3986==1.5.0
SecretStorage==3.3.3
six==1.17.0
snowballstemmer==2.2.0
Sphinx==5.3.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
tomli==1.2.3
tqdm==4.64.1
twine==3.8.0
typed-ast==1.5.5
typing_extensions==4.1.1
urllib3==1.26.20
webencodings==0.5.1
yarl==1.7.2
zipp==3.6.0

name: aioprometheus
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - aiohttp==3.8.6
      - aiosignal==1.2.0
      - alabaster==0.7.13
      - async-timeout==4.0.2
      - asynctest==0.13.0
      - attrs==22.2.0
      - babel==2.11.0
      - black==22.8.0
      - bleach==4.1.0
      - cffi==1.15.1
      - charset-normalizer==2.0.12
      - click==8.0.4
      - colorama==0.4.5
      - coverage==6.2
      - cryptography==40.0.2
      - dataclasses==0.8
      - docutils==0.18.1
      - frozenlist==1.2.0
      - idna==3.10
      - idna-ssl==1.1.0
      - imagesize==1.4.1
      - importlib-metadata==4.8.3
      - importlib-resources==5.4.0
      - iniconfig==1.1.1
      - jeepney==0.7.1
      - jinja2==3.0.3
      - keyring==23.4.1
      - markupsafe==2.0.1
      - multidict==5.2.0
      - mypy==0.971
      - mypy-extensions==1.0.0
      - packaging==21.3
      - pathspec==0.9.0
      - pkginfo==1.10.0
      - platformdirs==2.4.0
      - pluggy==1.0.0
      - prometheus-metrics-proto==18.1.2
      - protobuf==3.19.6
      - py==1.11.0
      - pycparser==2.21
      - pygments==2.14.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - pytest-asyncio==0.16.0
      - pytz==2025.2
      - quantile-python==1.1
      - readme-renderer==34.0
      - requests==2.27.1
      - requests-toolbelt==1.0.0
      - rfc3986==1.5.0
      - secretstorage==3.3.3
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==5.3.0
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - tomli==1.2.3
      - tqdm==4.64.1
      - twine==3.8.0
      - typed-ast==1.5.5
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - webencodings==0.5.1
      - yarl==1.7.2
      - zipp==3.6.0
prefix: /opt/conda/envs/aioprometheus

tests/test_aiohttp.py::TestAiohttpRender::test_render_in_aiohttp_app tests/test_renderer.py::TestRenderer::test_invalid_accepts_headers tests/test_renderer.py::TestRenderer::test_invalid_registry tests/test_renderer.py::TestRenderer::test_render_binary tests/test_renderer.py::TestRenderer::test_render_default tests/test_renderer.py::TestRenderer::test_render_text
README.rst examples/simple-example.py examples/frameworks/aiohttp-example.py examples/frameworks/vibora-example.py docs/user/index.rst src/aioprometheus/__init__.py src/aioprometheus/formats/text.py src/aioprometheus/service.py examples/app-example.py src/aioprometheus/renderer.py src/aioprometheus/formats/binary.py src/aioprometheus/negotiator.py examples/frameworks/quart-example.py docs/index.rst requirements.txt src/aioprometheus/formats/__init__.py
README.rst examples/simple-example.py examples/frameworks/aiohttp-example.py examples/frameworks/vibora-example.py docs/user/index.rst src/aioprometheus/__init__.py src/aioprometheus/formats/text.py src/aioprometheus/service.py examples/app-example.py src/aioprometheus/renderer.py src/aioprometheus/formats/binary.py src/aioprometheus/negotiator.py examples/frameworks/quart-example.py docs/index.rst requirements.txt src/aioprometheus/formats/__init__.py