agent-first-data 0.2.0__tar.gz → 0.2.2__tar.gz
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.
- {agent_first_data-0.2.0 → agent_first_data-0.2.2}/PKG-INFO +128 -2
- {agent_first_data-0.2.0 → agent_first_data-0.2.2}/README.md +127 -1
- {agent_first_data-0.2.0 → agent_first_data-0.2.2}/agent_first_data/__init__.py +17 -0
- agent_first_data-0.2.2/agent_first_data/afd_logging.py +161 -0
- {agent_first_data-0.2.0 → agent_first_data-0.2.2}/agent_first_data.egg-info/PKG-INFO +128 -2
- {agent_first_data-0.2.0 → agent_first_data-0.2.2}/agent_first_data.egg-info/SOURCES.txt +2 -0
- {agent_first_data-0.2.0 → agent_first_data-0.2.2}/pyproject.toml +1 -1
- agent_first_data-0.2.2/tests/test_afd_logging.py +173 -0
- {agent_first_data-0.2.0 → agent_first_data-0.2.2}/agent_first_data/format.py +0 -0
- {agent_first_data-0.2.0 → agent_first_data-0.2.2}/agent_first_data.egg-info/dependency_links.txt +0 -0
- {agent_first_data-0.2.0 → agent_first_data-0.2.2}/agent_first_data.egg-info/top_level.txt +0 -0
- {agent_first_data-0.2.0 → agent_first_data-0.2.2}/setup.cfg +0 -0
- {agent_first_data-0.2.0 → agent_first_data-0.2.2}/tests/test_format.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-first-data
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Agent-First Data (AFD) — suffix-driven output formatting and protocol templates for AI agents
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/cmnspore/agent-first-data
|
|
@@ -21,7 +21,7 @@ pip install agent-first-data
|
|
|
21
21
|
|
|
22
22
|
## API Reference
|
|
23
23
|
|
|
24
|
-
Total: **9 public APIs** (4 protocol builders + 3 output functions + 1 internal + 1 utility)
|
|
24
|
+
Total: **9 public APIs** + **AFD logging** (4 protocol builders + 3 output functions + 1 internal + 1 utility)
|
|
25
25
|
|
|
26
26
|
### Protocol Builders (returns dict)
|
|
27
27
|
|
|
@@ -256,6 +256,132 @@ print(output_plain(data))
|
|
|
256
256
|
# api_key=*** cache_ttl=3600s count=42 created_at=2025-02-07T00:00:00.000Z file_size=5.0MB payment=50000000msats price=$99.99 request_timeout=5.0s success_rate=95.5% user_name=alice
|
|
257
257
|
```
|
|
258
258
|
|
|
259
|
+
## AFD Logging
|
|
260
|
+
|
|
261
|
+
AFD-compliant structured logging via Python's `logging` module. Every log line is formatted using the library's own `output_json`/`output_plain`/`output_yaml` functions. Span fields are carried via `contextvars` (async-safe), automatically flattened into each log line.
|
|
262
|
+
|
|
263
|
+
### API
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
from agent_first_data import init_logging_json, init_logging_plain, init_logging_yaml
|
|
267
|
+
from agent_first_data.afd_logging import AfdHandler, get_logger, span
|
|
268
|
+
|
|
269
|
+
# Convenience initializers — set up the root logger with AFD output to stdout
|
|
270
|
+
init_logging_json(level="INFO") # Single-line JSONL (secrets redacted, original keys)
|
|
271
|
+
init_logging_plain(level="INFO") # Single-line logfmt (keys stripped, values formatted)
|
|
272
|
+
init_logging_yaml(level="INFO") # Multi-line YAML (keys stripped, values formatted)
|
|
273
|
+
|
|
274
|
+
# Low-level — create a handler for custom logger stacks
|
|
275
|
+
AfdHandler(format="json") # format: "json" | "plain" | "yaml"
|
|
276
|
+
|
|
277
|
+
# Logger with default fields (returns logging.LoggerAdapter)
|
|
278
|
+
get_logger(name, **fields)
|
|
279
|
+
|
|
280
|
+
# Span context manager — adds fields to all log events within the block
|
|
281
|
+
span(**fields)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Setup
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
from agent_first_data import init_logging_json, init_logging_plain, init_logging_yaml
|
|
288
|
+
|
|
289
|
+
# JSON output for production (one JSONL line per event, secrets redacted)
|
|
290
|
+
init_logging_json("INFO")
|
|
291
|
+
|
|
292
|
+
# Plain logfmt for development (keys stripped, values formatted)
|
|
293
|
+
init_logging_plain("DEBUG")
|
|
294
|
+
|
|
295
|
+
# YAML for detailed inspection (multi-line, keys stripped, values formatted)
|
|
296
|
+
init_logging_yaml("DEBUG")
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Log Output
|
|
300
|
+
|
|
301
|
+
Standard `logging` calls work unchanged. Output format depends on the init function used.
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
import logging
|
|
305
|
+
logger = logging.getLogger("myapp")
|
|
306
|
+
|
|
307
|
+
logger.info("Server started")
|
|
308
|
+
# JSON: {"timestamp_epoch_ms":1739000000000,"message":"Server started","target":"myapp","code":"info"}
|
|
309
|
+
# Plain: code=info message="Server started" target=myapp timestamp_epoch_ms=1739000000000
|
|
310
|
+
# YAML: ---
|
|
311
|
+
# code: "info"
|
|
312
|
+
# message: "Server started"
|
|
313
|
+
# target: "myapp"
|
|
314
|
+
# timestamp_epoch_ms: 1739000000000
|
|
315
|
+
|
|
316
|
+
logger.warning("DNS lookup failed")
|
|
317
|
+
# JSON: {"timestamp_epoch_ms":...,"message":"DNS lookup failed","target":"myapp","code":"warn"}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Span Support
|
|
321
|
+
|
|
322
|
+
Use the `span` context manager to add fields to all log events within the block. Spans nest and work with both sync and async code.
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
from agent_first_data import span
|
|
326
|
+
|
|
327
|
+
with span(request_id="abc-123"):
|
|
328
|
+
logger.info("Processing")
|
|
329
|
+
# {"timestamp_epoch_ms":...,"message":"Processing","target":"myapp","request_id":"abc-123","code":"info"}
|
|
330
|
+
|
|
331
|
+
with span(step="validate"):
|
|
332
|
+
logger.info("Validating input")
|
|
333
|
+
# {"timestamp_epoch_ms":...,"message":"Validating input","target":"myapp","request_id":"abc-123","step":"validate","code":"info"}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Logger with Default Fields
|
|
337
|
+
|
|
338
|
+
Use `get_logger` for per-component fields that appear on every log line:
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
from agent_first_data import get_logger
|
|
342
|
+
|
|
343
|
+
logger = get_logger("myapp.auth", component="auth")
|
|
344
|
+
logger.info("Token verified")
|
|
345
|
+
# {"timestamp_epoch_ms":...,"message":"Token verified","target":"myapp.auth","component":"auth","code":"info"}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Custom Code Override
|
|
349
|
+
|
|
350
|
+
The `code` field defaults to the log level. Override with an explicit field:
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
from agent_first_data import get_logger
|
|
354
|
+
|
|
355
|
+
logger = get_logger("myapp")
|
|
356
|
+
logger.info("Server ready", extra={"code": "startup"})
|
|
357
|
+
# {"timestamp_epoch_ms":...,"message":"Server ready","target":"myapp","code":"startup"}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Output Fields
|
|
361
|
+
|
|
362
|
+
Every log line contains:
|
|
363
|
+
|
|
364
|
+
| Field | Type | Description |
|
|
365
|
+
|:------|:-----|:------------|
|
|
366
|
+
| `timestamp_epoch_ms` | number | Unix milliseconds |
|
|
367
|
+
| `message` | string | Log message |
|
|
368
|
+
| `target` | string | Logger name |
|
|
369
|
+
| `code` | string | Level (debug/info/warn/error) or explicit override |
|
|
370
|
+
| *span fields* | any | From `span()` context manager |
|
|
371
|
+
| *event fields* | any | From `extra=` or `get_logger` fields |
|
|
372
|
+
|
|
373
|
+
### Log Output Formats
|
|
374
|
+
|
|
375
|
+
All three formats use the library's own output functions, so AFD suffix processing applies to log fields too:
|
|
376
|
+
|
|
377
|
+
| Format | Function | Keys | Values | Use case |
|
|
378
|
+
|:-------|:---------|:-----|:-------|:---------|
|
|
379
|
+
| **JSON** | `init_logging_json` | original (with suffix) | raw | production, log aggregation |
|
|
380
|
+
| **Plain** | `init_logging_plain` | stripped | formatted | development, compact scanning |
|
|
381
|
+
| **YAML** | `init_logging_yaml` | stripped | formatted | debugging, detailed inspection |
|
|
382
|
+
|
|
383
|
+
All formats automatically redact `_secret` fields in log output.
|
|
384
|
+
|
|
259
385
|
## Output Formats
|
|
260
386
|
|
|
261
387
|
Three output formats for different use cases:
|
|
@@ -12,7 +12,7 @@ pip install agent-first-data
|
|
|
12
12
|
|
|
13
13
|
## API Reference
|
|
14
14
|
|
|
15
|
-
Total: **9 public APIs** (4 protocol builders + 3 output functions + 1 internal + 1 utility)
|
|
15
|
+
Total: **9 public APIs** + **AFD logging** (4 protocol builders + 3 output functions + 1 internal + 1 utility)
|
|
16
16
|
|
|
17
17
|
### Protocol Builders (returns dict)
|
|
18
18
|
|
|
@@ -247,6 +247,132 @@ print(output_plain(data))
|
|
|
247
247
|
# api_key=*** cache_ttl=3600s count=42 created_at=2025-02-07T00:00:00.000Z file_size=5.0MB payment=50000000msats price=$99.99 request_timeout=5.0s success_rate=95.5% user_name=alice
|
|
248
248
|
```
|
|
249
249
|
|
|
250
|
+
## AFD Logging
|
|
251
|
+
|
|
252
|
+
AFD-compliant structured logging via Python's `logging` module. Every log line is formatted using the library's own `output_json`/`output_plain`/`output_yaml` functions. Span fields are carried via `contextvars` (async-safe), automatically flattened into each log line.
|
|
253
|
+
|
|
254
|
+
### API
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
from agent_first_data import init_logging_json, init_logging_plain, init_logging_yaml
|
|
258
|
+
from agent_first_data.afd_logging import AfdHandler, get_logger, span
|
|
259
|
+
|
|
260
|
+
# Convenience initializers — set up the root logger with AFD output to stdout
|
|
261
|
+
init_logging_json(level="INFO") # Single-line JSONL (secrets redacted, original keys)
|
|
262
|
+
init_logging_plain(level="INFO") # Single-line logfmt (keys stripped, values formatted)
|
|
263
|
+
init_logging_yaml(level="INFO") # Multi-line YAML (keys stripped, values formatted)
|
|
264
|
+
|
|
265
|
+
# Low-level — create a handler for custom logger stacks
|
|
266
|
+
AfdHandler(format="json") # format: "json" | "plain" | "yaml"
|
|
267
|
+
|
|
268
|
+
# Logger with default fields (returns logging.LoggerAdapter)
|
|
269
|
+
get_logger(name, **fields)
|
|
270
|
+
|
|
271
|
+
# Span context manager — adds fields to all log events within the block
|
|
272
|
+
span(**fields)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Setup
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
from agent_first_data import init_logging_json, init_logging_plain, init_logging_yaml
|
|
279
|
+
|
|
280
|
+
# JSON output for production (one JSONL line per event, secrets redacted)
|
|
281
|
+
init_logging_json("INFO")
|
|
282
|
+
|
|
283
|
+
# Plain logfmt for development (keys stripped, values formatted)
|
|
284
|
+
init_logging_plain("DEBUG")
|
|
285
|
+
|
|
286
|
+
# YAML for detailed inspection (multi-line, keys stripped, values formatted)
|
|
287
|
+
init_logging_yaml("DEBUG")
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Log Output
|
|
291
|
+
|
|
292
|
+
Standard `logging` calls work unchanged. Output format depends on the init function used.
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
import logging
|
|
296
|
+
logger = logging.getLogger("myapp")
|
|
297
|
+
|
|
298
|
+
logger.info("Server started")
|
|
299
|
+
# JSON: {"timestamp_epoch_ms":1739000000000,"message":"Server started","target":"myapp","code":"info"}
|
|
300
|
+
# Plain: code=info message="Server started" target=myapp timestamp_epoch_ms=1739000000000
|
|
301
|
+
# YAML: ---
|
|
302
|
+
# code: "info"
|
|
303
|
+
# message: "Server started"
|
|
304
|
+
# target: "myapp"
|
|
305
|
+
# timestamp_epoch_ms: 1739000000000
|
|
306
|
+
|
|
307
|
+
logger.warning("DNS lookup failed")
|
|
308
|
+
# JSON: {"timestamp_epoch_ms":...,"message":"DNS lookup failed","target":"myapp","code":"warn"}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Span Support
|
|
312
|
+
|
|
313
|
+
Use the `span` context manager to add fields to all log events within the block. Spans nest and work with both sync and async code.
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
from agent_first_data import span
|
|
317
|
+
|
|
318
|
+
with span(request_id="abc-123"):
|
|
319
|
+
logger.info("Processing")
|
|
320
|
+
# {"timestamp_epoch_ms":...,"message":"Processing","target":"myapp","request_id":"abc-123","code":"info"}
|
|
321
|
+
|
|
322
|
+
with span(step="validate"):
|
|
323
|
+
logger.info("Validating input")
|
|
324
|
+
# {"timestamp_epoch_ms":...,"message":"Validating input","target":"myapp","request_id":"abc-123","step":"validate","code":"info"}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Logger with Default Fields
|
|
328
|
+
|
|
329
|
+
Use `get_logger` for per-component fields that appear on every log line:
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
from agent_first_data import get_logger
|
|
333
|
+
|
|
334
|
+
logger = get_logger("myapp.auth", component="auth")
|
|
335
|
+
logger.info("Token verified")
|
|
336
|
+
# {"timestamp_epoch_ms":...,"message":"Token verified","target":"myapp.auth","component":"auth","code":"info"}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Custom Code Override
|
|
340
|
+
|
|
341
|
+
The `code` field defaults to the log level. Override with an explicit field:
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
from agent_first_data import get_logger
|
|
345
|
+
|
|
346
|
+
logger = get_logger("myapp")
|
|
347
|
+
logger.info("Server ready", extra={"code": "startup"})
|
|
348
|
+
# {"timestamp_epoch_ms":...,"message":"Server ready","target":"myapp","code":"startup"}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Output Fields
|
|
352
|
+
|
|
353
|
+
Every log line contains:
|
|
354
|
+
|
|
355
|
+
| Field | Type | Description |
|
|
356
|
+
|:------|:-----|:------------|
|
|
357
|
+
| `timestamp_epoch_ms` | number | Unix milliseconds |
|
|
358
|
+
| `message` | string | Log message |
|
|
359
|
+
| `target` | string | Logger name |
|
|
360
|
+
| `code` | string | Level (debug/info/warn/error) or explicit override |
|
|
361
|
+
| *span fields* | any | From `span()` context manager |
|
|
362
|
+
| *event fields* | any | From `extra=` or `get_logger` fields |
|
|
363
|
+
|
|
364
|
+
### Log Output Formats
|
|
365
|
+
|
|
366
|
+
All three formats use the library's own output functions, so AFD suffix processing applies to log fields too:
|
|
367
|
+
|
|
368
|
+
| Format | Function | Keys | Values | Use case |
|
|
369
|
+
|:-------|:---------|:-----|:-------|:---------|
|
|
370
|
+
| **JSON** | `init_logging_json` | original (with suffix) | raw | production, log aggregation |
|
|
371
|
+
| **Plain** | `init_logging_plain` | stripped | formatted | development, compact scanning |
|
|
372
|
+
| **YAML** | `init_logging_yaml` | stripped | formatted | debugging, detailed inspection |
|
|
373
|
+
|
|
374
|
+
All formats automatically redact `_secret` fields in log output.
|
|
375
|
+
|
|
250
376
|
## Output Formats
|
|
251
377
|
|
|
252
378
|
Three output formats for different use cases:
|
|
@@ -12,6 +12,16 @@ from agent_first_data.format import (
|
|
|
12
12
|
parse_size,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
+
from agent_first_data.afd_logging import (
|
|
16
|
+
AfdHandler,
|
|
17
|
+
AfdJsonHandler,
|
|
18
|
+
init_json as init_logging_json,
|
|
19
|
+
init_plain as init_logging_plain,
|
|
20
|
+
init_yaml as init_logging_yaml,
|
|
21
|
+
get_logger,
|
|
22
|
+
span,
|
|
23
|
+
)
|
|
24
|
+
|
|
15
25
|
__all__ = [
|
|
16
26
|
"build_json_startup",
|
|
17
27
|
"build_json_ok",
|
|
@@ -22,4 +32,11 @@ __all__ = [
|
|
|
22
32
|
"output_plain",
|
|
23
33
|
"internal_redact_secrets",
|
|
24
34
|
"parse_size",
|
|
35
|
+
"AfdHandler",
|
|
36
|
+
"AfdJsonHandler",
|
|
37
|
+
"init_logging_json",
|
|
38
|
+
"init_logging_plain",
|
|
39
|
+
"init_logging_yaml",
|
|
40
|
+
"get_logger",
|
|
41
|
+
"span",
|
|
25
42
|
]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""AFD-compliant structured logging.
|
|
2
|
+
|
|
3
|
+
Outputs log events using agent-first-data formatting functions:
|
|
4
|
+
- JSON: single-line JSONL via output_json (secrets redacted, original keys)
|
|
5
|
+
- Plain: single-line logfmt via output_plain (keys stripped, values formatted)
|
|
6
|
+
- YAML: multi-line via output_yaml (keys stripped, values formatted)
|
|
7
|
+
|
|
8
|
+
Span fields are carried via contextvars (async-safe).
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from agent_first_data.afd_logging import init_json, init_plain, init_yaml, span
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
init_json("INFO") # or init_plain("INFO") or init_yaml("DEBUG")
|
|
15
|
+
logger = logging.getLogger("myapp")
|
|
16
|
+
|
|
17
|
+
with span(request_id="abc-123"):
|
|
18
|
+
logger.info("Processing")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import sys
|
|
23
|
+
from contextvars import ContextVar, Token
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from agent_first_data.format import output_json, output_plain, output_yaml
|
|
27
|
+
|
|
28
|
+
_span_fields: ContextVar[dict[str, Any]] = ContextVar("afd_span", default={})
|
|
29
|
+
|
|
30
|
+
_LEVEL_TO_CODE = {
|
|
31
|
+
"CRITICAL": "error",
|
|
32
|
+
"ERROR": "error",
|
|
33
|
+
"WARNING": "warn",
|
|
34
|
+
"WARN": "warn",
|
|
35
|
+
"INFO": "info",
|
|
36
|
+
"DEBUG": "debug",
|
|
37
|
+
"NOTSET": "trace",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AfdHandler(logging.Handler):
|
|
42
|
+
"""Logging handler that outputs AFD-compliant log lines to stdout.
|
|
43
|
+
|
|
44
|
+
Formats output using the library's own output_json/output_plain/output_yaml.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, format: str = "json") -> None:
|
|
48
|
+
super().__init__()
|
|
49
|
+
if format not in ("json", "plain", "yaml"):
|
|
50
|
+
raise ValueError(f"Unknown format: {format!r}, expected json/plain/yaml")
|
|
51
|
+
self._format = format
|
|
52
|
+
|
|
53
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
54
|
+
entry: dict[str, Any] = {
|
|
55
|
+
"timestamp_epoch_ms": int(record.created * 1000),
|
|
56
|
+
"message": record.getMessage(),
|
|
57
|
+
"target": record.name,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Span fields (from contextvars, async-safe)
|
|
61
|
+
span_data = _span_fields.get()
|
|
62
|
+
if span_data:
|
|
63
|
+
entry.update(span_data)
|
|
64
|
+
|
|
65
|
+
# Event fields (passed via extra= in logging calls)
|
|
66
|
+
has_code = False
|
|
67
|
+
extra = getattr(record, "_afd_fields", None)
|
|
68
|
+
if extra:
|
|
69
|
+
for k, v in extra.items():
|
|
70
|
+
if k == "code":
|
|
71
|
+
has_code = True
|
|
72
|
+
entry[k] = v
|
|
73
|
+
|
|
74
|
+
# Default code from level
|
|
75
|
+
if not has_code:
|
|
76
|
+
entry["code"] = _LEVEL_TO_CODE.get(record.levelname, "info")
|
|
77
|
+
|
|
78
|
+
# Format using the library's own output functions
|
|
79
|
+
if self._format == "plain":
|
|
80
|
+
line = output_plain(entry)
|
|
81
|
+
elif self._format == "yaml":
|
|
82
|
+
line = output_yaml(entry)
|
|
83
|
+
else:
|
|
84
|
+
line = output_json(entry)
|
|
85
|
+
|
|
86
|
+
sys.stdout.write(line + "\n")
|
|
87
|
+
sys.stdout.flush()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Keep old name as alias for backwards compat
|
|
91
|
+
AfdJsonHandler = AfdHandler
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class _AfdLoggerAdapter(logging.LoggerAdapter):
|
|
95
|
+
"""Logger adapter that passes extra fields to AfdHandler."""
|
|
96
|
+
|
|
97
|
+
def process(self, msg: str, kwargs: Any) -> tuple[str, Any]:
|
|
98
|
+
extra = kwargs.get("extra", {})
|
|
99
|
+
if self.extra:
|
|
100
|
+
merged = {**self.extra, **extra}
|
|
101
|
+
else:
|
|
102
|
+
merged = extra
|
|
103
|
+
kwargs["extra"] = {"_afd_fields": merged}
|
|
104
|
+
return msg, kwargs
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _init_with_format(format: str, level: str = "INFO") -> None:
|
|
108
|
+
handler = AfdHandler(format=format)
|
|
109
|
+
root = logging.getLogger()
|
|
110
|
+
root.handlers = [handler]
|
|
111
|
+
root.setLevel(getattr(logging, level.upper(), logging.INFO))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def init_json(level: str = "INFO") -> None:
|
|
115
|
+
"""Initialize the root logger with AFD JSON output to stdout."""
|
|
116
|
+
_init_with_format("json", level)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def init_plain(level: str = "INFO") -> None:
|
|
120
|
+
"""Initialize the root logger with AFD plain/logfmt output to stdout."""
|
|
121
|
+
_init_with_format("plain", level)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def init_yaml(level: str = "INFO") -> None:
|
|
125
|
+
"""Initialize the root logger with AFD YAML output to stdout."""
|
|
126
|
+
_init_with_format("yaml", level)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_logger(name: str, **fields: Any) -> logging.LoggerAdapter:
|
|
130
|
+
"""Get a logger with optional default fields.
|
|
131
|
+
|
|
132
|
+
Fields passed here appear on every log line from this logger.
|
|
133
|
+
Use for per-module or per-component fields.
|
|
134
|
+
"""
|
|
135
|
+
base = logging.getLogger(name)
|
|
136
|
+
return _AfdLoggerAdapter(base, fields)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class span:
|
|
140
|
+
"""Context manager that adds fields to all log events within the block.
|
|
141
|
+
|
|
142
|
+
Spans nest: inner spans inherit and can override outer span fields.
|
|
143
|
+
Works with both sync and async code (uses contextvars).
|
|
144
|
+
|
|
145
|
+
Usage:
|
|
146
|
+
with span(request_id="abc-123", method="GET"):
|
|
147
|
+
logger.info("Handling request")
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(self, **fields: Any) -> None:
|
|
151
|
+
self.fields = fields
|
|
152
|
+
self._token: Token[dict[str, Any]] | None = None
|
|
153
|
+
|
|
154
|
+
def __enter__(self) -> "span":
|
|
155
|
+
current = _span_fields.get()
|
|
156
|
+
self._token = _span_fields.set({**current, **self.fields})
|
|
157
|
+
return self
|
|
158
|
+
|
|
159
|
+
def __exit__(self, *_: Any) -> None:
|
|
160
|
+
if self._token is not None:
|
|
161
|
+
_span_fields.reset(self._token)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-first-data
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Agent-First Data (AFD) — suffix-driven output formatting and protocol templates for AI agents
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/cmnspore/agent-first-data
|
|
@@ -21,7 +21,7 @@ pip install agent-first-data
|
|
|
21
21
|
|
|
22
22
|
## API Reference
|
|
23
23
|
|
|
24
|
-
Total: **9 public APIs** (4 protocol builders + 3 output functions + 1 internal + 1 utility)
|
|
24
|
+
Total: **9 public APIs** + **AFD logging** (4 protocol builders + 3 output functions + 1 internal + 1 utility)
|
|
25
25
|
|
|
26
26
|
### Protocol Builders (returns dict)
|
|
27
27
|
|
|
@@ -256,6 +256,132 @@ print(output_plain(data))
|
|
|
256
256
|
# api_key=*** cache_ttl=3600s count=42 created_at=2025-02-07T00:00:00.000Z file_size=5.0MB payment=50000000msats price=$99.99 request_timeout=5.0s success_rate=95.5% user_name=alice
|
|
257
257
|
```
|
|
258
258
|
|
|
259
|
+
## AFD Logging
|
|
260
|
+
|
|
261
|
+
AFD-compliant structured logging via Python's `logging` module. Every log line is formatted using the library's own `output_json`/`output_plain`/`output_yaml` functions. Span fields are carried via `contextvars` (async-safe), automatically flattened into each log line.
|
|
262
|
+
|
|
263
|
+
### API
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
from agent_first_data import init_logging_json, init_logging_plain, init_logging_yaml
|
|
267
|
+
from agent_first_data.afd_logging import AfdHandler, get_logger, span
|
|
268
|
+
|
|
269
|
+
# Convenience initializers — set up the root logger with AFD output to stdout
|
|
270
|
+
init_logging_json(level="INFO") # Single-line JSONL (secrets redacted, original keys)
|
|
271
|
+
init_logging_plain(level="INFO") # Single-line logfmt (keys stripped, values formatted)
|
|
272
|
+
init_logging_yaml(level="INFO") # Multi-line YAML (keys stripped, values formatted)
|
|
273
|
+
|
|
274
|
+
# Low-level — create a handler for custom logger stacks
|
|
275
|
+
AfdHandler(format="json") # format: "json" | "plain" | "yaml"
|
|
276
|
+
|
|
277
|
+
# Logger with default fields (returns logging.LoggerAdapter)
|
|
278
|
+
get_logger(name, **fields)
|
|
279
|
+
|
|
280
|
+
# Span context manager — adds fields to all log events within the block
|
|
281
|
+
span(**fields)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Setup
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
from agent_first_data import init_logging_json, init_logging_plain, init_logging_yaml
|
|
288
|
+
|
|
289
|
+
# JSON output for production (one JSONL line per event, secrets redacted)
|
|
290
|
+
init_logging_json("INFO")
|
|
291
|
+
|
|
292
|
+
# Plain logfmt for development (keys stripped, values formatted)
|
|
293
|
+
init_logging_plain("DEBUG")
|
|
294
|
+
|
|
295
|
+
# YAML for detailed inspection (multi-line, keys stripped, values formatted)
|
|
296
|
+
init_logging_yaml("DEBUG")
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Log Output
|
|
300
|
+
|
|
301
|
+
Standard `logging` calls work unchanged. Output format depends on the init function used.
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
import logging
|
|
305
|
+
logger = logging.getLogger("myapp")
|
|
306
|
+
|
|
307
|
+
logger.info("Server started")
|
|
308
|
+
# JSON: {"timestamp_epoch_ms":1739000000000,"message":"Server started","target":"myapp","code":"info"}
|
|
309
|
+
# Plain: code=info message="Server started" target=myapp timestamp_epoch_ms=1739000000000
|
|
310
|
+
# YAML: ---
|
|
311
|
+
# code: "info"
|
|
312
|
+
# message: "Server started"
|
|
313
|
+
# target: "myapp"
|
|
314
|
+
# timestamp_epoch_ms: 1739000000000
|
|
315
|
+
|
|
316
|
+
logger.warning("DNS lookup failed")
|
|
317
|
+
# JSON: {"timestamp_epoch_ms":...,"message":"DNS lookup failed","target":"myapp","code":"warn"}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Span Support
|
|
321
|
+
|
|
322
|
+
Use the `span` context manager to add fields to all log events within the block. Spans nest and work with both sync and async code.
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
from agent_first_data import span
|
|
326
|
+
|
|
327
|
+
with span(request_id="abc-123"):
|
|
328
|
+
logger.info("Processing")
|
|
329
|
+
# {"timestamp_epoch_ms":...,"message":"Processing","target":"myapp","request_id":"abc-123","code":"info"}
|
|
330
|
+
|
|
331
|
+
with span(step="validate"):
|
|
332
|
+
logger.info("Validating input")
|
|
333
|
+
# {"timestamp_epoch_ms":...,"message":"Validating input","target":"myapp","request_id":"abc-123","step":"validate","code":"info"}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Logger with Default Fields
|
|
337
|
+
|
|
338
|
+
Use `get_logger` for per-component fields that appear on every log line:
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
from agent_first_data import get_logger
|
|
342
|
+
|
|
343
|
+
logger = get_logger("myapp.auth", component="auth")
|
|
344
|
+
logger.info("Token verified")
|
|
345
|
+
# {"timestamp_epoch_ms":...,"message":"Token verified","target":"myapp.auth","component":"auth","code":"info"}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Custom Code Override
|
|
349
|
+
|
|
350
|
+
The `code` field defaults to the log level. Override with an explicit field:
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
from agent_first_data import get_logger
|
|
354
|
+
|
|
355
|
+
logger = get_logger("myapp")
|
|
356
|
+
logger.info("Server ready", extra={"code": "startup"})
|
|
357
|
+
# {"timestamp_epoch_ms":...,"message":"Server ready","target":"myapp","code":"startup"}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Output Fields
|
|
361
|
+
|
|
362
|
+
Every log line contains:
|
|
363
|
+
|
|
364
|
+
| Field | Type | Description |
|
|
365
|
+
|:------|:-----|:------------|
|
|
366
|
+
| `timestamp_epoch_ms` | number | Unix milliseconds |
|
|
367
|
+
| `message` | string | Log message |
|
|
368
|
+
| `target` | string | Logger name |
|
|
369
|
+
| `code` | string | Level (debug/info/warn/error) or explicit override |
|
|
370
|
+
| *span fields* | any | From `span()` context manager |
|
|
371
|
+
| *event fields* | any | From `extra=` or `get_logger` fields |
|
|
372
|
+
|
|
373
|
+
### Log Output Formats
|
|
374
|
+
|
|
375
|
+
All three formats use the library's own output functions, so AFD suffix processing applies to log fields too:
|
|
376
|
+
|
|
377
|
+
| Format | Function | Keys | Values | Use case |
|
|
378
|
+
|:-------|:---------|:-----|:-------|:---------|
|
|
379
|
+
| **JSON** | `init_logging_json` | original (with suffix) | raw | production, log aggregation |
|
|
380
|
+
| **Plain** | `init_logging_plain` | stripped | formatted | development, compact scanning |
|
|
381
|
+
| **YAML** | `init_logging_yaml` | stripped | formatted | debugging, detailed inspection |
|
|
382
|
+
|
|
383
|
+
All formats automatically redact `_secret` fields in log output.
|
|
384
|
+
|
|
259
385
|
## Output Formats
|
|
260
386
|
|
|
261
387
|
Three output formats for different use cases:
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
pyproject.toml
|
|
3
3
|
agent_first_data/__init__.py
|
|
4
|
+
agent_first_data/afd_logging.py
|
|
4
5
|
agent_first_data/format.py
|
|
5
6
|
agent_first_data.egg-info/PKG-INFO
|
|
6
7
|
agent_first_data.egg-info/SOURCES.txt
|
|
7
8
|
agent_first_data.egg-info/dependency_links.txt
|
|
8
9
|
agent_first_data.egg-info/top_level.txt
|
|
10
|
+
tests/test_afd_logging.py
|
|
9
11
|
tests/test_format.py
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Tests for AFD logging module."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
from agent_first_data.afd_logging import AfdHandler, AfdJsonHandler, init_json, init_plain, init_yaml, span, get_logger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def capture_log(fn):
|
|
13
|
+
"""Run fn and return the parsed JSON log line."""
|
|
14
|
+
buf = StringIO()
|
|
15
|
+
with patch("sys.stdout", buf):
|
|
16
|
+
fn()
|
|
17
|
+
line = buf.getvalue().strip()
|
|
18
|
+
assert line, "No log output captured"
|
|
19
|
+
return json.loads(line)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def make_logger(name="test"):
|
|
23
|
+
"""Create a fresh logger with AfdJsonHandler."""
|
|
24
|
+
logger = logging.getLogger(name)
|
|
25
|
+
logger.handlers = [AfdJsonHandler()]
|
|
26
|
+
logger.setLevel(logging.DEBUG)
|
|
27
|
+
return logger
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestBasicFields:
|
|
31
|
+
def test_info_message(self):
|
|
32
|
+
logger = make_logger("test_basic")
|
|
33
|
+
m = capture_log(lambda: logger.info("hello world"))
|
|
34
|
+
assert m["message"] == "hello world"
|
|
35
|
+
assert m["code"] == "info"
|
|
36
|
+
assert "timestamp_epoch_ms" in m
|
|
37
|
+
assert m["target"] == "test_basic"
|
|
38
|
+
|
|
39
|
+
def test_warning_code(self):
|
|
40
|
+
logger = make_logger("test_warn")
|
|
41
|
+
m = capture_log(lambda: logger.warning("something wrong"))
|
|
42
|
+
assert m["code"] == "warn"
|
|
43
|
+
|
|
44
|
+
def test_error_code(self):
|
|
45
|
+
logger = make_logger("test_error")
|
|
46
|
+
m = capture_log(lambda: logger.error("failure"))
|
|
47
|
+
assert m["code"] == "error"
|
|
48
|
+
|
|
49
|
+
def test_debug_code(self):
|
|
50
|
+
logger = make_logger("test_debug")
|
|
51
|
+
m = capture_log(lambda: logger.debug("verbose"))
|
|
52
|
+
assert m["code"] == "debug"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TestSpan:
|
|
56
|
+
def test_span_adds_fields(self):
|
|
57
|
+
logger = make_logger("test_span")
|
|
58
|
+
|
|
59
|
+
def run():
|
|
60
|
+
with span(request_id="abc-123"):
|
|
61
|
+
logger.info("processing")
|
|
62
|
+
|
|
63
|
+
m = capture_log(run)
|
|
64
|
+
assert m["request_id"] == "abc-123"
|
|
65
|
+
assert m["message"] == "processing"
|
|
66
|
+
|
|
67
|
+
def test_nested_spans(self):
|
|
68
|
+
logger = make_logger("test_nested")
|
|
69
|
+
|
|
70
|
+
def run():
|
|
71
|
+
with span(request_id="outer"):
|
|
72
|
+
with span(step="inner"):
|
|
73
|
+
logger.info("nested")
|
|
74
|
+
|
|
75
|
+
m = capture_log(run)
|
|
76
|
+
assert m["request_id"] == "outer"
|
|
77
|
+
assert m["step"] == "inner"
|
|
78
|
+
|
|
79
|
+
def test_inner_span_overrides_parent(self):
|
|
80
|
+
logger = make_logger("test_override")
|
|
81
|
+
|
|
82
|
+
def run():
|
|
83
|
+
with span(source="parent"):
|
|
84
|
+
with span(source="child"):
|
|
85
|
+
logger.info("test")
|
|
86
|
+
|
|
87
|
+
m = capture_log(run)
|
|
88
|
+
assert m["source"] == "child"
|
|
89
|
+
|
|
90
|
+
def test_span_fields_removed_after_exit(self):
|
|
91
|
+
logger = make_logger("test_exit")
|
|
92
|
+
buf = StringIO()
|
|
93
|
+
|
|
94
|
+
with patch("sys.stdout", buf):
|
|
95
|
+
with span(request_id="temp"):
|
|
96
|
+
logger.info("inside")
|
|
97
|
+
buf2 = StringIO()
|
|
98
|
+
|
|
99
|
+
with patch("sys.stdout", buf2):
|
|
100
|
+
logger.info("outside")
|
|
101
|
+
|
|
102
|
+
outside = json.loads(buf2.getvalue().strip())
|
|
103
|
+
assert "request_id" not in outside
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TestCodeOverride:
|
|
107
|
+
def test_explicit_code(self):
|
|
108
|
+
logger = make_logger("test_code")
|
|
109
|
+
adapter = get_logger("test_code")
|
|
110
|
+
|
|
111
|
+
m = capture_log(lambda: adapter.info("ready", extra={"code": "startup"}))
|
|
112
|
+
assert m["code"] == "startup"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class TestGetLogger:
|
|
116
|
+
def test_default_fields(self):
|
|
117
|
+
# Ensure root logger has AfdJsonHandler
|
|
118
|
+
root = logging.getLogger()
|
|
119
|
+
root.handlers = [AfdJsonHandler()]
|
|
120
|
+
root.setLevel(logging.DEBUG)
|
|
121
|
+
|
|
122
|
+
adapter = get_logger("test_adapter", component="myservice")
|
|
123
|
+
|
|
124
|
+
m = capture_log(lambda: adapter.info("event"))
|
|
125
|
+
assert m["component"] == "myservice"
|
|
126
|
+
assert m["message"] == "event"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def capture_raw(fn):
|
|
130
|
+
"""Run fn and return the raw output string."""
|
|
131
|
+
buf = StringIO()
|
|
132
|
+
with patch("sys.stdout", buf):
|
|
133
|
+
fn()
|
|
134
|
+
return buf.getvalue()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class TestPlainFormat:
|
|
138
|
+
def test_plain_output(self):
|
|
139
|
+
logger = logging.getLogger("test_plain")
|
|
140
|
+
logger.handlers = [AfdHandler(format="plain")]
|
|
141
|
+
logger.setLevel(logging.DEBUG)
|
|
142
|
+
|
|
143
|
+
output = capture_raw(lambda: logger.info("hello"))
|
|
144
|
+
# Plain format is single-line logfmt
|
|
145
|
+
assert "message=" in output
|
|
146
|
+
assert "code=info" in output
|
|
147
|
+
|
|
148
|
+
def test_init_plain(self):
|
|
149
|
+
buf = StringIO()
|
|
150
|
+
with patch("sys.stdout", buf):
|
|
151
|
+
init_plain("DEBUG")
|
|
152
|
+
logging.getLogger("test_init_plain").info("test")
|
|
153
|
+
output = buf.getvalue()
|
|
154
|
+
assert "message=" in output
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class TestYamlFormat:
|
|
158
|
+
def test_yaml_output(self):
|
|
159
|
+
logger = logging.getLogger("test_yaml")
|
|
160
|
+
logger.handlers = [AfdHandler(format="yaml")]
|
|
161
|
+
logger.setLevel(logging.DEBUG)
|
|
162
|
+
|
|
163
|
+
output = capture_raw(lambda: logger.info("hello"))
|
|
164
|
+
# YAML format starts with ---
|
|
165
|
+
assert output.startswith("---")
|
|
166
|
+
|
|
167
|
+
def test_init_yaml(self):
|
|
168
|
+
buf = StringIO()
|
|
169
|
+
with patch("sys.stdout", buf):
|
|
170
|
+
init_yaml("DEBUG")
|
|
171
|
+
logging.getLogger("test_init_yaml").info("test")
|
|
172
|
+
output = buf.getvalue()
|
|
173
|
+
assert output.startswith("---")
|
|
File without changes
|
{agent_first_data-0.2.0 → agent_first_data-0.2.2}/agent_first_data.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|