Skip to content

Commit 7e10c04

Browse files
author
Ken
committed
Adding additional logging, refactoring connection handler into separate class from server
1 parent a9c08cd commit 7e10c04

File tree

9 files changed

+106
-50
lines changed

9 files changed

+106
-50
lines changed

example/Containerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM docker.io/library/haproxy:latest
1+
FROM docker.io/library/haproxy:2.3
22

33
COPY haproxy/ /usr/local/etc/haproxy/
44

example/Pipfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ verify_ssl = true
77

88
[packages]
99
aiohttp = "*"
10+
haproxyspoa = {editable = true, path = "./.."}
1011

1112
[requires]
1213
python_version = "3.8"

example/Pipfile.lock

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/haproxy_spoa.py renamed to example/agent.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from haproxyspoa.payloads.ack import AckPayload
44
from haproxyspoa.spoa_server import SpoaServer
55

6+
67
agent = SpoaServer()
78

89

haproxyspoa/logging.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import logging
2+
3+
logging.basicConfig(
4+
level=logging.INFO,
5+
format='[%(asctime)s] [%(levelname)s] %(message)s',
6+
datefmt="%Y-%m-%dT%H:%M:%S%z"
7+
)
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
class FlowIdLoggerAdapter(logging.LoggerAdapter):
13+
14+
def process(self, msg, kwargs):
15+
return f"[{self.extra['flow_id']}] {msg}", kwargs

haproxyspoa/payloads/agent_hello.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ def support_async(self):
1919
self.supported_list.add("async")
2020
return self
2121

22+
def __str__(self):
23+
return ",".join(self.supported_list)
24+
2225

2326
class AgentHelloPayload:
2427
GENEROUS_MAX_FRAME_SIZE = 65536
@@ -32,7 +35,7 @@ def __init__(
3235
):
3336
self.version = spop_version
3437
self.max_frame_size = max_frame_size
35-
self.capabilities = ",".join(capabilities.supported_list) if capabilities is not None else ""
38+
self.capabilities = str(capabilities) if capabilities is not None else ""
3639

3740
def to_bytes(self) -> bytes:
3841
return write_kv_list({

haproxyspoa/spoa_server.py

Lines changed: 70 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import asyncio
22
import functools
3-
import logging
43
from collections import defaultdict
54
from typing import List
65

6+
from haproxyspoa.logging import logger, FlowIdLoggerAdapter
77
from haproxyspoa.payloads.ack import AckPayload
88
from haproxyspoa.payloads.agent_disconnect import DisconnectStatusCode, AgentDisconnectPayload
99
from haproxyspoa.payloads.agent_hello import AgentHelloPayload, AgentCapabilities
@@ -12,91 +12,116 @@
1212
from haproxyspoa.payloads.notify import NotifyPayload
1313
from haproxyspoa.spoa_frame import Frame, AgentHelloFrame, FrameType
1414

15+
import secrets
1516

16-
class SpoaServer:
17-
18-
def __init__(self):
19-
self.handlers = defaultdict(list)
20-
21-
def handler(self, message_key: str):
22-
def _handler(fn):
23-
@functools.wraps(fn)
24-
def wrapper(*args, **kwargs):
25-
return fn(*args, **kwargs)
26-
self.handlers[message_key].append(wrapper)
27-
return wrapper
28-
return _handler
29-
30-
async def handle_connection(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
31-
haproxy_hello_frame = await Frame.read_frame(reader)
32-
if not haproxy_hello_frame.headers.is_haproxy_hello():
33-
await self.send_agent_disconnect(writer)
34-
return
35-
await self.handle_hello_handshake(haproxy_hello_frame, writer)
36-
37-
if HaproxyHelloPayload(haproxy_hello_frame.payload).healthcheck():
38-
logging.info("It is just a health check, immediately disconnecting")
39-
return
4017

41-
logging.info("Completed new handshake with Haproxy")
18+
class SpoaConnection:
19+
20+
def __init__(self, writer: asyncio.StreamWriter, handlers):
21+
self.logger = FlowIdLoggerAdapter(logger, {"flow_id": secrets.token_hex(4)})
22+
self.handlers = handlers
23+
self.writer = writer
4224

43-
while True:
44-
frame = await Frame.read_frame(reader)
45-
46-
if frame.headers.is_haproxy_disconnect():
47-
await self.handle_haproxy_disconnect(frame)
48-
await self.send_agent_disconnect(writer)
49-
return
50-
elif frame.headers.is_haproxy_notify():
51-
await self.handle_haproxy_notify(frame, writer)
52-
53-
async def handle_haproxy_notify(self, frame: Frame, writer: asyncio.StreamWriter):
25+
async def handle_haproxy_notify(self, frame: Frame):
26+
self.logger.debug("Incoming `notify` frame from HAProxy")
5427
notify_payload = NotifyPayload(frame.payload)
5528

5629
response_futures = []
5730
for msg_key, msg_val in notify_payload.messages.items():
31+
self.logger.info(f"Received request on key '{msg_key}'")
5832
for handler in self.handlers[msg_key]:
5933
response_futures.append(handler(**notify_payload.messages[msg_key]))
6034

35+
self.logger.info(f"Found {len(response_futures)} matching handlers, awaiting response...")
6136
ack_payloads: List[AckPayload] = await asyncio.gather(*response_futures)
6237
ack = AckPayload.create_from_all(*ack_payloads)
38+
payload = ack.to_bytes()
39+
40+
self.logger.info(f"Responding with combined payload of {len(payload.getbuffer())} bytes")
6341

6442
ack_frame = Frame(
6543
frame_type=FrameType.ACK,
6644
stream_id=frame.headers.stream_id,
6745
frame_id=frame.headers.frame_id,
6846
flags=1,
69-
payload=ack.to_bytes()
47+
payload=payload
7048
)
71-
await ack_frame.write_frame(writer)
49+
await ack_frame.write_frame(self.writer)
7250

73-
async def send_agent_disconnect(self, writer: asyncio.StreamWriter):
51+
async def send_agent_disconnect(self):
52+
self.logger.info("Agent is now dropping connection")
7453
disconnect_frame = Frame(
7554
frame_type=FrameType.AGENT_DISCONNECT,
7655
flags=1,
7756
stream_id=0,
7857
frame_id=0,
7958
payload=AgentDisconnectPayload().to_buffer()
8059
)
81-
await disconnect_frame.write_frame(writer)
60+
await disconnect_frame.write_frame(self.writer)
8261

8362
async def handle_haproxy_disconnect(self, frame: Frame):
8463
payload = HaproxyDisconnectPayload(frame.payload)
8564
if payload.status_code() != DisconnectStatusCode.NORMAL:
86-
logging.info(f"Haproxy is disconnecting us with status code {payload.status_code()} - `{payload.message()}`")
65+
self.logger.info(f"Haproxy is disconnecting us with status code {payload.status_code()} - `{payload.message()}`")
8766

88-
async def handle_hello_handshake(self, frame: Frame, writer: asyncio.StreamWriter):
67+
async def handle_hello_handshake(self, frame: Frame):
68+
capabilities = AgentCapabilities()
69+
self.logger.info(f"Received `hello handshake`, responding with agent capabilities of: '{capabilities}'")
8970
agent_hello_frame = AgentHelloFrame(
9071
payload=AgentHelloPayload(
91-
capabilities=AgentCapabilities()
72+
capabilities=capabilities,
9273
),
9374
stream_id=frame.headers.stream_id,
9475
frame_id=frame.headers.frame_id,
9576
)
96-
await agent_hello_frame.write_frame(writer)
77+
await agent_hello_frame.write_frame(self.writer)
78+
79+
80+
class SpoaServer:
81+
82+
def __init__(self):
83+
self.handlers = defaultdict(list)
84+
85+
def handler(self, message_key: str):
86+
def _handler(fn):
87+
@functools.wraps(fn)
88+
def wrapper(*args, **kwargs):
89+
return fn(*args, **kwargs)
90+
self.handlers[message_key].append(wrapper)
91+
return wrapper
92+
return _handler
93+
94+
async def handle_connection(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
95+
conn = SpoaConnection(writer, self.handlers)
96+
97+
haproxy_hello_frame = await Frame.read_frame(reader)
98+
99+
if not haproxy_hello_frame.headers.is_haproxy_hello():
100+
conn.logger.error(f"""
101+
Expected a `hello` frame from HAProxy,
102+
but received unexpected frame of type {haproxy_hello_frame.headers.frame_type}
103+
""".strip())
104+
await conn.send_agent_disconnect()
105+
return
106+
await conn.handle_hello_handshake(haproxy_hello_frame)
107+
108+
if HaproxyHelloPayload(haproxy_hello_frame.payload).healthcheck():
109+
conn.logger.info("Health check, immediately disconnecting")
110+
return
111+
112+
while True:
113+
frame = await Frame.read_frame(reader)
114+
115+
if frame.headers.is_haproxy_disconnect():
116+
await conn.handle_haproxy_disconnect(frame)
117+
await conn.send_agent_disconnect()
118+
return
119+
elif frame.headers.is_haproxy_notify():
120+
await conn.handle_haproxy_notify(frame)
97121

98122
async def _run(self, host: str = "0.0.0.0", port: int = 9002):
99123
server = await asyncio.start_server(self.handle_connection, host=host, port=port, )
124+
logger.info(f"HAProxy SPO Agent listening at {host}:{port}")
100125
await server.serve_forever()
101126

102127
def run(self, *args, **kwargs):

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
requires = ["setuptools", "wheel"]
3+
build-backend = "setuptools.build_meta:__legacy__"

setup.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@
1616
classifiers=[
1717
"License :: OSI Approved :: Apache Software License",
1818
"Programming Language :: Python :: 3",
19-
# "Programming Language :: Python :: 3.7",
19+
"Programming Language :: Python :: 3.7",
2020
"Programming Language :: Python :: 3.8",
2121
"Programming Language :: Python :: 3.9",
22+
"Development Status :: 2 - Pre-Alpha",
23+
"Topic :: System :: Networking",
24+
"Topic :: Internet :: WWW/HTTP",
25+
"Framework :: AsyncIO",
2226
],
23-
packages=find_packages(exclude=("example")),
27+
packages=find_packages(exclude=("example", "tests")),
2428
# include_package_data=True,
2529
install_requires=[],
2630
)

0 commit comments

Comments
 (0)