asap-protocol 0.3.0__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- asap/__init__.py +1 -1
- asap/cli.py +137 -2
- asap/errors.py +167 -0
- asap/examples/README.md +81 -10
- asap/examples/auth_patterns.py +212 -0
- asap/examples/error_recovery.py +248 -0
- asap/examples/long_running.py +287 -0
- asap/examples/mcp_integration.py +240 -0
- asap/examples/multi_step_workflow.py +134 -0
- asap/examples/orchestration.py +293 -0
- asap/examples/rate_limiting.py +137 -0
- asap/examples/run_demo.py +9 -4
- asap/examples/secure_handler.py +84 -0
- asap/examples/state_migration.py +240 -0
- asap/examples/streaming_response.py +108 -0
- asap/examples/websocket_concept.py +129 -0
- asap/mcp/__init__.py +43 -0
- asap/mcp/client.py +224 -0
- asap/mcp/protocol.py +179 -0
- asap/mcp/server.py +333 -0
- asap/mcp/server_runner.py +40 -0
- asap/models/__init__.py +4 -0
- asap/models/base.py +0 -3
- asap/models/constants.py +76 -1
- asap/models/entities.py +58 -7
- asap/models/envelope.py +14 -1
- asap/models/ids.py +8 -4
- asap/models/parts.py +33 -3
- asap/models/validators.py +16 -0
- asap/observability/__init__.py +6 -0
- asap/observability/dashboards/README.md +24 -0
- asap/observability/dashboards/asap-detailed.json +131 -0
- asap/observability/dashboards/asap-red.json +129 -0
- asap/observability/logging.py +81 -1
- asap/observability/metrics.py +15 -1
- asap/observability/trace_parser.py +238 -0
- asap/observability/trace_ui.py +218 -0
- asap/observability/tracing.py +293 -0
- asap/state/machine.py +15 -2
- asap/state/snapshot.py +0 -9
- asap/testing/__init__.py +31 -0
- asap/testing/assertions.py +108 -0
- asap/testing/fixtures.py +113 -0
- asap/testing/mocks.py +152 -0
- asap/transport/__init__.py +31 -0
- asap/transport/cache.py +180 -0
- asap/transport/circuit_breaker.py +194 -0
- asap/transport/client.py +989 -72
- asap/transport/compression.py +389 -0
- asap/transport/handlers.py +106 -53
- asap/transport/middleware.py +64 -39
- asap/transport/server.py +461 -94
- asap/transport/validators.py +320 -0
- asap/utils/__init__.py +7 -0
- asap/utils/sanitization.py +134 -0
- asap_protocol-1.0.0.dist-info/METADATA +264 -0
- asap_protocol-1.0.0.dist-info/RECORD +70 -0
- asap_protocol-0.3.0.dist-info/METADATA +0 -227
- asap_protocol-0.3.0.dist-info/RECORD +0 -37
- {asap_protocol-0.3.0.dist-info → asap_protocol-1.0.0.dist-info}/WHEEL +0 -0
- {asap_protocol-0.3.0.dist-info → asap_protocol-1.0.0.dist-info}/entry_points.txt +0 -0
- {asap_protocol-0.3.0.dist-info → asap_protocol-1.0.0.dist-info}/licenses/LICENSE +0 -0
asap/models/parts.py
CHANGED
|
@@ -14,6 +14,10 @@ from pydantic import Discriminator, Field, TypeAdapter, field_validator
|
|
|
14
14
|
from asap.models.base import ASAPBaseModel
|
|
15
15
|
from asap.models.types import MIMEType
|
|
16
16
|
|
|
17
|
+
# Security: reject URIs that could lead to path traversal or local file access
|
|
18
|
+
PATH_TRAVERSAL_PATTERN = re.compile(r"\.\.")
|
|
19
|
+
FILE_URI_PREFIX = "file://"
|
|
20
|
+
|
|
17
21
|
|
|
18
22
|
class TextPart(ASAPBaseModel):
|
|
19
23
|
"""Plain text content part.
|
|
@@ -70,7 +74,7 @@ class FilePart(ASAPBaseModel):
|
|
|
70
74
|
|
|
71
75
|
Attributes:
|
|
72
76
|
type: Discriminator field, always "file"
|
|
73
|
-
uri: File URI (
|
|
77
|
+
uri: File URI (asap://, https://, or data:; file:// and path traversal rejected)
|
|
74
78
|
mime_type: MIME type of the file (e.g., "application/pdf")
|
|
75
79
|
inline_data: Optional base64-encoded inline file data
|
|
76
80
|
|
|
@@ -91,12 +95,38 @@ class FilePart(ASAPBaseModel):
|
|
|
91
95
|
"""
|
|
92
96
|
|
|
93
97
|
type: Literal["file"] = Field(..., description="Part type discriminator")
|
|
94
|
-
uri: str = Field(
|
|
98
|
+
uri: str = Field(
|
|
99
|
+
...,
|
|
100
|
+
description="File URI (asap://, https://, data:; file:// and .. rejected)",
|
|
101
|
+
)
|
|
95
102
|
mime_type: MIMEType = Field(..., description="MIME type (e.g., application/pdf)")
|
|
96
103
|
inline_data: str | None = Field(
|
|
97
104
|
default=None, description="Optional base64-encoded inline file data"
|
|
98
105
|
)
|
|
99
106
|
|
|
107
|
+
@field_validator("uri")
|
|
108
|
+
@classmethod
|
|
109
|
+
def validate_uri(cls, v: str) -> str:
|
|
110
|
+
"""Validate URI: reject path traversal and suspicious file:// URIs.
|
|
111
|
+
|
|
112
|
+
Rejects URIs containing '..' (path traversal) and file:// URIs
|
|
113
|
+
to prevent reading arbitrary server paths from user-supplied input.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
v: URI string to validate
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
The same URI if valid
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
ValueError: If URI contains path traversal or is a file:// URI
|
|
123
|
+
"""
|
|
124
|
+
if PATH_TRAVERSAL_PATTERN.search(v):
|
|
125
|
+
raise ValueError(f"URI must not contain path traversal (..): {v!r}")
|
|
126
|
+
if v.strip().lower().startswith(FILE_URI_PREFIX):
|
|
127
|
+
raise ValueError("file:// URIs are not allowed for security (path traversal risk)")
|
|
128
|
+
return v
|
|
129
|
+
|
|
100
130
|
@field_validator("mime_type")
|
|
101
131
|
@classmethod
|
|
102
132
|
def validate_mime_type(cls, v: str) -> str:
|
|
@@ -201,7 +231,7 @@ Example:
|
|
|
201
231
|
>>> # Deserializes to FilePart
|
|
202
232
|
>>> file_part = Part.validate_python({
|
|
203
233
|
... "type": "file",
|
|
204
|
-
... "uri": "
|
|
234
|
+
... "uri": "https://example.com/doc.pdf",
|
|
205
235
|
... "mime_type": "application/pdf"
|
|
206
236
|
... })
|
|
207
237
|
"""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Shared validators for ASAP protocol models."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from asap.models.constants import AGENT_URN_PATTERN, MAX_URN_LENGTH
|
|
6
|
+
|
|
7
|
+
_AGENT_URN_RE = re.compile(AGENT_URN_PATTERN)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def validate_agent_urn(v: str) -> str:
|
|
11
|
+
"""Validate agent URN format and length (pattern + max length)."""
|
|
12
|
+
if len(v) > MAX_URN_LENGTH:
|
|
13
|
+
raise ValueError(f"Agent URN must be at most {MAX_URN_LENGTH} characters, got {len(v)}")
|
|
14
|
+
if not _AGENT_URN_RE.match(v):
|
|
15
|
+
raise ValueError(f"Agent ID must follow URN format 'urn:asap:agent:{{name}}', got: {v}")
|
|
16
|
+
return v
|
asap/observability/__init__.py
CHANGED
|
@@ -25,6 +25,9 @@ from asap.observability.logging import (
|
|
|
25
25
|
clear_context,
|
|
26
26
|
configure_logging,
|
|
27
27
|
get_logger,
|
|
28
|
+
is_debug_log_mode,
|
|
29
|
+
is_debug_mode,
|
|
30
|
+
sanitize_for_logging,
|
|
28
31
|
)
|
|
29
32
|
from asap.observability.metrics import (
|
|
30
33
|
MetricsCollector,
|
|
@@ -38,6 +41,9 @@ __all__ = [
|
|
|
38
41
|
"configure_logging",
|
|
39
42
|
"get_logger",
|
|
40
43
|
"get_metrics",
|
|
44
|
+
"is_debug_log_mode",
|
|
45
|
+
"is_debug_mode",
|
|
41
46
|
"reset_metrics",
|
|
42
47
|
"MetricsCollector",
|
|
48
|
+
"sanitize_for_logging",
|
|
43
49
|
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# ASAP Grafana Dashboards
|
|
2
|
+
|
|
3
|
+
Pre-built dashboards for ASAP Protocol observability. Use with Prometheus scraping the `/asap/metrics` endpoint.
|
|
4
|
+
|
|
5
|
+
## Dashboards
|
|
6
|
+
|
|
7
|
+
- **asap-red.json** – RED metrics (Request rate, Error rate, Duration/latency) for ASAP requests.
|
|
8
|
+
- **asap-detailed.json** – Topology, state machine transitions, and circuit breaker status.
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
1. Configure a Prometheus datasource in Grafana (scrape ASAP server at `http://<asap-host>:<port>/asap/metrics`).
|
|
13
|
+
2. **Provisioning**: Copy the JSON files to Grafana's provisioning path, e.g.:
|
|
14
|
+
- Copy to `<grafana provisioning dir>/dashboards/asap/` and set the provisioning config to load from that directory.
|
|
15
|
+
- Or import manually: Grafana UI → Dashboards → Import → Upload JSON file.
|
|
16
|
+
3. Select the Prometheus datasource when prompted.
|
|
17
|
+
|
|
18
|
+
## Metrics used
|
|
19
|
+
|
|
20
|
+
- `asap_requests_total` – Total requests (labels: `payload_type`, `status`).
|
|
21
|
+
- `asap_requests_error_total` – Failed requests.
|
|
22
|
+
- `asap_request_duration_seconds` – Request latency histogram.
|
|
23
|
+
- `asap_state_transitions_total` – State machine transitions (labels: `from_status`, `to_status`).
|
|
24
|
+
- `asap_circuit_breaker_open` – Circuit open state (if exposed by client metrics).
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
{
|
|
2
|
+
"annotations": { "list": [] },
|
|
3
|
+
"editable": true,
|
|
4
|
+
"fiscalYearStartMonth": 0,
|
|
5
|
+
"graphTooltip": 1,
|
|
6
|
+
"id": null,
|
|
7
|
+
"links": [],
|
|
8
|
+
"liveNow": false,
|
|
9
|
+
"panels": [
|
|
10
|
+
{
|
|
11
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
12
|
+
"description": "Handler executions per second by payload type (activity view)",
|
|
13
|
+
"fieldConfig": {
|
|
14
|
+
"defaults": {
|
|
15
|
+
"color": { "mode": "palette-classic" },
|
|
16
|
+
"custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } },
|
|
17
|
+
"unit": "reqps"
|
|
18
|
+
},
|
|
19
|
+
"overrides": []
|
|
20
|
+
},
|
|
21
|
+
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 },
|
|
22
|
+
"id": 10,
|
|
23
|
+
"options": { "legend": { "displayMode": "list", "placement": "bottom", "showLegend": true } },
|
|
24
|
+
"targets": [
|
|
25
|
+
{
|
|
26
|
+
"expr": "sum(rate(asap_handler_executions_total[1m])) by (payload_type)",
|
|
27
|
+
"legendFormat": "{{payload_type}}",
|
|
28
|
+
"refId": "A"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"title": "Handler executions (by payload type)",
|
|
32
|
+
"type": "timeseries"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
36
|
+
"description": "Task state machine transition rate by from_state and to_state",
|
|
37
|
+
"fieldConfig": {
|
|
38
|
+
"defaults": {
|
|
39
|
+
"color": { "mode": "palette-classic" },
|
|
40
|
+
"custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } },
|
|
41
|
+
"unit": "reqps"
|
|
42
|
+
},
|
|
43
|
+
"overrides": []
|
|
44
|
+
},
|
|
45
|
+
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 },
|
|
46
|
+
"id": 11,
|
|
47
|
+
"options": { "legend": { "displayMode": "list", "placement": "bottom", "showLegend": true } },
|
|
48
|
+
"targets": [
|
|
49
|
+
{
|
|
50
|
+
"expr": "sum(rate(asap_state_transitions_total[1m])) by (from_status, to_status)",
|
|
51
|
+
"legendFormat": "{{from_status}} → {{to_status}}",
|
|
52
|
+
"refId": "A"
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"title": "State transitions",
|
|
56
|
+
"type": "timeseries"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
60
|
+
"description": "Circuit breaker open state (1 = open). Requires asap_circuit_breaker_open metric from client.",
|
|
61
|
+
"fieldConfig": {
|
|
62
|
+
"defaults": {
|
|
63
|
+
"color": { "mode": "thresholds" },
|
|
64
|
+
"mappings": [
|
|
65
|
+
{ "options": { "0": { "color": "green", "index": 0, "text": "closed" } }, "type": "value" },
|
|
66
|
+
{ "options": { "1": { "color": "red", "index": 0, "text": "open" } }, "type": "value" }
|
|
67
|
+
],
|
|
68
|
+
"thresholds": {
|
|
69
|
+
"mode": "absolute",
|
|
70
|
+
"steps": [
|
|
71
|
+
{ "color": "green", "value": null },
|
|
72
|
+
{ "color": "red", "value": 1 }
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
"unit": "short"
|
|
76
|
+
},
|
|
77
|
+
"overrides": []
|
|
78
|
+
},
|
|
79
|
+
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
|
|
80
|
+
"id": 12,
|
|
81
|
+
"options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
|
|
82
|
+
"targets": [
|
|
83
|
+
{
|
|
84
|
+
"expr": "asap_circuit_breaker_open",
|
|
85
|
+
"legendFormat": "{{instance}}",
|
|
86
|
+
"refId": "A"
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"title": "Circuit breaker status",
|
|
90
|
+
"type": "stat"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
94
|
+
"description": "State transition rate as table (from_state, to_state, rate)",
|
|
95
|
+
"fieldConfig": {
|
|
96
|
+
"defaults": {
|
|
97
|
+
"color": { "mode": "palette-classic" },
|
|
98
|
+
"custom": { "align": "auto", "cellOptions": { "type": "auto" }, "inspect": false },
|
|
99
|
+
"mappings": [],
|
|
100
|
+
"unit": "reqps"
|
|
101
|
+
},
|
|
102
|
+
"overrides": []
|
|
103
|
+
},
|
|
104
|
+
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
|
|
105
|
+
"id": 13,
|
|
106
|
+
"options": { "cellHeight": "sm", "footer": { "countRows": false, "fields": "", "reducer": ["sum"], "show": false }, "showHeader": true, "sortBy": [] },
|
|
107
|
+
"targets": [
|
|
108
|
+
{
|
|
109
|
+
"expr": "sum(rate(asap_state_transitions_total[1m])) by (from_status, to_status)",
|
|
110
|
+
"format": "table",
|
|
111
|
+
"instant": true,
|
|
112
|
+
"refId": "A"
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
"title": "State transitions (table)",
|
|
116
|
+
"type": "table"
|
|
117
|
+
}
|
|
118
|
+
],
|
|
119
|
+
"refresh": "10s",
|
|
120
|
+
"schemaVersion": 38,
|
|
121
|
+
"style": "dark",
|
|
122
|
+
"tags": ["asap", "detailed", "state-machine"],
|
|
123
|
+
"templating": { "list": [] },
|
|
124
|
+
"time": { "from": "now-1h", "to": "now" },
|
|
125
|
+
"timepicker": {},
|
|
126
|
+
"timezone": "utc",
|
|
127
|
+
"title": "ASAP Detailed (topology & state)",
|
|
128
|
+
"uid": "asap-detailed",
|
|
129
|
+
"version": 1,
|
|
130
|
+
"weekStart": ""
|
|
131
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
{
|
|
2
|
+
"annotations": { "list": [] },
|
|
3
|
+
"editable": true,
|
|
4
|
+
"fiscalYearStartMonth": 0,
|
|
5
|
+
"graphTooltip": 1,
|
|
6
|
+
"id": null,
|
|
7
|
+
"links": [],
|
|
8
|
+
"liveNow": false,
|
|
9
|
+
"panels": [
|
|
10
|
+
{
|
|
11
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
12
|
+
"description": "Requests per second by payload type",
|
|
13
|
+
"fieldConfig": {
|
|
14
|
+
"defaults": {
|
|
15
|
+
"color": { "mode": "palette-classic" },
|
|
16
|
+
"custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } },
|
|
17
|
+
"unit": "reqps"
|
|
18
|
+
},
|
|
19
|
+
"overrides": []
|
|
20
|
+
},
|
|
21
|
+
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 },
|
|
22
|
+
"id": 1,
|
|
23
|
+
"options": { "legend": { "displayMode": "list", "placement": "bottom", "showLegend": true } },
|
|
24
|
+
"targets": [
|
|
25
|
+
{
|
|
26
|
+
"expr": "sum(rate(asap_requests_total[1m])) by (payload_type)",
|
|
27
|
+
"legendFormat": "{{payload_type}}",
|
|
28
|
+
"refId": "A"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"title": "Request Rate",
|
|
32
|
+
"type": "timeseries"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
36
|
+
"description": "Error rate: errors / total requests",
|
|
37
|
+
"fieldConfig": {
|
|
38
|
+
"defaults": {
|
|
39
|
+
"color": { "mode": "thresholds" },
|
|
40
|
+
"mappings": [],
|
|
41
|
+
"max": 1,
|
|
42
|
+
"min": 0,
|
|
43
|
+
"thresholds": {
|
|
44
|
+
"mode": "absolute",
|
|
45
|
+
"steps": [
|
|
46
|
+
{ "color": "green", "value": null },
|
|
47
|
+
{ "color": "yellow", "value": 0.01 },
|
|
48
|
+
{ "color": "red", "value": 0.05 }
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"unit": "percentunit"
|
|
52
|
+
},
|
|
53
|
+
"overrides": []
|
|
54
|
+
},
|
|
55
|
+
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 },
|
|
56
|
+
"id": 2,
|
|
57
|
+
"options": { "legend": { "displayMode": "list", "placement": "bottom", "showLegend": true } },
|
|
58
|
+
"targets": [
|
|
59
|
+
{
|
|
60
|
+
"expr": "sum(rate(asap_requests_error_total[1m])) / (sum(rate(asap_requests_total[1m])) + 1e-9)",
|
|
61
|
+
"legendFormat": "Error rate",
|
|
62
|
+
"refId": "A"
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"title": "Error Rate",
|
|
66
|
+
"type": "timeseries"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
70
|
+
"description": "95th percentile request duration",
|
|
71
|
+
"fieldConfig": {
|
|
72
|
+
"defaults": {
|
|
73
|
+
"color": { "mode": "palette-classic" },
|
|
74
|
+
"custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } },
|
|
75
|
+
"unit": "s"
|
|
76
|
+
},
|
|
77
|
+
"overrides": []
|
|
78
|
+
},
|
|
79
|
+
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
|
|
80
|
+
"id": 3,
|
|
81
|
+
"options": { "legend": { "displayMode": "list", "placement": "bottom", "showLegend": true } },
|
|
82
|
+
"targets": [
|
|
83
|
+
{
|
|
84
|
+
"expr": "histogram_quantile(0.95, sum(rate(asap_request_duration_seconds_bucket[1m])) by (le))",
|
|
85
|
+
"legendFormat": "p95",
|
|
86
|
+
"refId": "A"
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"title": "Latency (p95)",
|
|
90
|
+
"type": "timeseries"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
94
|
+
"description": "99th percentile request duration",
|
|
95
|
+
"fieldConfig": {
|
|
96
|
+
"defaults": {
|
|
97
|
+
"color": { "mode": "palette-classic" },
|
|
98
|
+
"custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } },
|
|
99
|
+
"unit": "s"
|
|
100
|
+
},
|
|
101
|
+
"overrides": []
|
|
102
|
+
},
|
|
103
|
+
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
|
|
104
|
+
"id": 4,
|
|
105
|
+
"options": { "legend": { "displayMode": "list", "placement": "bottom", "showLegend": true } },
|
|
106
|
+
"targets": [
|
|
107
|
+
{
|
|
108
|
+
"expr": "histogram_quantile(0.99, sum(rate(asap_request_duration_seconds_bucket[1m])) by (le))",
|
|
109
|
+
"legendFormat": "p99",
|
|
110
|
+
"refId": "A"
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
"title": "Latency (p99)",
|
|
114
|
+
"type": "timeseries"
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
"refresh": "10s",
|
|
118
|
+
"schemaVersion": 38,
|
|
119
|
+
"style": "dark",
|
|
120
|
+
"tags": ["asap", "red"],
|
|
121
|
+
"templating": { "list": [] },
|
|
122
|
+
"time": { "from": "now-1h", "to": "now" },
|
|
123
|
+
"timepicker": {},
|
|
124
|
+
"timezone": "utc",
|
|
125
|
+
"title": "ASAP RED",
|
|
126
|
+
"uid": "asap-red",
|
|
127
|
+
"version": 1,
|
|
128
|
+
"weekStart": ""
|
|
129
|
+
}
|
asap/observability/logging.py
CHANGED
|
@@ -13,6 +13,8 @@ Environment Variables:
|
|
|
13
13
|
ASAP_LOG_FORMAT: Set to "json" for JSON output, "console" for colored output
|
|
14
14
|
ASAP_LOG_LEVEL: Set log level (DEBUG, INFO, WARNING, ERROR)
|
|
15
15
|
ASAP_SERVICE_NAME: Service name to include in logs
|
|
16
|
+
ASAP_DEBUG: Set to "true" or "1" to log full data and stack traces; otherwise sensitive fields are redacted
|
|
17
|
+
ASAP_DEBUG_LOG: Set to "true" or "1" to log full request/response bodies (structured JSON); for development
|
|
16
18
|
|
|
17
19
|
Example:
|
|
18
20
|
>>> from asap.observability.logging import get_logger, configure_logging
|
|
@@ -42,11 +44,90 @@ DEFAULT_SERVICE_NAME = "asap-protocol"
|
|
|
42
44
|
ENV_LOG_FORMAT = "ASAP_LOG_FORMAT"
|
|
43
45
|
ENV_LOG_LEVEL = "ASAP_LOG_LEVEL"
|
|
44
46
|
ENV_SERVICE_NAME = "ASAP_SERVICE_NAME"
|
|
47
|
+
ENV_DEBUG = "ASAP_DEBUG"
|
|
48
|
+
ENV_DEBUG_LOG = "ASAP_DEBUG_LOG"
|
|
49
|
+
|
|
50
|
+
# Placeholder for redacted sensitive values in logs
|
|
51
|
+
REDACTED_PLACEHOLDER = "***REDACTED***"
|
|
52
|
+
|
|
53
|
+
# Key substrings (case-insensitive) that indicate sensitive data to redact
|
|
54
|
+
_SENSITIVE_KEY_PATTERNS = frozenset(
|
|
55
|
+
{
|
|
56
|
+
"password",
|
|
57
|
+
"token",
|
|
58
|
+
"secret",
|
|
59
|
+
"key",
|
|
60
|
+
"authorization",
|
|
61
|
+
"auth",
|
|
62
|
+
"credential",
|
|
63
|
+
"api_key",
|
|
64
|
+
"apikey",
|
|
65
|
+
"access_token",
|
|
66
|
+
"refresh_token",
|
|
67
|
+
}
|
|
68
|
+
)
|
|
45
69
|
|
|
46
70
|
# Module-level flag to track if logging has been configured
|
|
47
71
|
_logging_configured = False
|
|
48
72
|
|
|
49
73
|
|
|
74
|
+
def _is_sensitive_key(key: str) -> bool:
|
|
75
|
+
"""Return True if the key name indicates sensitive data that should be redacted."""
|
|
76
|
+
lower = key.lower()
|
|
77
|
+
return any(pattern in lower for pattern in _SENSITIVE_KEY_PATTERNS)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def sanitize_for_logging(data: dict[str, Any]) -> dict[str, Any]:
|
|
81
|
+
"""Sanitize a dict for safe logging by redacting sensitive field values.
|
|
82
|
+
|
|
83
|
+
Keys matching (case-insensitive) password, token, secret, key, authorization,
|
|
84
|
+
auth, credential, api_key, apikey, access_token, refresh_token have their
|
|
85
|
+
values replaced with REDACTED_PLACEHOLDER. Handles nested
|
|
86
|
+
dicts and lists of dicts recursively. Non-sensitive keys and correlation_id
|
|
87
|
+
are preserved for debugging.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
data: The dict to sanitize (e.g. envelope payload or request params).
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
A new dict with sensitive values replaced; nested structures are deep-copied
|
|
94
|
+
and sanitized. Non-dict/non-list values for sensitive keys are redacted.
|
|
95
|
+
|
|
96
|
+
Example:
|
|
97
|
+
>>> sanitize_for_logging({"user": "alice", "password": "secret123"})
|
|
98
|
+
{'user': 'alice', 'password': '***REDACTED***'}
|
|
99
|
+
>>> sanitize_for_logging({"nested": {"token": "sk_live_abc"}})
|
|
100
|
+
{'nested': {'token': '***REDACTED***'}}
|
|
101
|
+
"""
|
|
102
|
+
if not data:
|
|
103
|
+
return {}
|
|
104
|
+
result: dict[str, Any] = {}
|
|
105
|
+
for k, v in data.items():
|
|
106
|
+
if _is_sensitive_key(k):
|
|
107
|
+
result[k] = REDACTED_PLACEHOLDER
|
|
108
|
+
elif isinstance(v, dict):
|
|
109
|
+
result[k] = sanitize_for_logging(v)
|
|
110
|
+
elif isinstance(v, list):
|
|
111
|
+
result[k] = [
|
|
112
|
+
sanitize_for_logging(item) if isinstance(item, dict) else item for item in v
|
|
113
|
+
]
|
|
114
|
+
else:
|
|
115
|
+
result[k] = v
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def is_debug_mode() -> bool:
|
|
120
|
+
"""Return True if ASAP_DEBUG is set to a truthy value (e.g. true, 1)."""
|
|
121
|
+
value = os.environ.get(ENV_DEBUG, "").strip().lower()
|
|
122
|
+
return value in ("true", "1", "yes", "on")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def is_debug_log_mode() -> bool:
|
|
126
|
+
"""Return True if ASAP_DEBUG_LOG is set to a truthy value (log full request/response)."""
|
|
127
|
+
value = os.environ.get(ENV_DEBUG_LOG, "").strip().lower()
|
|
128
|
+
return value in ("true", "1", "yes", "on")
|
|
129
|
+
|
|
130
|
+
|
|
50
131
|
def _get_log_level() -> str:
|
|
51
132
|
"""Get log level from environment or use default."""
|
|
52
133
|
return os.environ.get(ENV_LOG_LEVEL, DEFAULT_LOG_LEVEL).upper()
|
|
@@ -185,7 +266,6 @@ def get_logger(name: str) -> structlog.stdlib.BoundLogger:
|
|
|
185
266
|
>>> logger = logger.bind(trace_id="trace_123")
|
|
186
267
|
>>> logger.info("request.processed") # trace_id automatically included
|
|
187
268
|
"""
|
|
188
|
-
# Ensure logging is configured
|
|
189
269
|
if not _logging_configured:
|
|
190
270
|
configure_logging()
|
|
191
271
|
|
asap/observability/metrics.py
CHANGED
|
@@ -150,16 +150,30 @@ class MetricsCollector:
|
|
|
150
150
|
>>> print(collector.export_prometheus())
|
|
151
151
|
"""
|
|
152
152
|
|
|
153
|
-
# Default metric definitions
|
|
153
|
+
# Default metric definitions (20+ for observability)
|
|
154
154
|
DEFAULT_COUNTERS: ClassVar[dict[str, str]] = {
|
|
155
155
|
"asap_requests_total": "Total number of ASAP requests received",
|
|
156
156
|
"asap_requests_success_total": "Total number of successful ASAP requests",
|
|
157
157
|
"asap_requests_error_total": "Total number of failed ASAP requests",
|
|
158
158
|
"asap_thread_pool_exhausted_total": "Total number of thread pool exhaustion events",
|
|
159
|
+
"asap_handler_executions_total": "Total number of handler executions",
|
|
160
|
+
"asap_handler_errors_total": "Total number of handler execution failures",
|
|
161
|
+
"asap_state_transitions_total": "Total number of state machine transitions",
|
|
162
|
+
"asap_transport_send_total": "Total number of transport send attempts",
|
|
163
|
+
"asap_transport_send_errors_total": "Total number of transport send errors",
|
|
164
|
+
"asap_transport_retries_total": "Total number of transport retries",
|
|
165
|
+
"asap_parse_errors_total": "Total number of JSON-RPC parse errors",
|
|
166
|
+
"asap_auth_failures_total": "Total number of authentication failures",
|
|
167
|
+
"asap_validation_errors_total": "Total number of envelope validation errors",
|
|
168
|
+
"asap_invalid_timestamp_total": "Total number of invalid timestamp rejections",
|
|
169
|
+
"asap_invalid_nonce_total": "Total number of invalid nonce rejections",
|
|
170
|
+
"asap_sender_mismatch_total": "Total number of sender identity mismatches",
|
|
159
171
|
}
|
|
160
172
|
|
|
161
173
|
DEFAULT_HISTOGRAMS: ClassVar[dict[str, str]] = {
|
|
162
174
|
"asap_request_duration_seconds": "Request processing duration in seconds",
|
|
175
|
+
"asap_handler_duration_seconds": "Handler execution duration in seconds",
|
|
176
|
+
"asap_transport_send_duration_seconds": "Transport send duration in seconds",
|
|
163
177
|
}
|
|
164
178
|
|
|
165
179
|
def __init__(self) -> None:
|