feat: prometheus sdk for circuitpython
This commit is contained in:
commit
26b88e364a
|
@ -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.
|
|
@ -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...
|
||||
```
|
|
@ -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()
|
|
@ -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),
|
||||
]
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue