|
3 | 3 | import html |
4 | 4 | import inspect |
5 | 5 | import io |
6 | | -import json |
7 | 6 | import os |
8 | 7 | import random |
9 | 8 | import re |
|
12 | 11 | import sys |
13 | 12 | import traceback |
14 | 13 | import warnings |
15 | | -from collections import namedtuple |
16 | 14 | from collections.abc import Callable |
17 | 15 | from datetime import datetime, timezone |
18 | 16 | from http import HTTPStatus |
|
26 | 24 | BaseModel, |
27 | 25 | BeforeValidator, |
28 | 26 | ConfigDict, |
| 27 | + Field, |
29 | 28 | ValidationError, |
30 | 29 | WithJsonSchema, |
31 | 30 | field_validator, |
|
64 | 63 | } |
65 | 64 | # Match something that looks like a four digit year |
66 | 65 | YEAR_RE = re.compile(r"^\d{4}") |
67 | | -TELEMETRY = namedtuple( |
68 | | - "TELEMETRY", |
69 | | - [ |
70 | | - "timing", |
71 | | - "status_code", |
72 | | - "client_addr", |
73 | | - "app", |
74 | | - "request_uri", |
75 | | - "vhost", |
76 | | - "valid", |
77 | | - ], |
78 | | -) |
| 66 | + |
| 67 | + |
| 68 | +class TELEMETRY(BaseModel): |
| 69 | + timing: Annotated[ |
| 70 | + float, Field(description="Request processing time in seconds") |
| 71 | + ] |
| 72 | + status_code: Annotated[int, Field(description="HTTP response code")] |
| 73 | + client_addr: Annotated[ |
| 74 | + str | None, |
| 75 | + Field( |
| 76 | + pattern=( |
| 77 | + r"^(([0-9]{1,3}\.){3}[0-9]{1,3}|" |
| 78 | + r"([a-fA-F0-9:]+:+)+[a-fA-F0-9]+)$" |
| 79 | + ), |
| 80 | + description="Valid IPv4/IPv6 address", |
| 81 | + ), |
| 82 | + ] = None |
| 83 | + app: Annotated[ |
| 84 | + str | None, Field(description="App generating this telemetry") |
| 85 | + ] = None |
| 86 | + request_uri: Annotated[str | None, Field(description="Request URI")] = None |
| 87 | + vhost: Annotated[str | None, Field(description="virtual host")] = None |
| 88 | + valid: Annotated[ |
| 89 | + datetime, |
| 90 | + Field( |
| 91 | + description="Timestamp when this telemetry record was generated" |
| 92 | + ), |
| 93 | + ] |
| 94 | + |
| 95 | + |
79 | 96 | TELEMETRY_PREFIX = "Telemetry " |
80 | 97 | # A rsyslog socket established via akrherz/infra-ansible that is a side-door |
81 | 98 | # to send rsyslog messages without systemd intercepting them and filling |
@@ -213,9 +230,7 @@ def write_telemetry(data: TELEMETRY) -> bool: |
213 | 230 | # 141 is local1.notice and is critical to make this work. |
214 | 231 | # The TELEMETRY_PREFIX becomes the syslog tag |
215 | 232 | payload = ( |
216 | | - "<141>" |
217 | | - + TELEMETRY_PREFIX |
218 | | - + json.dumps(data._asdict(), separators=(",", ":"), sort_keys=True) |
| 233 | + "<141>" + TELEMETRY_PREFIX + data.model_dump_json(indent=None) |
219 | 234 | ).encode("utf-8") |
220 | 235 | # We need to avoid syslog as systemd/journal will intercept this and |
221 | 236 | # fill logs quickly. |
@@ -621,13 +636,13 @@ def _iemapp_emit_telemetry( |
621 | 636 | end_time = datetime.now(timezone.utc) |
622 | 637 | write_telemetry( |
623 | 638 | TELEMETRY( |
624 | | - (end_time - start_time).total_seconds(), |
625 | | - status_code, |
626 | | - environ.get("REMOTE_ADDR"), |
627 | | - environ.get("SCRIPT_NAME"), |
628 | | - environ.get("REQUEST_URI"), |
629 | | - environ.get("HTTP_HOST"), |
630 | | - end_time.strftime(ISO8601), |
| 639 | + timing=(end_time - start_time).total_seconds(), |
| 640 | + status_code=status_code, |
| 641 | + client_addr=environ.get("REMOTE_ADDR"), |
| 642 | + app=environ.get("SCRIPT_NAME"), |
| 643 | + request_uri=environ.get("REQUEST_URI"), |
| 644 | + vhost=environ.get("HTTP_HOST"), |
| 645 | + valid=end_time.strftime(ISO8601), |
631 | 646 | ) |
632 | 647 | ) |
633 | 648 |
|
|
0 commit comments