-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathapi_outgoing_log.py
More file actions
220 lines (191 loc) · 6.51 KB
/
api_outgoing_log.py
File metadata and controls
220 lines (191 loc) · 6.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
"""Audit log for outgoing API calls to external services."""
import logging
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
class ApiOutgoingLog(models.Model):
"""
Audit log for outgoing HTTP calls to external services.
Captures all outgoing API requests (DCI, webhooks, etc.) with
request/response details for troubleshooting and compliance.
"""
_name = "spp.api.outgoing.log"
_description = "Outgoing API Log"
_order = "timestamp desc"
_rec_name = "display_name"
# ==========================================
# Request - WHAT was sent
# ==========================================
url = fields.Char(
required=True,
index=True,
help="Full URL called",
)
endpoint = fields.Char(
index=True,
help="Path portion of the URL, e.g. /registry/sync/search",
)
http_method = fields.Selection(
[
("POST", "POST"),
("GET", "GET"),
("PUT", "PUT"),
("PATCH", "PATCH"),
("DELETE", "DELETE"),
],
default="POST",
required=True,
)
request_summary = fields.Json(
groups="spp_api_v2.group_api_v2_auditor",
help="Request payload (secrets redacted)",
)
# ==========================================
# Response - WHAT came back
# ==========================================
response_summary = fields.Json(
groups="spp_api_v2.group_api_v2_auditor",
help="Response payload",
)
response_status_code = fields.Integer(
index=True,
)
# ==========================================
# Context - WHO triggered it
# ==========================================
user_id = fields.Many2one(
"res.users",
default=lambda self: self.env.uid,
index=True,
)
origin_model = fields.Char(
index=True,
help="Model that triggered the call, e.g. spp.dci.data.source",
)
origin_record_id = fields.Integer(
help="Record ID that triggered the call",
)
# ==========================================
# Timestamps & Performance
# ==========================================
timestamp = fields.Datetime(
required=True,
default=fields.Datetime.now,
index=True,
)
duration_ms = fields.Integer(
help="Request duration in milliseconds",
)
# ==========================================
# Service Context
# ==========================================
service_name = fields.Char(
index=True,
help="Human-readable service name, e.g. DCI Client",
)
service_code = fields.Char(
index=True,
help="Machine-readable service code, e.g. crvs_main",
)
# ==========================================
# Result
# ==========================================
status = fields.Selection(
[
("success", "Success"),
("http_error", "HTTP Error"),
("connection_error", "Connection Error"),
("timeout", "Timeout"),
("error", "Error"),
],
default="success",
required=True,
index=True,
)
error_detail = fields.Text(
groups="spp_api_v2.group_api_v2_auditor",
help="Error message or traceback",
)
# ==========================================
# Computed fields
# ==========================================
display_name = fields.Char(
compute="_compute_display_name",
store=True,
)
@api.depends("http_method", "endpoint", "timestamp")
def _compute_display_name(self):
for record in self:
timestamp_str = record.timestamp.strftime("%Y-%m-%d %H:%M") if record.timestamp else ""
record.display_name = f"{record.http_method} {record.endpoint or record.url} @ {timestamp_str}"
# ==========================================
# API Methods
# ==========================================
@api.model
def log_call(
self,
url: str,
http_method: str = "POST",
endpoint: str = None,
request_summary: dict = None,
response_summary: dict = None,
response_status_code: int = None,
user_id: int = None,
origin_model: str = None,
origin_record_id: int = None,
duration_ms: int = None,
service_name: str = None,
service_code: str = None,
status: str = "success",
error_detail: str = None,
):
"""
Log an outgoing API call.
Args:
url: Full URL called
http_method: HTTP method (POST, GET, PUT, PATCH, DELETE)
endpoint: Path portion of the URL
request_summary: Request payload (secrets redacted)
response_summary: Response payload
response_status_code: HTTP response status code
user_id: User who triggered the call
origin_model: Odoo model that triggered the call
origin_record_id: Record ID that triggered the call
duration_ms: Request duration in milliseconds
service_name: Human-readable service name
service_code: Machine-readable service code
status: Result status
error_detail: Error message or traceback
Returns:
Created spp.api.outgoing.log record
"""
vals = {
"url": url,
"http_method": http_method,
"status": status,
"timestamp": fields.Datetime.now(),
}
# Optional fields
if endpoint:
vals["endpoint"] = endpoint
if request_summary is not None:
vals["request_summary"] = request_summary
if response_summary is not None:
vals["response_summary"] = response_summary
if response_status_code is not None:
vals["response_status_code"] = response_status_code
if user_id is not None:
vals["user_id"] = user_id
if origin_model:
vals["origin_model"] = origin_model
if origin_record_id is not None:
vals["origin_record_id"] = origin_record_id
if duration_ms is not None:
vals["duration_ms"] = duration_ms
if service_name:
vals["service_name"] = service_name
if service_code:
vals["service_code"] = service_code
if error_detail:
vals["error_detail"] = error_detail
return self.create(vals)