08 - Monitor socket (Flask / WSGI request attribution)¶
Run a Monitor with --listen-on-socket, send start/end events from a
tiny HTTP server, and get per-request energy + p50/p95 latency. This is
the recipe for Apache+mod_wsgi+Flask and any other already-running web
server -- exactly the use case from GitHub Discussion #5.
What you learn¶
- Monitor's Unix-domain-socket annotation channel.
- The
codegreen.annotate_request("label")context manager. - The
per_labelsummary in the Monitor's stop record:count,total_joules,p50_ms,p95_ms.
Prerequisites¶
pip install codegreen(stdlib only; no Flask required).- Linux or macOS with a working backend.
Source¶
examples/08_monitor_socket/main.py
"""08 Request-level energy attribution via the annotation UDS.
Starts an in-process Monitor with a Unix-domain socket listener, runs a
tiny HTTP server with 3 endpoints, hits each endpoint a few times via
`http.client`, and prints per-label energy + p50/p95 latency.
This is the XaBerr-style use case: instrument the web server with a
middleware that opens an annotation span per request; CodeGreen aggregates
the cumulative RAPL energy into per-route buckets. No Flask dependency.
"""
import http.client
import http.server
import json
import socket
import sys
import tempfile
import threading
import time
from pathlib import Path
import codegreen
HANDLERS = {
"/light": lambda: sum(range(10_000)),
"/medium": lambda: sum(i * i for i in range(200_000)),
"/heavy": lambda: sum(i * i for i in range(2_000_000)),
}
def _send(sock_path: str, op: str, **kwargs) -> dict:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(sock_path)
s.sendall((json.dumps({"op": op, **kwargs}) + "\n").encode())
resp = b""
while b"\n" not in resp:
chunk = s.recv(4096)
if not chunk:
break
resp += chunk
s.close()
return json.loads(resp.decode().strip())
class Handler(http.server.BaseHTTPRequestHandler):
sock_path = ""
def do_GET(self):
path = self.path.split("?", 1)[0]
if path not in HANDLERS:
self.send_response(404); self.end_headers(); return
ack = _send(self.sock_path, "start", name=f"GET {path}")
tok = ack["token"]
try:
result = HANDLERS[path]()
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(f"{result}\n".encode())
finally:
_send(self.sock_path, "end", token=tok, status="ok")
def log_message(self, *a, **kw): pass
def main() -> int:
with tempfile.TemporaryDirectory(prefix="cg_monitor_sock_") as td:
sock_path = str(Path(td) / "cg.sock")
m = codegreen.Monitor(interval_ms=150, listen_socket_path=sock_path).start()
Handler.sock_path = sock_path
srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
port = srv.server_address[1]
t = threading.Thread(target=srv.serve_forever, daemon=True)
t.start()
try:
time.sleep(0.2)
for path in ("/light", "/medium", "/heavy", "/heavy", "/medium"):
c = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
c.request("GET", path); c.getresponse().read(); c.close()
time.sleep(0.4)
finally:
srv.shutdown()
srv.server_close()
summary = m.stop()
per_label = summary.get("per_label", {})
print(json.dumps({"labels": {k: v for k, v in per_label.items()}}, indent=2))
print(f"OK: monitor_socket; labels={len(per_label)}; total_j={summary['total_joules']:.3f}; "
f"requests={sum(v['count'] for v in per_label.values())}")
return 0
if __name__ == "__main__":
sys.exit(main())
Run¶
Equivalent CLI¶
Flask middleware recipe¶
from flask import Flask, g, request
import codegreen
app = Flask(__name__)
@app.before_request
def _cg_start():
g._cg = codegreen.annotate_request(
f"{request.method} {request.path}",
socket_path="/tmp/cg.sock",
).__enter__()
@app.teardown_request
def _cg_end(exc):
g._cg.__exit__(None, None, None)
Start the Monitor separately:
Expected output¶
{"labels": {"GET /light": {"count": 1, ...}, "GET /heavy": {"count": 2, ...}}}
OK: monitor_socket; labels=3; total_j=0.321; requests=5
Why it matters¶
This is the first per-request energy primitive that works with the "server is already running, you can't embed instrumentation" case. The UDS listener is non-blocking, daemon-thread-local, and racing-safe; a single Monitor process can serve hundreds of workers via a shared socket.
See also: User guide: Continuous Monitoring, Python API: annotate_request.