Improved metrics handling - remove timestamps
All checks were successful
Build Docker Image / build-and-deploy-image (push) Successful in 43s
All checks were successful
Build Docker Image / build-and-deploy-image (push) Successful in 43s
This commit is contained in:
77
app.py
77
app.py
@@ -1,71 +1,82 @@
|
|||||||
import contextlib
|
import contextlib
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from collections.abc import Awaitable, Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import orjson
|
import orjson
|
||||||
from environs import Env
|
from environs import Env
|
||||||
|
|
||||||
env = Env()
|
env: Env = Env()
|
||||||
env.read_env()
|
env.read_env()
|
||||||
|
|
||||||
unit_log_file: str = env.str("UNIT_LOG_FILE")
|
unit_log_file: str = env.str("UNIT_LOG_FILE")
|
||||||
|
|
||||||
|
|
||||||
def escape_label_value(value):
|
def escape_label_value(value: str) -> str:
|
||||||
return value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
return value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
||||||
|
|
||||||
|
|
||||||
async def read_and_process_logs():
|
async def read_and_process_logs() -> dict[tuple[str, str], dict[str, float]]:
|
||||||
metric_lines = []
|
metrics_data: dict[tuple[str, str], dict[str, float]] = {}
|
||||||
|
|
||||||
if os.path.exists(unit_log_file):
|
if os.path.exists(unit_log_file):
|
||||||
async with aiofiles.open(unit_log_file, mode="r+") as f:
|
async with aiofiles.open(unit_log_file, mode="r+") as f:
|
||||||
lines = await f.readlines()
|
lines: list[str] = await f.readlines()
|
||||||
await f.truncate(0)
|
await f.truncate(0)
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
log_data = orjson.loads(line)
|
log_data: dict[str, str] = orjson.loads(line)
|
||||||
request_time = float(log_data["request_time"])
|
request_time: float = float(log_data["request_time"])
|
||||||
url = log_data["url"]
|
url: str = log_data["url"]
|
||||||
http_method = log_data["http_method"]
|
http_method: str = log_data["http_method"]
|
||||||
datetime_str = log_data["datetime"]
|
key: tuple[str, str] = (url, http_method)
|
||||||
|
|
||||||
# Parse datetime_str to Unix timestamp in milliseconds
|
if key in metrics_data:
|
||||||
dt = datetime.strptime(datetime_str, "%d/%b/%Y:%H:%M:%S %z")
|
metrics_data[key]["count"] += 1
|
||||||
timestamp_ms = int(dt.timestamp() * 1000)
|
metrics_data[key]["sum"] += request_time
|
||||||
|
else:
|
||||||
|
metrics_data[key] = {"count": 1, "sum": request_time}
|
||||||
|
|
||||||
labels = (
|
return metrics_data
|
||||||
f'url="{escape_label_value(url)}",'
|
|
||||||
f'http_method="{escape_label_value(http_method)}"'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Generate the metric line
|
|
||||||
metric_line = f"request_duration_seconds{{{labels}}} {request_time} {timestamp_ms}"
|
|
||||||
metric_lines.append(metric_line)
|
|
||||||
return metric_lines
|
|
||||||
|
|
||||||
|
|
||||||
async def generate_metrics_text():
|
async def generate_metrics_text() -> str:
|
||||||
lines = [
|
lines: list[str] = [
|
||||||
"# HELP request_duration_seconds Request duration in seconds",
|
"# HELP request_duration_seconds_total Total request duration in seconds",
|
||||||
"# TYPE request_duration_seconds gauge",
|
"# TYPE request_duration_seconds_total counter",
|
||||||
|
"# HELP request_duration_seconds_count Total number of requests",
|
||||||
|
"# TYPE request_duration_seconds_count counter",
|
||||||
]
|
]
|
||||||
|
|
||||||
metric_lines = await read_and_process_logs()
|
metrics_data: dict[tuple[str, str], dict[str, float]] = await read_and_process_logs()
|
||||||
|
|
||||||
lines.extend(metric_lines)
|
for key, data in metrics_data.items():
|
||||||
|
url, http_method = key
|
||||||
|
count: int = int(data["count"])
|
||||||
|
sum_values: float = data["sum"]
|
||||||
|
|
||||||
|
labels: str = (
|
||||||
|
f'url="{escape_label_value(url)}",' f'http_method="{escape_label_value(http_method)}"'
|
||||||
|
)
|
||||||
|
|
||||||
|
lines.append(f"request_duration_seconds_count{{{labels}}} {count}")
|
||||||
|
lines.append(f"request_duration_seconds_total{{{labels}}} {sum_values}")
|
||||||
|
|
||||||
return "\n".join(lines) + "\n"
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
async def app(scope, receive, send):
|
async def app(
|
||||||
|
scope: dict[str, Any],
|
||||||
|
_: Callable[[], Awaitable[dict[str, Any]]],
|
||||||
|
send: Callable[[dict[str, Any]], Awaitable[None]],
|
||||||
|
) -> None:
|
||||||
if scope["type"] == "http":
|
if scope["type"] == "http":
|
||||||
if scope["path"] == "/metrics":
|
if scope["path"] == "/metrics":
|
||||||
headers = [(b"content-type", b"text/plain; charset=utf-8")]
|
headers: list[tuple[bytes, bytes]] = [(b"content-type", b"text/plain; charset=utf-8")]
|
||||||
metrics_text = await generate_metrics_text()
|
metrics_text: str = await generate_metrics_text()
|
||||||
body = metrics_text.encode("utf-8")
|
body: bytes = metrics_text.encode("utf-8")
|
||||||
|
|
||||||
await send({"type": "http.response.start", "status": 200, "headers": headers})
|
await send({"type": "http.response.start", "status": 200, "headers": headers})
|
||||||
await send({"type": "http.response.body", "body": body})
|
await send({"type": "http.response.body", "body": body})
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ dependencies = [
|
|||||||
"environs>=11.0.0",
|
"environs>=11.0.0",
|
||||||
"granian>=1.6.0",
|
"granian>=1.6.0",
|
||||||
"orjson>=3.10.7",
|
"orjson>=3.10.7",
|
||||||
"prometheus-client>=0.21.0",
|
|
||||||
"uvloop>=0.20.0",
|
"uvloop>=0.20.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
11
uv.lock
generated
11
uv.lock
generated
@@ -91,7 +91,6 @@ dependencies = [
|
|||||||
{ name = "environs" },
|
{ name = "environs" },
|
||||||
{ name = "granian" },
|
{ name = "granian" },
|
||||||
{ name = "orjson" },
|
{ name = "orjson" },
|
||||||
{ name = "prometheus-client" },
|
|
||||||
{ name = "uvloop" },
|
{ name = "uvloop" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -101,7 +100,6 @@ requires-dist = [
|
|||||||
{ name = "environs", specifier = ">=11.0.0" },
|
{ name = "environs", specifier = ">=11.0.0" },
|
||||||
{ name = "granian", specifier = ">=1.6.0" },
|
{ name = "granian", specifier = ">=1.6.0" },
|
||||||
{ name = "orjson", specifier = ">=3.10.7" },
|
{ name = "orjson", specifier = ">=3.10.7" },
|
||||||
{ name = "prometheus-client", specifier = ">=0.21.0" },
|
|
||||||
{ name = "uvloop", specifier = ">=0.20.0" },
|
{ name = "uvloop", specifier = ">=0.20.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -138,15 +136,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 },
|
{ url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "prometheus-client"
|
|
||||||
version = "0.21.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/54/a369868ed7a7f1ea5163030f4fc07d85d22d7a1d270560dab675188fb612/prometheus_client-0.21.0.tar.gz", hash = "sha256:96c83c606b71ff2b0a433c98889d275f51ffec6c5e267de37c7a2b5c9aa9233e", size = 78634 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/2d/46ed6436849c2c88228c3111865f44311cff784b4aabcdef4ea2545dbc3d/prometheus_client-0.21.0-py3-none-any.whl", hash = "sha256:4fa6b4dd0ac16d58bb587c04b1caae65b8c5043e85f778f42f5f632f6af2e166", size = 54686 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user