fraiseql-confiture 0.3.4__cp311-cp311-win_amd64.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.
- confiture/__init__.py +48 -0
- confiture/_core.cp311-win_amd64.pyd +0 -0
- confiture/cli/__init__.py +0 -0
- confiture/cli/dry_run.py +116 -0
- confiture/cli/lint_formatter.py +193 -0
- confiture/cli/main.py +1656 -0
- confiture/config/__init__.py +0 -0
- confiture/config/environment.py +263 -0
- confiture/core/__init__.py +51 -0
- confiture/core/anonymization/__init__.py +0 -0
- confiture/core/anonymization/audit.py +485 -0
- confiture/core/anonymization/benchmarking.py +372 -0
- confiture/core/anonymization/breach_notification.py +652 -0
- confiture/core/anonymization/compliance.py +617 -0
- confiture/core/anonymization/composer.py +298 -0
- confiture/core/anonymization/data_subject_rights.py +669 -0
- confiture/core/anonymization/factory.py +319 -0
- confiture/core/anonymization/governance.py +737 -0
- confiture/core/anonymization/performance.py +1092 -0
- confiture/core/anonymization/profile.py +284 -0
- confiture/core/anonymization/registry.py +195 -0
- confiture/core/anonymization/security/kms_manager.py +547 -0
- confiture/core/anonymization/security/lineage.py +888 -0
- confiture/core/anonymization/security/token_store.py +686 -0
- confiture/core/anonymization/strategies/__init__.py +41 -0
- confiture/core/anonymization/strategies/address.py +359 -0
- confiture/core/anonymization/strategies/credit_card.py +374 -0
- confiture/core/anonymization/strategies/custom.py +161 -0
- confiture/core/anonymization/strategies/date.py +218 -0
- confiture/core/anonymization/strategies/differential_privacy.py +398 -0
- confiture/core/anonymization/strategies/email.py +141 -0
- confiture/core/anonymization/strategies/format_preserving_encryption.py +310 -0
- confiture/core/anonymization/strategies/hash.py +150 -0
- confiture/core/anonymization/strategies/ip_address.py +235 -0
- confiture/core/anonymization/strategies/masking_retention.py +252 -0
- confiture/core/anonymization/strategies/name.py +298 -0
- confiture/core/anonymization/strategies/phone.py +119 -0
- confiture/core/anonymization/strategies/preserve.py +85 -0
- confiture/core/anonymization/strategies/redact.py +101 -0
- confiture/core/anonymization/strategies/salted_hashing.py +322 -0
- confiture/core/anonymization/strategies/text_redaction.py +183 -0
- confiture/core/anonymization/strategies/tokenization.py +334 -0
- confiture/core/anonymization/strategy.py +241 -0
- confiture/core/anonymization/syncer_audit.py +357 -0
- confiture/core/blue_green.py +683 -0
- confiture/core/builder.py +500 -0
- confiture/core/checksum.py +358 -0
- confiture/core/connection.py +132 -0
- confiture/core/differ.py +522 -0
- confiture/core/drift.py +564 -0
- confiture/core/dry_run.py +182 -0
- confiture/core/health.py +313 -0
- confiture/core/hooks/__init__.py +87 -0
- confiture/core/hooks/base.py +232 -0
- confiture/core/hooks/context.py +146 -0
- confiture/core/hooks/execution_strategies.py +57 -0
- confiture/core/hooks/observability.py +220 -0
- confiture/core/hooks/phases.py +53 -0
- confiture/core/hooks/registry.py +295 -0
- confiture/core/large_tables.py +775 -0
- confiture/core/linting/__init__.py +70 -0
- confiture/core/linting/composer.py +192 -0
- confiture/core/linting/libraries/__init__.py +17 -0
- confiture/core/linting/libraries/gdpr.py +168 -0
- confiture/core/linting/libraries/general.py +184 -0
- confiture/core/linting/libraries/hipaa.py +144 -0
- confiture/core/linting/libraries/pci_dss.py +104 -0
- confiture/core/linting/libraries/sox.py +120 -0
- confiture/core/linting/schema_linter.py +491 -0
- confiture/core/linting/versioning.py +151 -0
- confiture/core/locking.py +389 -0
- confiture/core/migration_generator.py +298 -0
- confiture/core/migrator.py +793 -0
- confiture/core/observability/__init__.py +44 -0
- confiture/core/observability/audit.py +323 -0
- confiture/core/observability/logging.py +187 -0
- confiture/core/observability/metrics.py +174 -0
- confiture/core/observability/tracing.py +192 -0
- confiture/core/pg_version.py +418 -0
- confiture/core/pool.py +406 -0
- confiture/core/risk/__init__.py +39 -0
- confiture/core/risk/predictor.py +188 -0
- confiture/core/risk/scoring.py +248 -0
- confiture/core/rollback_generator.py +388 -0
- confiture/core/schema_analyzer.py +769 -0
- confiture/core/schema_to_schema.py +590 -0
- confiture/core/security/__init__.py +32 -0
- confiture/core/security/logging.py +201 -0
- confiture/core/security/validation.py +416 -0
- confiture/core/signals.py +371 -0
- confiture/core/syncer.py +540 -0
- confiture/exceptions.py +192 -0
- confiture/integrations/__init__.py +0 -0
- confiture/models/__init__.py +0 -0
- confiture/models/lint.py +193 -0
- confiture/models/migration.py +180 -0
- confiture/models/schema.py +203 -0
- confiture/scenarios/__init__.py +36 -0
- confiture/scenarios/compliance.py +586 -0
- confiture/scenarios/ecommerce.py +199 -0
- confiture/scenarios/financial.py +253 -0
- confiture/scenarios/healthcare.py +315 -0
- confiture/scenarios/multi_tenant.py +340 -0
- confiture/scenarios/saas.py +295 -0
- confiture/testing/FRAMEWORK_API.md +722 -0
- confiture/testing/__init__.py +38 -0
- confiture/testing/fixtures/__init__.py +11 -0
- confiture/testing/fixtures/data_validator.py +229 -0
- confiture/testing/fixtures/migration_runner.py +167 -0
- confiture/testing/fixtures/schema_snapshotter.py +352 -0
- confiture/testing/frameworks/__init__.py +10 -0
- confiture/testing/frameworks/mutation.py +587 -0
- confiture/testing/frameworks/performance.py +479 -0
- confiture/testing/utils/__init__.py +0 -0
- fraiseql_confiture-0.3.4.dist-info/METADATA +438 -0
- fraiseql_confiture-0.3.4.dist-info/RECORD +119 -0
- fraiseql_confiture-0.3.4.dist-info/WHEEL +4 -0
- fraiseql_confiture-0.3.4.dist-info/entry_points.txt +2 -0
- fraiseql_confiture-0.3.4.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Observability module for Confiture.
|
|
2
|
+
|
|
3
|
+
Provides optional integrations with:
|
|
4
|
+
- OpenTelemetry (tracing)
|
|
5
|
+
- Prometheus (metrics)
|
|
6
|
+
- Structured logging
|
|
7
|
+
- Audit trail
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from confiture.core.observability.audit import (
|
|
11
|
+
AuditConfig,
|
|
12
|
+
AuditEntry,
|
|
13
|
+
AuditTrail,
|
|
14
|
+
)
|
|
15
|
+
from confiture.core.observability.logging import (
|
|
16
|
+
LoggingConfig,
|
|
17
|
+
StructuredLogger,
|
|
18
|
+
configure_logging,
|
|
19
|
+
)
|
|
20
|
+
from confiture.core.observability.metrics import (
|
|
21
|
+
MetricsConfig,
|
|
22
|
+
MigrationMetrics,
|
|
23
|
+
create_metrics,
|
|
24
|
+
)
|
|
25
|
+
from confiture.core.observability.tracing import (
|
|
26
|
+
MigrationTracer,
|
|
27
|
+
TracingConfig,
|
|
28
|
+
create_tracer,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"TracingConfig",
|
|
33
|
+
"MigrationTracer",
|
|
34
|
+
"create_tracer",
|
|
35
|
+
"MetricsConfig",
|
|
36
|
+
"MigrationMetrics",
|
|
37
|
+
"create_metrics",
|
|
38
|
+
"LoggingConfig",
|
|
39
|
+
"StructuredLogger",
|
|
40
|
+
"configure_logging",
|
|
41
|
+
"AuditConfig",
|
|
42
|
+
"AuditTrail",
|
|
43
|
+
"AuditEntry",
|
|
44
|
+
]
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Audit trail for migration compliance.
|
|
2
|
+
|
|
3
|
+
Tracks who ran migrations, when, and the outcome for compliance purposes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import getpass
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import socket
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import UTC, datetime
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import psycopg
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class AuditConfig:
|
|
22
|
+
"""Configuration for audit trail."""
|
|
23
|
+
|
|
24
|
+
enabled: bool = True
|
|
25
|
+
table_name: str = "confiture_audit_log"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class AuditEntry:
|
|
30
|
+
"""A single audit log entry."""
|
|
31
|
+
|
|
32
|
+
migration_version: str
|
|
33
|
+
migration_name: str
|
|
34
|
+
action: str # "apply", "rollback", "dry_run"
|
|
35
|
+
status: str # "started", "completed", "failed"
|
|
36
|
+
user: str
|
|
37
|
+
hostname: str
|
|
38
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
39
|
+
duration_ms: int | None = None
|
|
40
|
+
error_message: str | None = None
|
|
41
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> dict[str, Any]:
|
|
44
|
+
"""Convert to dictionary for JSON serialization."""
|
|
45
|
+
return {
|
|
46
|
+
"migration_version": self.migration_version,
|
|
47
|
+
"migration_name": self.migration_name,
|
|
48
|
+
"action": self.action,
|
|
49
|
+
"status": self.status,
|
|
50
|
+
"user": self.user,
|
|
51
|
+
"hostname": self.hostname,
|
|
52
|
+
"timestamp": self.timestamp.isoformat(),
|
|
53
|
+
"duration_ms": self.duration_ms,
|
|
54
|
+
"error_message": self.error_message,
|
|
55
|
+
"metadata": self.metadata,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AuditTrail:
|
|
60
|
+
"""Records audit trail for compliance.
|
|
61
|
+
|
|
62
|
+
Tracks who ran migrations, when, from which machine, and whether
|
|
63
|
+
they succeeded or failed.
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
>>> audit = AuditTrail(conn, AuditConfig(enabled=True))
|
|
67
|
+
>>> audit.initialize()
|
|
68
|
+
>>> entry_id = audit.record_start("001", "create_users", "apply")
|
|
69
|
+
>>> # ... run migration ...
|
|
70
|
+
>>> audit.record_complete(entry_id, duration_ms=1500)
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
connection: psycopg.Connection,
|
|
76
|
+
config: AuditConfig | None = None,
|
|
77
|
+
):
|
|
78
|
+
"""Initialize audit trail.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
connection: Database connection
|
|
82
|
+
config: Audit configuration (optional)
|
|
83
|
+
"""
|
|
84
|
+
self.connection = connection
|
|
85
|
+
self.config = config or AuditConfig()
|
|
86
|
+
self._initialized = False
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def is_enabled(self) -> bool:
|
|
90
|
+
"""Check if audit trail is enabled."""
|
|
91
|
+
return self.config.enabled
|
|
92
|
+
|
|
93
|
+
def initialize(self) -> None:
|
|
94
|
+
"""Create audit log table if it doesn't exist."""
|
|
95
|
+
if not self.config.enabled:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
with self.connection.cursor() as cur:
|
|
99
|
+
cur.execute(f"""
|
|
100
|
+
CREATE TABLE IF NOT EXISTS {self.config.table_name} (
|
|
101
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
102
|
+
migration_version VARCHAR(255) NOT NULL,
|
|
103
|
+
migration_name VARCHAR(255) NOT NULL,
|
|
104
|
+
action VARCHAR(50) NOT NULL,
|
|
105
|
+
status VARCHAR(50) NOT NULL,
|
|
106
|
+
username VARCHAR(255) NOT NULL,
|
|
107
|
+
hostname VARCHAR(255) NOT NULL,
|
|
108
|
+
started_at TIMESTAMPTZ NOT NULL,
|
|
109
|
+
completed_at TIMESTAMPTZ,
|
|
110
|
+
duration_ms INTEGER,
|
|
111
|
+
error_message TEXT,
|
|
112
|
+
metadata JSONB DEFAULT '{{}}'::jsonb
|
|
113
|
+
)
|
|
114
|
+
""")
|
|
115
|
+
|
|
116
|
+
cur.execute(f"""
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_{self.config.table_name}_version
|
|
118
|
+
ON {self.config.table_name}(migration_version)
|
|
119
|
+
""")
|
|
120
|
+
|
|
121
|
+
cur.execute(f"""
|
|
122
|
+
CREATE INDEX IF NOT EXISTS idx_{self.config.table_name}_started
|
|
123
|
+
ON {self.config.table_name}(started_at DESC)
|
|
124
|
+
""")
|
|
125
|
+
|
|
126
|
+
cur.execute(f"""
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_{self.config.table_name}_action
|
|
128
|
+
ON {self.config.table_name}(action)
|
|
129
|
+
""")
|
|
130
|
+
|
|
131
|
+
self.connection.commit()
|
|
132
|
+
self._initialized = True
|
|
133
|
+
logger.info("Audit trail initialized")
|
|
134
|
+
|
|
135
|
+
def record_start(
|
|
136
|
+
self,
|
|
137
|
+
migration_version: str,
|
|
138
|
+
migration_name: str,
|
|
139
|
+
action: str,
|
|
140
|
+
metadata: dict[str, Any] | None = None,
|
|
141
|
+
) -> int:
|
|
142
|
+
"""Record migration start.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
migration_version: Migration version
|
|
146
|
+
migration_name: Migration name
|
|
147
|
+
action: Action type ("apply", "rollback", "dry_run")
|
|
148
|
+
metadata: Additional metadata (optional)
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Audit entry ID for later completion
|
|
152
|
+
"""
|
|
153
|
+
if not self.config.enabled:
|
|
154
|
+
return -1
|
|
155
|
+
|
|
156
|
+
entry = AuditEntry(
|
|
157
|
+
migration_version=migration_version,
|
|
158
|
+
migration_name=migration_name,
|
|
159
|
+
action=action,
|
|
160
|
+
status="started",
|
|
161
|
+
user=self._get_current_user(),
|
|
162
|
+
hostname=self._get_hostname(),
|
|
163
|
+
metadata=metadata or {},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
with self.connection.cursor() as cur:
|
|
167
|
+
cur.execute(
|
|
168
|
+
f"""
|
|
169
|
+
INSERT INTO {self.config.table_name}
|
|
170
|
+
(migration_version, migration_name, action, status,
|
|
171
|
+
username, hostname, started_at, metadata)
|
|
172
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
|
173
|
+
RETURNING id
|
|
174
|
+
""",
|
|
175
|
+
(
|
|
176
|
+
entry.migration_version,
|
|
177
|
+
entry.migration_name,
|
|
178
|
+
entry.action,
|
|
179
|
+
entry.status,
|
|
180
|
+
entry.user,
|
|
181
|
+
entry.hostname,
|
|
182
|
+
entry.timestamp,
|
|
183
|
+
json.dumps(entry.metadata),
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
result = cur.fetchone()
|
|
187
|
+
entry_id = result[0] if result else -1
|
|
188
|
+
|
|
189
|
+
self.connection.commit()
|
|
190
|
+
return entry_id
|
|
191
|
+
|
|
192
|
+
def record_complete(
|
|
193
|
+
self,
|
|
194
|
+
entry_id: int,
|
|
195
|
+
duration_ms: int,
|
|
196
|
+
error_message: str | None = None,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Record migration completion.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
entry_id: Audit entry ID from record_start
|
|
202
|
+
duration_ms: Duration in milliseconds
|
|
203
|
+
error_message: Error message if failed (optional)
|
|
204
|
+
"""
|
|
205
|
+
if not self.config.enabled or entry_id < 0:
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
status = "failed" if error_message else "completed"
|
|
209
|
+
|
|
210
|
+
with self.connection.cursor() as cur:
|
|
211
|
+
cur.execute(
|
|
212
|
+
f"""
|
|
213
|
+
UPDATE {self.config.table_name}
|
|
214
|
+
SET status = %s,
|
|
215
|
+
completed_at = %s,
|
|
216
|
+
duration_ms = %s,
|
|
217
|
+
error_message = %s
|
|
218
|
+
WHERE id = %s
|
|
219
|
+
""",
|
|
220
|
+
(
|
|
221
|
+
status,
|
|
222
|
+
datetime.now(UTC),
|
|
223
|
+
duration_ms,
|
|
224
|
+
error_message,
|
|
225
|
+
entry_id,
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
self.connection.commit()
|
|
230
|
+
|
|
231
|
+
def get_history(
|
|
232
|
+
self,
|
|
233
|
+
migration_version: str | None = None,
|
|
234
|
+
action: str | None = None,
|
|
235
|
+
limit: int = 100,
|
|
236
|
+
) -> list[dict[str, Any]]:
|
|
237
|
+
"""Get audit history.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
migration_version: Filter by version (optional)
|
|
241
|
+
action: Filter by action (optional)
|
|
242
|
+
limit: Maximum number of entries
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
List of audit entries as dictionaries
|
|
246
|
+
"""
|
|
247
|
+
if not self.config.enabled:
|
|
248
|
+
return []
|
|
249
|
+
|
|
250
|
+
conditions = []
|
|
251
|
+
params: list[Any] = []
|
|
252
|
+
|
|
253
|
+
if migration_version:
|
|
254
|
+
conditions.append("migration_version = %s")
|
|
255
|
+
params.append(migration_version)
|
|
256
|
+
if action:
|
|
257
|
+
conditions.append("action = %s")
|
|
258
|
+
params.append(action)
|
|
259
|
+
|
|
260
|
+
where_clause = ""
|
|
261
|
+
if conditions:
|
|
262
|
+
where_clause = "WHERE " + " AND ".join(conditions)
|
|
263
|
+
|
|
264
|
+
params.append(limit)
|
|
265
|
+
|
|
266
|
+
with self.connection.cursor() as cur:
|
|
267
|
+
cur.execute(
|
|
268
|
+
f"""
|
|
269
|
+
SELECT id, migration_version, migration_name, action, status,
|
|
270
|
+
username, hostname, started_at, completed_at,
|
|
271
|
+
duration_ms, error_message, metadata
|
|
272
|
+
FROM {self.config.table_name}
|
|
273
|
+
{where_clause}
|
|
274
|
+
ORDER BY started_at DESC
|
|
275
|
+
LIMIT %s
|
|
276
|
+
""",
|
|
277
|
+
params,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
columns = [desc[0] for desc in cur.description]
|
|
281
|
+
return [dict(zip(columns, row, strict=True)) for row in cur.fetchall()]
|
|
282
|
+
|
|
283
|
+
def get_recent_failures(self, limit: int = 10) -> list[dict[str, Any]]:
|
|
284
|
+
"""Get recent failed migrations.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
limit: Maximum number of entries
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
List of failed audit entries
|
|
291
|
+
"""
|
|
292
|
+
if not self.config.enabled:
|
|
293
|
+
return []
|
|
294
|
+
|
|
295
|
+
with self.connection.cursor() as cur:
|
|
296
|
+
cur.execute(
|
|
297
|
+
f"""
|
|
298
|
+
SELECT id, migration_version, migration_name, action,
|
|
299
|
+
username, hostname, started_at, error_message
|
|
300
|
+
FROM {self.config.table_name}
|
|
301
|
+
WHERE status = 'failed'
|
|
302
|
+
ORDER BY started_at DESC
|
|
303
|
+
LIMIT %s
|
|
304
|
+
""",
|
|
305
|
+
(limit,),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
columns = [desc[0] for desc in cur.description]
|
|
309
|
+
return [dict(zip(columns, row, strict=True)) for row in cur.fetchall()]
|
|
310
|
+
|
|
311
|
+
def _get_current_user(self) -> str:
|
|
312
|
+
"""Get current username."""
|
|
313
|
+
try:
|
|
314
|
+
return getpass.getuser()
|
|
315
|
+
except Exception:
|
|
316
|
+
return os.environ.get("USER", "unknown")
|
|
317
|
+
|
|
318
|
+
def _get_hostname(self) -> str:
|
|
319
|
+
"""Get current hostname."""
|
|
320
|
+
try:
|
|
321
|
+
return socket.gethostname()
|
|
322
|
+
except Exception:
|
|
323
|
+
return "unknown"
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Structured JSON logging for migrations.
|
|
2
|
+
|
|
3
|
+
Provides structured logging with correlation IDs for migration operations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import sys
|
|
9
|
+
import uuid
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class LoggingConfig:
|
|
17
|
+
"""Configuration for structured logging."""
|
|
18
|
+
|
|
19
|
+
enabled: bool = True
|
|
20
|
+
format: str = "json" # "json" or "text"
|
|
21
|
+
level: str = "INFO"
|
|
22
|
+
include_timestamp: bool = True
|
|
23
|
+
include_correlation_id: bool = True
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StructuredFormatter(logging.Formatter):
|
|
27
|
+
"""JSON formatter for structured logging."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, config: LoggingConfig):
|
|
30
|
+
"""Initialize structured formatter.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
config: Logging configuration
|
|
34
|
+
"""
|
|
35
|
+
super().__init__()
|
|
36
|
+
self.config = config
|
|
37
|
+
|
|
38
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
39
|
+
"""Format log record as JSON.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
record: Log record to format
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
JSON-formatted log string
|
|
46
|
+
"""
|
|
47
|
+
log_data: dict[str, Any] = {
|
|
48
|
+
"level": record.levelname,
|
|
49
|
+
"message": record.getMessage(),
|
|
50
|
+
"logger": record.name,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if self.config.include_timestamp:
|
|
54
|
+
log_data["timestamp"] = datetime.now(UTC).isoformat()
|
|
55
|
+
|
|
56
|
+
if self.config.include_correlation_id:
|
|
57
|
+
log_data["correlation_id"] = getattr(record, "correlation_id", str(uuid.uuid4())[:8])
|
|
58
|
+
|
|
59
|
+
# Add extra fields from record
|
|
60
|
+
extra_fields = [
|
|
61
|
+
"migration_version",
|
|
62
|
+
"migration_name",
|
|
63
|
+
"duration_ms",
|
|
64
|
+
"rows_affected",
|
|
65
|
+
"operation",
|
|
66
|
+
"table_name",
|
|
67
|
+
"status",
|
|
68
|
+
]
|
|
69
|
+
for field in extra_fields:
|
|
70
|
+
if hasattr(record, field):
|
|
71
|
+
log_data[field] = getattr(record, field)
|
|
72
|
+
|
|
73
|
+
# Add exception info
|
|
74
|
+
if record.exc_info:
|
|
75
|
+
log_data["exception"] = self.formatException(record.exc_info)
|
|
76
|
+
|
|
77
|
+
return json.dumps(log_data)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class StructuredLogger:
|
|
81
|
+
"""Logger with structured output and correlation IDs.
|
|
82
|
+
|
|
83
|
+
Provides a logging interface that supports correlation IDs
|
|
84
|
+
and structured fields for migration operations.
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
>>> logger = StructuredLogger("confiture.migration")
|
|
88
|
+
>>> logger.set_correlation_id("abc123")
|
|
89
|
+
>>> logger.info("Starting migration", migration_version="001")
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, name: str, config: LoggingConfig | None = None):
|
|
93
|
+
"""Initialize structured logger.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
name: Logger name
|
|
97
|
+
config: Logging configuration (optional)
|
|
98
|
+
"""
|
|
99
|
+
self.config = config or LoggingConfig()
|
|
100
|
+
self._logger = logging.getLogger(name)
|
|
101
|
+
self._correlation_id: str | None = None
|
|
102
|
+
|
|
103
|
+
def _log(self, level: int, message: str, **kwargs: Any) -> None:
|
|
104
|
+
"""Log with extra fields.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
level: Log level
|
|
108
|
+
message: Log message
|
|
109
|
+
**kwargs: Extra fields to include
|
|
110
|
+
"""
|
|
111
|
+
extra = {"correlation_id": self._correlation_id or str(uuid.uuid4())[:8]}
|
|
112
|
+
extra.update(kwargs)
|
|
113
|
+
self._logger.log(level, message, extra=extra)
|
|
114
|
+
|
|
115
|
+
def debug(self, message: str, **kwargs: Any) -> None:
|
|
116
|
+
"""Log debug message."""
|
|
117
|
+
self._log(logging.DEBUG, message, **kwargs)
|
|
118
|
+
|
|
119
|
+
def info(self, message: str, **kwargs: Any) -> None:
|
|
120
|
+
"""Log info message."""
|
|
121
|
+
self._log(logging.INFO, message, **kwargs)
|
|
122
|
+
|
|
123
|
+
def warning(self, message: str, **kwargs: Any) -> None:
|
|
124
|
+
"""Log warning message."""
|
|
125
|
+
self._log(logging.WARNING, message, **kwargs)
|
|
126
|
+
|
|
127
|
+
def error(self, message: str, **kwargs: Any) -> None:
|
|
128
|
+
"""Log error message."""
|
|
129
|
+
self._log(logging.ERROR, message, **kwargs)
|
|
130
|
+
|
|
131
|
+
def exception(self, message: str, **kwargs: Any) -> None:
|
|
132
|
+
"""Log exception with traceback."""
|
|
133
|
+
extra = {"correlation_id": self._correlation_id or str(uuid.uuid4())[:8]}
|
|
134
|
+
extra.update(kwargs)
|
|
135
|
+
self._logger.exception(message, extra=extra)
|
|
136
|
+
|
|
137
|
+
def set_correlation_id(self, correlation_id: str) -> None:
|
|
138
|
+
"""Set correlation ID for subsequent logs.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
correlation_id: Correlation ID to use
|
|
142
|
+
"""
|
|
143
|
+
self._correlation_id = correlation_id
|
|
144
|
+
|
|
145
|
+
def clear_correlation_id(self) -> None:
|
|
146
|
+
"""Clear the correlation ID."""
|
|
147
|
+
self._correlation_id = None
|
|
148
|
+
|
|
149
|
+
def new_correlation_id(self) -> str:
|
|
150
|
+
"""Generate and set a new correlation ID.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
The new correlation ID
|
|
154
|
+
"""
|
|
155
|
+
self._correlation_id = str(uuid.uuid4())[:8]
|
|
156
|
+
return self._correlation_id
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def configure_logging(config: LoggingConfig | None = None) -> None:
|
|
160
|
+
"""Configure structured logging for Confiture.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
config: Logging configuration (optional)
|
|
164
|
+
"""
|
|
165
|
+
config = config or LoggingConfig()
|
|
166
|
+
|
|
167
|
+
# Get root logger for confiture
|
|
168
|
+
logger = logging.getLogger("confiture")
|
|
169
|
+
logger.setLevel(getattr(logging, config.level))
|
|
170
|
+
|
|
171
|
+
# Remove existing handlers
|
|
172
|
+
logger.handlers.clear()
|
|
173
|
+
|
|
174
|
+
# Add handler with appropriate formatter
|
|
175
|
+
handler = logging.StreamHandler(sys.stderr)
|
|
176
|
+
|
|
177
|
+
if config.format == "json":
|
|
178
|
+
handler.setFormatter(StructuredFormatter(config))
|
|
179
|
+
else:
|
|
180
|
+
handler.setFormatter(
|
|
181
|
+
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
logger.addHandler(handler)
|
|
185
|
+
|
|
186
|
+
# Prevent propagation to root logger
|
|
187
|
+
logger.propagate = False
|