forked from claw/flaskpaste
feat: add observability and CLI enhancements
Audit logging: - audit_log table with event tracking - app/audit.py module with log_event(), query_audit_log() - GET /audit endpoint (admin only) - configurable retention and cleanup Prometheus metrics: - app/metrics.py with custom counters - paste create/access/delete, rate limit, PoW, dedup metrics - instrumentation in API routes CLI clipboard integration: - fpaste create -C/--clipboard (read from clipboard) - fpaste create --copy-url (copy result URL) - fpaste get -c/--copy (copy content) - cross-platform: xclip, xsel, pbcopy, wl-copy Shell completions: - completions/ directory with bash/zsh/fish scripts - fpaste completion --shell command
This commit is contained in:
160
app/metrics.py
Normal file
160
app/metrics.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""Custom Prometheus metrics for FlaskPaste."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from flask import Flask
|
||||
|
||||
# Metrics are only initialized when not testing
|
||||
_paste_created_counter = None
|
||||
_paste_accessed_counter = None
|
||||
_paste_deleted_counter = None
|
||||
_rate_limit_counter = None
|
||||
_pow_counter = None
|
||||
_dedup_counter = None
|
||||
_request_duration_histogram = None
|
||||
|
||||
|
||||
def setup_custom_metrics(app: Flask) -> None:
|
||||
"""Initialize custom Prometheus metrics.
|
||||
|
||||
Should be called after prometheus_flask_exporter is set up.
|
||||
"""
|
||||
global _paste_created_counter, _paste_accessed_counter, _paste_deleted_counter
|
||||
global _rate_limit_counter, _pow_counter, _dedup_counter, _request_duration_histogram
|
||||
|
||||
if app.config.get("TESTING"):
|
||||
return
|
||||
|
||||
try:
|
||||
from prometheus_client import Counter, Histogram
|
||||
except ImportError:
|
||||
app.logger.warning("prometheus_client not available, custom metrics disabled")
|
||||
return
|
||||
|
||||
_paste_created_counter = Counter(
|
||||
"flaskpaste_paste_created_total",
|
||||
"Total number of paste creation attempts",
|
||||
["auth_type", "outcome"],
|
||||
)
|
||||
|
||||
_paste_accessed_counter = Counter(
|
||||
"flaskpaste_paste_accessed_total",
|
||||
"Total number of paste accesses",
|
||||
["auth_type", "burn"],
|
||||
)
|
||||
|
||||
_paste_deleted_counter = Counter(
|
||||
"flaskpaste_paste_deleted_total",
|
||||
"Total number of paste deletion attempts",
|
||||
["auth_type", "outcome"],
|
||||
)
|
||||
|
||||
_rate_limit_counter = Counter(
|
||||
"flaskpaste_rate_limit_total",
|
||||
"Total number of rate limit events",
|
||||
["outcome"],
|
||||
)
|
||||
|
||||
_pow_counter = Counter(
|
||||
"flaskpaste_pow_total",
|
||||
"Total number of proof-of-work validations",
|
||||
["outcome"],
|
||||
)
|
||||
|
||||
_dedup_counter = Counter(
|
||||
"flaskpaste_dedup_total",
|
||||
"Total number of content deduplication events",
|
||||
["outcome"],
|
||||
)
|
||||
|
||||
_request_duration_histogram = Histogram(
|
||||
"flaskpaste_request_duration_seconds",
|
||||
"Request duration in seconds",
|
||||
["method", "endpoint", "status"],
|
||||
buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
|
||||
)
|
||||
|
||||
app.logger.info("Custom Prometheus metrics initialized")
|
||||
|
||||
|
||||
def record_paste_created(auth_type: str, outcome: str) -> None:
|
||||
"""Record a paste creation attempt.
|
||||
|
||||
Args:
|
||||
auth_type: "authenticated" or "anonymous"
|
||||
outcome: "success", "failure", or "blocked"
|
||||
"""
|
||||
if _paste_created_counter:
|
||||
_paste_created_counter.labels(auth_type=auth_type, outcome=outcome).inc()
|
||||
|
||||
|
||||
def record_paste_accessed(auth_type: str, burn: bool) -> None:
|
||||
"""Record a paste access.
|
||||
|
||||
Args:
|
||||
auth_type: "authenticated" or "anonymous"
|
||||
burn: True if paste was burn-after-read
|
||||
"""
|
||||
if _paste_accessed_counter:
|
||||
_paste_accessed_counter.labels(auth_type=auth_type, burn=str(burn).lower()).inc()
|
||||
|
||||
|
||||
def record_paste_deleted(auth_type: str, outcome: str) -> None:
|
||||
"""Record a paste deletion attempt.
|
||||
|
||||
Args:
|
||||
auth_type: "authenticated" or "anonymous"
|
||||
outcome: "success" or "failure"
|
||||
"""
|
||||
if _paste_deleted_counter:
|
||||
_paste_deleted_counter.labels(auth_type=auth_type, outcome=outcome).inc()
|
||||
|
||||
|
||||
def record_rate_limit(outcome: str) -> None:
|
||||
"""Record a rate limit event.
|
||||
|
||||
Args:
|
||||
outcome: "allowed" or "blocked"
|
||||
"""
|
||||
if _rate_limit_counter:
|
||||
_rate_limit_counter.labels(outcome=outcome).inc()
|
||||
|
||||
|
||||
def record_pow(outcome: str) -> None:
|
||||
"""Record a proof-of-work validation.
|
||||
|
||||
Args:
|
||||
outcome: "success" or "failure"
|
||||
"""
|
||||
if _pow_counter:
|
||||
_pow_counter.labels(outcome=outcome).inc()
|
||||
|
||||
|
||||
def record_dedup(outcome: str) -> None:
|
||||
"""Record a content deduplication event.
|
||||
|
||||
Args:
|
||||
outcome: "allowed" or "blocked"
|
||||
"""
|
||||
if _dedup_counter:
|
||||
_dedup_counter.labels(outcome=outcome).inc()
|
||||
|
||||
|
||||
def observe_request_duration(
|
||||
method: str, endpoint: str, status: int, duration: float
|
||||
) -> None:
|
||||
"""Record request duration.
|
||||
|
||||
Args:
|
||||
method: HTTP method (GET, POST, etc.)
|
||||
endpoint: Request endpoint path
|
||||
status: HTTP status code
|
||||
duration: Request duration in seconds
|
||||
"""
|
||||
if _request_duration_histogram:
|
||||
_request_duration_histogram.labels(
|
||||
method=method, endpoint=endpoint, status=str(status)
|
||||
).observe(duration)
|
||||
Reference in New Issue
Block a user