1
0
Fork 0

feat: prometheus sdk for circuitpython

This commit is contained in:
ssube 2019-08-17 09:00:57 -05:00
commit 26b88e364a
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
8 changed files with 325 additions and 0 deletions

21
LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2019 Sean Sube
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.

84
README.md Normal file
View File

@ -0,0 +1,84 @@
# prometheus_express
A Prometheus SDK for CircuitPython/MicroPython boards.
- only depends on `socket`
- API-compatible with [prometheus/client_python](https://github.com/prometheus/client_python)
- not terribly slow (`wrk` reports upwards of 100rps with 2 metrics)
## Supported Features
### HTTP
This module implements a very rudimentary HTTP server that likely violates some part of the spec. However, it works
with Chrome, curl, and Prometheus itself.
Call `start_http_server(port)` to bind a socket and begin listening.
Call `await_http_request(server, registry)` to await an incoming HTTP request and respond with the metrics in
`registry`. As
## Metric Types
Currently, `Counter` and `Gauge` are the only metric types implemented.
### Counter
Both `inc` and `dec` are implemented.
### Gauge
Extends [counter](#counter) with `set`.
### Labels
Labels are not yet implemented.
## Known Issues
### Load Causes OSError
Load testing the HTTP endpoint may cause one of a variety of `OSError`s, often `errno` 3, 4, or 7.
Not sure what is causing the errors, but it is not predictable and may not appear immediately:
```shell
> ./wrk -c 1 -d 60s -t 1 http://server:8080/
Running 1m test @ http://server:8080/
1 threads and 1 connections
^C Thread Stats Avg Stdev Max +/- Stdev
Latency 8.64ms 485.57us 12.81ms 97.75%
Req/Sec 111.60 5.21 121.00 71.00%
2222 requests in 20.61s, 671.83KB read
Socket errors: connect 0, read 1, write 2743, timeout 0
Requests/sec: 107.82
Transfer/sec: 32.60KB
```
Some are fatal:
```
Connection from ('client', 8080)
Accepting...
Connection from ('client', 8080)
Accepting...
Error accepting request: [Errno 128] ENOTCONN
Binding: server:8080
Traceback (most recent call last):
File "code.py", line 90, in <module>
File "code.py", line 87, in main
File "code.py", line 87, in main
File "code.py", line 55, in bind
File "/lib/prometheus_express/http.py", line 11, in start_http_server
OSError: 4
```
Others require the socket to be rebound:
```
Connection from ('10.2.1.193', 8080)
Accepting...
Error accepting request: 7
Binding: 10.2.2.136:8080
Accepting...
```

View File

@ -0,0 +1,93 @@
# custom
from prometheus_express.http import start_http_server, await_http_request
from prometheus_express.metric import Counter, Gauge
from prometheus_express.registry import CollectorRegistry
# system
import board
import busio
import digitalio
import random
import socket
import time
# hardware
import neopixel
import wiznet
# colors
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
# set up networking
spi = busio.SPI(clock=board.SCK, MOSI=board.MOSI, MISO=board.MISO)
eth = wiznet.WIZNET5K(spi, board.D10, board.D11)
eth.dhcp = True
# initialize the LEDs
led = digitalio.DigitalInOut(board.D13)
led.direction = digitalio.Direction.OUTPUT
rgb = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2)
def check_network():
online = eth.connected
network = eth.ifconfig()
led.value = online
print('Online: {}'.format(online))
if online == False:
return False
print('Network: {}'.format(network))
if network[0] == '0.0.0.0':
return False
return True
def bind():
ip_addr = eth.ifconfig()[0]
ip_port = 8080
print('Binding: {}:{}'.format(ip_addr, ip_port))
return (start_http_server(ip_port, address=ip_addr), True)
def main():
ready = False
bound = False
server = False
registry = CollectorRegistry()
metric_c = Counter('prom_express_test_counter',
'a test counter', registry=registry)
metric_g = Gauge('prom_express_test_gauge',
'a test gauge', registry=registry)
rgb[0] = RED # starting
while ready == False:
ready = check_network()
rgb[0] = BLUE # connected
while bound == False:
server, bound = bind()
rgb[0] = GREEN # ready
while True:
metric_c.inc(random.randint(0, 50))
metric_g.set(random.randint(0, 5000))
print('Accepting...')
try:
await_http_request(server, registry)
except OSError as err:
print('Error accepting request: {}'.format(err))
server, bound = bind()
main()

View File

View File

@ -0,0 +1,48 @@
import socket
http_encoding = 'utf-8'
def start_http_server(port, address='0.0.0.0', extraRoutes={}, metricsRoute='/metrics'):
bind_address = (address, port)
http_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
http_socket.bind(bind_address)
http_socket.listen(1)
return http_socket
def await_http_request(server_socket, registry):
conn, addr = server_socket.accept()
req = conn.recv(1024).decode(http_encoding)
print('Connection from {}'.format(addr))
line_break = '\n'.encode(http_encoding)
content_lines = registry.print()
content_data = '\n'.join(content_lines).encode(http_encoding)
content_length = len(content_data)
headers = print_http_headers(length=content_length)
try:
for line in headers:
conn.send(line.encode(http_encoding))
conn.send(line_break)
conn.send(line_break)
conn.send(content_data)
conn.close()
except OSError as err:
print('Error sending response: {}'.format(err))
def print_http_headers(status='200 OK', type='text/plain', length=0):
return [
'HTTP/1.1 {}'.format(status),
'Connection: close',
'Content-Type: {}'.format(type),
'Content-Length: {}'.format(length),
]

View File

@ -0,0 +1,57 @@
# base class for metric types
class Metric():
name = ''
desc = ''
labelKeys = []
metricType = 'untyped'
def __init__(self, name, desc, labels = [], registry = False):
self.name = name
self.desc = desc
self.labelKeys = labels
if registry != False:
registry.register(self)
# TODO: fluent API for labeling metrics
def labels(self, *labelValues):
if len(labelValues) != len(self.labelKeys):
raise ValueError('length of label values must equal label keys')
return self
def print(self, namespace):
name = self.printName(namespace)
return [
'# HELP {} {}'.format(name, self.desc),
'# TYPE {} {}'.format(name, self.metricType),
]
def printName(self, namespace):
if namespace != '':
return '{}_{}'.format(namespace, self.name)
else:
return self.name
class Counter(Metric):
metricType = 'counter'
value = 0
def inc(self, value):
self.value += value
def dec(self, value):
self.value -= value
def print(self, namespace):
return super().print(namespace) + [
'{} {}'.format(self.name, self.value)
]
class Gauge(Counter):
metricType = 'gauge'
def set(self, value):
self.value = value

View File

@ -0,0 +1,22 @@
class CollectorRegistry():
metrics = []
namespace = ''
def __init__(self, metrics=[], namespace=''):
self.metrics = set(metrics)
self.namespace = namespace
def register(self, metric):
if metric in self.metrics:
return True
self.metrics.add(metric)
return True
def print(self):
metrics = []
for m in self.metrics:
line = m.print(self.namespace)
metrics.extend(line)
return metrics

View File