agent-scaffold-cli 0.1.1__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.
- agent_scaffold/__init__.py +8 -0
- agent_scaffold/__main__.py +6 -0
- agent_scaffold/_bundled_deployments/__init__.py +15 -0
- agent_scaffold/_bundled_deployments/docs/cross-cutting/README.md +15 -0
- agent_scaffold/_bundled_deployments/docs/cross-cutting/auth-jwt.md +235 -0
- agent_scaffold/_bundled_deployments/docs/cross-cutting/logging-structured.md +196 -0
- agent_scaffold/_bundled_deployments/docs/cross-cutting/observability.md +259 -0
- agent_scaffold/_bundled_deployments/docs/cross-cutting/rate-limiting.md +171 -0
- agent_scaffold/_bundled_deployments/docs/cross-cutting/testing-strategy.md +261 -0
- agent_scaffold/_bundled_deployments/docs/frameworks/README.md +22 -0
- agent_scaffold/_bundled_deployments/docs/frameworks/crewai.md +91 -0
- agent_scaffold/_bundled_deployments/docs/frameworks/langgraph.md +79 -0
- agent_scaffold/_bundled_deployments/docs/frameworks/mastra.md +74 -0
- agent_scaffold/_bundled_deployments/docs/frameworks/pydantic-ai.md +77 -0
- agent_scaffold/_bundled_deployments/docs/frameworks/vercel-ai-sdk.md +83 -0
- agent_scaffold/_bundled_deployments/docs/patterns/README.md +26 -0
- agent_scaffold/_bundled_deployments/docs/patterns/memory.md +82 -0
- agent_scaffold/_bundled_deployments/docs/patterns/multi-agent-flat.md +72 -0
- agent_scaffold/_bundled_deployments/docs/patterns/multi-agent-hierarchical.md +83 -0
- agent_scaffold/_bundled_deployments/docs/patterns/parallel-calls.md +73 -0
- agent_scaffold/_bundled_deployments/docs/patterns/plan-execute-reflect.md +77 -0
- agent_scaffold/_bundled_deployments/docs/patterns/prompt-chaining.md +73 -0
- agent_scaffold/_bundled_deployments/docs/patterns/rag.md +84 -0
- agent_scaffold/_bundled_deployments/docs/patterns/react.md +77 -0
- agent_scaffold/_bundled_deployments/docs/patterns/routing-tool-use.md +69 -0
- agent_scaffold/_bundled_deployments/docs/recipes/README.md +39 -0
- agent_scaffold/_bundled_deployments/docs/recipes/code-review-agent.md +518 -0
- agent_scaffold/_bundled_deployments/docs/recipes/content-pipeline.md +525 -0
- agent_scaffold/_bundled_deployments/docs/recipes/customer-support-triage.md +1679 -0
- agent_scaffold/_bundled_deployments/docs/recipes/docs-rag-qa.md +1254 -0
- agent_scaffold/_bundled_deployments/docs/recipes/hierarchical-agent.md +554 -0
- agent_scaffold/_bundled_deployments/docs/recipes/memory-assistant.md +499 -0
- agent_scaffold/_bundled_deployments/docs/recipes/ops-crew.md +457 -0
- agent_scaffold/_bundled_deployments/docs/recipes/parallel-enricher.md +457 -0
- agent_scaffold/_bundled_deployments/docs/recipes/research-assistant.md +1096 -0
- agent_scaffold/_bundled_deployments/docs/stack/README.md +19 -0
- agent_scaffold/_bundled_deployments/docs/stack/api-fastapi.md +112 -0
- agent_scaffold/_bundled_deployments/docs/stack/api-hono.md +108 -0
- agent_scaffold/_bundled_deployments/docs/stack/cache-redis.md +85 -0
- agent_scaffold/_bundled_deployments/docs/stack/eval-deepeval-ragas-promptfoo.md +164 -0
- agent_scaffold/_bundled_deployments/docs/stack/llm-claude.md +105 -0
- agent_scaffold/_bundled_deployments/docs/stack/relational-postgres.md +122 -0
- agent_scaffold/_bundled_deployments/docs/stack/tool-protocol-mcp.md +275 -0
- agent_scaffold/_bundled_deployments/docs/stack/tracing-langfuse.md +108 -0
- agent_scaffold/_bundled_deployments/docs/stack/vector-qdrant.md +121 -0
- agent_scaffold/cache.py +32 -0
- agent_scaffold/cli.py +512 -0
- agent_scaffold/config.py +117 -0
- agent_scaffold/context.py +253 -0
- agent_scaffold/contract.py +141 -0
- agent_scaffold/discovery.py +112 -0
- agent_scaffold/generator.py +213 -0
- agent_scaffold/languages/__init__.py +0 -0
- agent_scaffold/languages/python.yaml +28 -0
- agent_scaffold/languages/typescript.yaml +25 -0
- agent_scaffold/prompts/__init__.py +0 -0
- agent_scaffold/prompts/repair.md +9 -0
- agent_scaffold/prompts/system.md +21 -0
- agent_scaffold/prompts/user_template.md +43 -0
- agent_scaffold/validator.py +133 -0
- agent_scaffold/writer.py +171 -0
- agent_scaffold_cli-0.1.1.dist-info/METADATA +147 -0
- agent_scaffold_cli-0.1.1.dist-info/RECORD +66 -0
- agent_scaffold_cli-0.1.1.dist-info/WHEEL +4 -0
- agent_scaffold_cli-0.1.1.dist-info/entry_points.txt +2 -0
- agent_scaffold_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Bundled agent-deployments docs for zero-config usage.
|
|
2
|
+
|
|
3
|
+
The docs/ directory is populated at build time by scripts/sync_deployments.sh.
|
|
4
|
+
When installed via PyPI/Homebrew, the bundled docs allow agent-scaffold to work
|
|
5
|
+
without requiring a separate agent-deployments clone.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def bundled_docs_path() -> Path:
|
|
14
|
+
"""Return the path to the bundled deployments root (contains docs/)."""
|
|
15
|
+
return Path(__file__).parent
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Cross-cutting Concerns
|
|
2
|
+
|
|
3
|
+
Shared production plumbing used by all agents. Each file answers: **"What production scaffolding do I need?"**
|
|
4
|
+
|
|
5
|
+
| Concern | Library (Py / TS) | Reference |
|
|
6
|
+
|---------|-------------------|-----------|
|
|
7
|
+
| [Auth](auth-jwt.md) | python-jose / hono-jwt | Inline implementation below |
|
|
8
|
+
| [Logging](logging-structured.md) | structlog / pino | Inline implementation below |
|
|
9
|
+
| [Observability](observability.md) | Langfuse SDK | Inline implementation below |
|
|
10
|
+
| [Rate Limiting](rate-limiting.md) | slowapi / hono-rate-limiter | Inline implementation below |
|
|
11
|
+
| [Testing](testing-strategy.md) | pytest + DeepEval / vitest + Promptfoo | Inline implementation below |
|
|
12
|
+
|
|
13
|
+
## The 11-point production checklist
|
|
14
|
+
|
|
15
|
+
Every blueprint specifies these concerns. See [playbook/production-checklist.md](../playbook/production-checklist.md) for the full checklist.
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Cross-cutting: JWT Authentication
|
|
2
|
+
|
|
3
|
+
**Concern:** Protect all agent endpoints with bearer-token authentication.
|
|
4
|
+
**Library:** `python-jose` (Py) / `jose` (TS)
|
|
5
|
+
**Lives in:** Inline below (formerly `common/python/agent_common/auth/` and `common/typescript/src/auth/`)
|
|
6
|
+
|
|
7
|
+
## What it provides
|
|
8
|
+
|
|
9
|
+
- **Token creation** -- `create_token()` / `createToken()` signs a JWT with a user ID, expiry, and optional extra claims.
|
|
10
|
+
- **Token verification** -- `verify_token()` / `verifyToken()` decodes and validates a JWT, returning a typed payload.
|
|
11
|
+
- **FastAPI dependency** -- `get_current_user(secret)` returns a FastAPI `Depends()` that extracts the bearer token from the `Authorization` header, verifies it, and returns a `TokenPayload`. Returns 401 on failure.
|
|
12
|
+
- **Typed payload** -- `TokenPayload` model (Pydantic / TS interface) with `sub`, `exp`, and extra claims.
|
|
13
|
+
|
|
14
|
+
## How to use
|
|
15
|
+
|
|
16
|
+
### Python (FastAPI)
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from agent_common.auth import create_token, get_current_user, TokenPayload
|
|
20
|
+
|
|
21
|
+
# Create a token (for testing or a /login endpoint)
|
|
22
|
+
token = create_token("user-123", secret="my-secret", expires_hours=24)
|
|
23
|
+
|
|
24
|
+
# Protect an endpoint
|
|
25
|
+
@app.post("/query")
|
|
26
|
+
async def query(
|
|
27
|
+
request: QueryRequest,
|
|
28
|
+
user: TokenPayload = Depends(get_current_user("my-secret")),
|
|
29
|
+
):
|
|
30
|
+
# user.sub is the authenticated user ID
|
|
31
|
+
return await handle_query(request, user_id=user.sub)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### TypeScript (Hono)
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { createToken, verifyToken } from "@agent-deployments/common";
|
|
38
|
+
|
|
39
|
+
// Create a token
|
|
40
|
+
const token = await createToken("user-123", "my-secret");
|
|
41
|
+
|
|
42
|
+
// Verify in middleware
|
|
43
|
+
app.use("/query/*", async (c, next) => {
|
|
44
|
+
const auth = c.req.header("Authorization");
|
|
45
|
+
const token = auth?.replace("Bearer ", "");
|
|
46
|
+
if (!token) return c.json({ error: "Unauthorized" }, 401);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const payload = await verifyToken(token, "my-secret");
|
|
50
|
+
c.set("userId", payload.sub);
|
|
51
|
+
await next();
|
|
52
|
+
} catch {
|
|
53
|
+
return c.json({ error: "Invalid token" }, 401);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Configuration via env
|
|
59
|
+
|
|
60
|
+
| Var | Default | Effect |
|
|
61
|
+
|-----|---------|--------|
|
|
62
|
+
| `JWT_SECRET` | `change-me-in-production` | Signing secret (HS256) |
|
|
63
|
+
| Algorithm | `HS256` | Symmetric signing for local dev. Switch to RS256 with a key pair for production |
|
|
64
|
+
| Expiry | 24 hours | Token lifetime |
|
|
65
|
+
|
|
66
|
+
## Tests
|
|
67
|
+
|
|
68
|
+
Test token creation, verification, expiry, invalid tokens, and the FastAPI dependency (Py) / token round-trip and invalid signature (TS).
|
|
69
|
+
|
|
70
|
+
## Production considerations
|
|
71
|
+
|
|
72
|
+
- **HS256 is fine for local dev** where the secret is in `.env`. For production, switch to **RS256** with asymmetric keys so the API only needs the public key.
|
|
73
|
+
- **Token rotation:** The current implementation has no refresh token flow. For production, add a `/refresh` endpoint or use short-lived tokens with an external auth provider.
|
|
74
|
+
- **Extra claims:** Pass `extra={"role": "admin"}` to embed authorization data in the token. The payload is available in the endpoint handler.
|
|
75
|
+
|
|
76
|
+
## Swapping to an external auth provider
|
|
77
|
+
|
|
78
|
+
To use Auth0, Clerk, or Supabase Auth instead of self-managed JWT:
|
|
79
|
+
|
|
80
|
+
1. Replace `create_token()` with the provider's token issuance (usually handled by their SDK).
|
|
81
|
+
2. Replace `verify_token()` with JWKS-based verification against the provider's public keys.
|
|
82
|
+
3. Keep the `get_current_user` dependency shape -- just change the verification logic inside.
|
|
83
|
+
|
|
84
|
+
This is a **single-file swap** (only the auth module changes).
|
|
85
|
+
|
|
86
|
+
## Reference Implementation
|
|
87
|
+
|
|
88
|
+
<details>
|
|
89
|
+
<summary>Python — <code>jwt.py</code></summary>
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
"""JWT utilities for FastAPI-based agent prototypes."""
|
|
93
|
+
|
|
94
|
+
from datetime import UTC, datetime, timedelta
|
|
95
|
+
from typing import Any
|
|
96
|
+
|
|
97
|
+
from fastapi import Depends, HTTPException, status
|
|
98
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
99
|
+
from jose import JWTError, jwt
|
|
100
|
+
from pydantic import BaseModel
|
|
101
|
+
|
|
102
|
+
_security = HTTPBearer()
|
|
103
|
+
|
|
104
|
+
_DEFAULT_ALGORITHM = "HS256"
|
|
105
|
+
_DEFAULT_EXPIRY_HOURS = 24
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TokenPayload(BaseModel):
|
|
109
|
+
sub: str
|
|
110
|
+
exp: datetime
|
|
111
|
+
extra: dict[str, Any] = {}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def create_token(
|
|
115
|
+
user_id: str,
|
|
116
|
+
secret: str,
|
|
117
|
+
*,
|
|
118
|
+
algorithm: str = _DEFAULT_ALGORITHM,
|
|
119
|
+
expires_hours: int = _DEFAULT_EXPIRY_HOURS,
|
|
120
|
+
extra: dict[str, Any] | None = None,
|
|
121
|
+
) -> str:
|
|
122
|
+
"""Create a signed JWT."""
|
|
123
|
+
now = datetime.now(UTC)
|
|
124
|
+
payload = {
|
|
125
|
+
"sub": user_id,
|
|
126
|
+
"iat": now,
|
|
127
|
+
"exp": now + timedelta(hours=expires_hours),
|
|
128
|
+
**(extra or {}),
|
|
129
|
+
}
|
|
130
|
+
return jwt.encode(payload, secret, algorithm=algorithm)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def verify_token(
|
|
134
|
+
token: str,
|
|
135
|
+
secret: str,
|
|
136
|
+
*,
|
|
137
|
+
algorithm: str = _DEFAULT_ALGORITHM,
|
|
138
|
+
) -> TokenPayload:
|
|
139
|
+
"""Verify and decode a JWT. Raises ValueError on failure."""
|
|
140
|
+
try:
|
|
141
|
+
payload = jwt.decode(token, secret, algorithms=[algorithm])
|
|
142
|
+
return TokenPayload(
|
|
143
|
+
sub=payload["sub"],
|
|
144
|
+
exp=datetime.fromtimestamp(payload["exp"], tz=UTC),
|
|
145
|
+
extra={k: v for k, v in payload.items() if k not in ("sub", "exp", "iat")},
|
|
146
|
+
)
|
|
147
|
+
except JWTError as exc:
|
|
148
|
+
raise ValueError(f"Invalid token: {exc}") from exc
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_current_user(
|
|
152
|
+
secret: str,
|
|
153
|
+
algorithm: str = _DEFAULT_ALGORITHM,
|
|
154
|
+
):
|
|
155
|
+
"""Return a FastAPI dependency that extracts and verifies the JWT bearer token."""
|
|
156
|
+
|
|
157
|
+
async def _dependency(
|
|
158
|
+
credentials: HTTPAuthorizationCredentials = Depends(_security),
|
|
159
|
+
) -> TokenPayload:
|
|
160
|
+
try:
|
|
161
|
+
return verify_token(credentials.credentials, secret, algorithm=algorithm)
|
|
162
|
+
except ValueError:
|
|
163
|
+
raise HTTPException(
|
|
164
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
165
|
+
detail="Invalid or expired token",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return _dependency
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
</details>
|
|
172
|
+
|
|
173
|
+
<details>
|
|
174
|
+
<summary>TypeScript — <code>jwt.ts</code></summary>
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
/**
|
|
178
|
+
* JWT utilities for Hono-based agent prototypes.
|
|
179
|
+
*/
|
|
180
|
+
|
|
181
|
+
import * as jose from "jose";
|
|
182
|
+
|
|
183
|
+
export interface TokenPayload {
|
|
184
|
+
sub: string;
|
|
185
|
+
exp: number;
|
|
186
|
+
iat: number;
|
|
187
|
+
[key: string]: unknown;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const DEFAULT_ALGORITHM = "HS256";
|
|
191
|
+
const DEFAULT_EXPIRY_HOURS = 24;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Create a signed JWT.
|
|
195
|
+
*/
|
|
196
|
+
export async function createToken(
|
|
197
|
+
userId: string,
|
|
198
|
+
secret: string,
|
|
199
|
+
options: {
|
|
200
|
+
algorithm?: string;
|
|
201
|
+
expiresHours?: number;
|
|
202
|
+
extra?: Record<string, unknown>;
|
|
203
|
+
} = {},
|
|
204
|
+
): Promise<string> {
|
|
205
|
+
const { expiresHours = DEFAULT_EXPIRY_HOURS, extra = {} } = options;
|
|
206
|
+
|
|
207
|
+
const secretKey = new TextEncoder().encode(secret);
|
|
208
|
+
const now = Math.floor(Date.now() / 1000);
|
|
209
|
+
|
|
210
|
+
return new jose.SignJWT({ sub: userId, ...extra })
|
|
211
|
+
.setProtectedHeader({ alg: DEFAULT_ALGORITHM })
|
|
212
|
+
.setIssuedAt(now)
|
|
213
|
+
.setExpirationTime(now + expiresHours * 3600)
|
|
214
|
+
.sign(secretKey);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Verify and decode a JWT. Throws on failure.
|
|
219
|
+
*/
|
|
220
|
+
export async function verifyToken(
|
|
221
|
+
token: string,
|
|
222
|
+
secret: string,
|
|
223
|
+
options: { algorithm?: string } = {},
|
|
224
|
+
): Promise<TokenPayload> {
|
|
225
|
+
const secretKey = new TextEncoder().encode(secret);
|
|
226
|
+
|
|
227
|
+
const { payload } = await jose.jwtVerify(token, secretKey, {
|
|
228
|
+
algorithms: [DEFAULT_ALGORITHM],
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return payload as TokenPayload;
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
</details>
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Cross-cutting: Structured Logging
|
|
2
|
+
|
|
3
|
+
**Concern:** JSON-structured logs with request/session/user context on every line.
|
|
4
|
+
**Library:** `structlog` (Py) / `pino` (TS)
|
|
5
|
+
**Lives in:** Inline below (formerly `common/python/agent_common/logs/` and `common/typescript/src/logging/`)
|
|
6
|
+
|
|
7
|
+
## What it provides
|
|
8
|
+
|
|
9
|
+
- **One-call setup** -- `configure(service_name, env, log_level)` (Py) / `createLogger(config)` (TS) configures the logger for the entire app.
|
|
10
|
+
- **Environment-aware output** -- Development mode renders human-readable colored output. Production mode renders JSON for log aggregators.
|
|
11
|
+
- **Contextual binding** -- `structlog.contextvars` (Py) / `pino.child()` (TS) lets you attach request-scoped context (trace ID, user ID, session ID) that appears on every subsequent log line.
|
|
12
|
+
- **Standard fields** -- Every log line includes: `service`, `env`, `level`, `timestamp` (ISO 8601), `msg`.
|
|
13
|
+
|
|
14
|
+
## How to use
|
|
15
|
+
|
|
16
|
+
### Python
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from agent_common.logs import configure
|
|
20
|
+
import structlog
|
|
21
|
+
|
|
22
|
+
# At app startup (typically in lifespan)
|
|
23
|
+
configure("docs-rag-qa", env="production", log_level="INFO")
|
|
24
|
+
|
|
25
|
+
# Get a logger anywhere
|
|
26
|
+
logger = structlog.get_logger()
|
|
27
|
+
|
|
28
|
+
# Basic logging
|
|
29
|
+
logger.info("query_received", question="What is MCP?")
|
|
30
|
+
|
|
31
|
+
# Bind context for a request scope
|
|
32
|
+
log = logger.bind(trace_id="abc-123", user_id="user-1")
|
|
33
|
+
log.info("processing_query")
|
|
34
|
+
log.info("query_answered", citation_count=3)
|
|
35
|
+
# Both lines include trace_id and user_id
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Output (production):
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{"service":"docs-rag-qa","env":"production","level":"info","timestamp":"2026-04-27T10:00:00Z","msg":"query_received","question":"What is MCP?"}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### TypeScript
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { createLogger } from "@agent-deployments/common";
|
|
48
|
+
|
|
49
|
+
const logger = createLogger({
|
|
50
|
+
serviceName: "docs-rag-qa",
|
|
51
|
+
env: "production",
|
|
52
|
+
level: "info",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Basic logging
|
|
56
|
+
logger.info({ question: "What is MCP?" }, "query_received");
|
|
57
|
+
|
|
58
|
+
// Child logger with request context
|
|
59
|
+
const reqLog = logger.child({ traceId: "abc-123", userId: "user-1" });
|
|
60
|
+
reqLog.info("processing_query");
|
|
61
|
+
reqLog.info({ citationCount: 3 }, "query_answered");
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Configuration via env
|
|
65
|
+
|
|
66
|
+
| Var | Default | Effect |
|
|
67
|
+
|-----|---------|--------|
|
|
68
|
+
| `LOG_LEVEL` | `INFO` | Minimum level to emit (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
|
|
69
|
+
| `APP_ENV` | `development` | Controls output format: `development` = colored console, anything else = JSON |
|
|
70
|
+
|
|
71
|
+
## Tests
|
|
72
|
+
|
|
73
|
+
Test configure in both modes and verify output format (Py). Test logger creation, child loggers, and level filtering (TS).
|
|
74
|
+
|
|
75
|
+
## Logging conventions
|
|
76
|
+
|
|
77
|
+
Use these patterns consistently across prototypes:
|
|
78
|
+
|
|
79
|
+
| Event | Key | Example |
|
|
80
|
+
|-------|-----|---------|
|
|
81
|
+
| Request received | `{endpoint}_received` | `logger.info("query_received", ...)` |
|
|
82
|
+
| Processing step | `{step}_completed` | `logger.info("retrieval_completed", chunk_count=5)` |
|
|
83
|
+
| Error | `{operation}_failed` | `logger.error("query_failed", error=str(exc))` |
|
|
84
|
+
| External call | `{service}_called` | `logger.info("llm_called", model="claude-sonnet-4-6", tokens=150)` |
|
|
85
|
+
|
|
86
|
+
Always use **snake_case** event names. Always include the **trace_id** and **user_id** via context binding, not per-call arguments.
|
|
87
|
+
|
|
88
|
+
## Swapping to OpenTelemetry-native logging
|
|
89
|
+
|
|
90
|
+
If your deployment already uses an OTel collector:
|
|
91
|
+
|
|
92
|
+
1. Replace structlog/pino with `opentelemetry-sdk` (Py) / `@opentelemetry/sdk-logs` (TS).
|
|
93
|
+
2. Remove the `configure()` / `createLogger()` call.
|
|
94
|
+
3. Set `OTEL_EXPORTER_OTLP_ENDPOINT` in env.
|
|
95
|
+
|
|
96
|
+
This is a **multi-file swap** (common module + app startup + docker-compose for the OTel collector).
|
|
97
|
+
|
|
98
|
+
## Reference Implementation
|
|
99
|
+
|
|
100
|
+
<details>
|
|
101
|
+
<summary>Python — <code>structlog_config.py</code></summary>
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
"""Structured logging configuration using structlog."""
|
|
105
|
+
|
|
106
|
+
import logging
|
|
107
|
+
import sys
|
|
108
|
+
|
|
109
|
+
import structlog
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def configure(
|
|
113
|
+
service_name: str,
|
|
114
|
+
*,
|
|
115
|
+
env: str = "development",
|
|
116
|
+
log_level: str = "INFO",
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Configure structlog for the application.
|
|
119
|
+
|
|
120
|
+
In production, outputs JSON. In development, outputs colored human-readable logs.
|
|
121
|
+
"""
|
|
122
|
+
shared_processors: list[structlog.types.Processor] = [
|
|
123
|
+
structlog.contextvars.merge_contextvars,
|
|
124
|
+
structlog.processors.add_log_level,
|
|
125
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
126
|
+
structlog.processors.StackInfoRenderer(),
|
|
127
|
+
structlog.processors.format_exc_info,
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
if env == "development":
|
|
131
|
+
renderer: structlog.types.Processor = structlog.dev.ConsoleRenderer()
|
|
132
|
+
else:
|
|
133
|
+
renderer = structlog.processors.JSONRenderer()
|
|
134
|
+
|
|
135
|
+
structlog.configure(
|
|
136
|
+
processors=[
|
|
137
|
+
*shared_processors,
|
|
138
|
+
structlog.processors.EventRenamer("msg"),
|
|
139
|
+
renderer,
|
|
140
|
+
],
|
|
141
|
+
wrapper_class=structlog.make_filtering_bound_logger(logging.getLevelNamesMapping()[log_level.upper()]),
|
|
142
|
+
context_class=dict,
|
|
143
|
+
logger_factory=structlog.PrintLoggerFactory(file=sys.stdout),
|
|
144
|
+
cache_logger_on_first_use=True,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
structlog.contextvars.clear_contextvars()
|
|
148
|
+
structlog.contextvars.bind_contextvars(service=service_name, env=env)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
</details>
|
|
152
|
+
|
|
153
|
+
<details>
|
|
154
|
+
<summary>TypeScript — <code>logger.ts</code></summary>
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
/**
|
|
158
|
+
* Structured logging using pino.
|
|
159
|
+
*/
|
|
160
|
+
|
|
161
|
+
import pino from "pino";
|
|
162
|
+
|
|
163
|
+
export interface LoggerConfig {
|
|
164
|
+
serviceName: string;
|
|
165
|
+
env?: string;
|
|
166
|
+
level?: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Create a configured pino logger instance.
|
|
171
|
+
*/
|
|
172
|
+
export function createLogger(config: LoggerConfig): pino.Logger {
|
|
173
|
+
const { serviceName, env = "development", level = "info" } = config;
|
|
174
|
+
|
|
175
|
+
const transport =
|
|
176
|
+
env === "development"
|
|
177
|
+
? {
|
|
178
|
+
target: "pino/file",
|
|
179
|
+
options: { destination: 1 }, // stdout
|
|
180
|
+
}
|
|
181
|
+
: undefined;
|
|
182
|
+
|
|
183
|
+
return pino({
|
|
184
|
+
name: serviceName,
|
|
185
|
+
level,
|
|
186
|
+
transport,
|
|
187
|
+
base: {
|
|
188
|
+
service: serviceName,
|
|
189
|
+
env,
|
|
190
|
+
},
|
|
191
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
</details>
|