| description | How to write apcore middleware with before/after hooks for logging, metrics, error handling, data transformation, caching, and rate limiting around module execution. |
|---|
Use middleware to extend logic before and after module execution.
Middleware are hooks that run before and after module execution, used for:
- Logging: Record invocation logs
- Performance Monitoring: Collect execution time and metrics
- Error Handling: Unified exception handling
- Data Transformation: Modify inputs/outputs
- Caching: Cache execution results
- Rate Limiting: Control invocation frequency
=== "Python"
```python
from typing import Any
from apcore import Context, Middleware
class MyMiddleware(Middleware):
"""Middleware base class"""
def before(
self,
module_id: str,
inputs: dict[str, Any],
context: Context,
) -> dict[str, Any] | None:
"""
Called before module execution.
Args:
module_id: Module ID
inputs: Input parameters
context: Invocation context
Returns:
Modified inputs, or None to keep unchanged.
"""
return None
def after(
self,
module_id: str,
inputs: dict[str, Any],
output: dict[str, Any],
context: Context,
) -> dict[str, Any] | None:
"""
Called after module execution.
Returns:
Modified output, or None to keep unchanged.
"""
return None
def on_error(
self,
module_id: str,
inputs: dict[str, Any],
error: Exception,
context: Context,
) -> dict[str, Any] | None:
"""
Called when module execution fails.
Returns:
Alternative output (for error recovery), or None to continue
raising the exception.
"""
return None
```
=== "TypeScript"
```typescript
import { Middleware } from 'apcore-js';
import type { Context } from 'apcore-js';
class MyMiddleware extends Middleware {
constructor() {
// Priority 0-1000; higher runs first. Default is 100.
super(100);
}
// Called before module execution. Return modified inputs, or null to
// keep them unchanged. May also return a Promise of either.
override before(
moduleId: string,
inputs: Record<string, unknown>,
context: Context,
): Record<string, unknown> | null {
return null;
}
// Called after successful module execution. Return modified output,
// or null to keep it unchanged.
override after(
moduleId: string,
inputs: Record<string, unknown>,
output: Record<string, unknown>,
context: Context,
): Record<string, unknown> | null {
return null;
}
// Called when module execution fails. Return a recovery value to
// "swallow" the error, or null to let it propagate.
override onError(
moduleId: string,
inputs: Record<string, unknown>,
error: Error,
context: Context,
): Record<string, unknown> | null {
return null;
}
}
```
=== "Rust"
```rust
use apcore::context::Context;
use apcore::errors::ModuleError;
use apcore::middleware::Middleware;
use async_trait::async_trait;
use serde_json::Value;
#[derive(Debug)]
struct MyMiddleware;
#[async_trait]
impl Middleware for MyMiddleware {
fn name(&self) -> &str {
"my_middleware"
}
// Priority 0-1000; higher runs first. Default is 100.
fn priority(&self) -> u16 {
100
}
/// Called before module execution. Return `Some(inputs)` to modify,
/// or `None` to leave unchanged.
async fn before(
&self,
_module_id: &str,
_inputs: Value,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
Ok(None)
}
/// Called after successful module execution.
async fn after(
&self,
_module_id: &str,
_inputs: Value,
_output: Value,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
Ok(None)
}
/// Called when module execution fails. Return `Some(value)` to
/// recover, or `None` to let the error propagate.
async fn on_error(
&self,
_module_id: &str,
_inputs: Value,
_error: &ModuleError,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
Ok(None)
}
}
```
=== "Python"
```python
from apcore import Middleware, Context
class SimpleLoggingMiddleware(Middleware):
"""Logging middleware"""
def before(self, module_id: str, inputs: dict, context: Context) -> None:
print(f"[{context.trace_id}] Calling {module_id}")
# Use context.redacted_inputs to avoid leaking sensitive data
print(f" Inputs: {context.redacted_inputs}")
def after(self, module_id: str, inputs: dict, output: dict, context: Context) -> None:
print(f"[{context.trace_id}] {module_id} completed")
print(f" Output: {output}")
def on_error(self, module_id: str, inputs: dict, error: Exception, context: Context) -> None:
print(f"[{context.trace_id}] {module_id} failed: {error}")
```
=== "TypeScript"
```typescript
import { Middleware } from 'apcore-js';
import type { Context } from 'apcore-js';
class SimpleLoggingMiddleware extends Middleware {
override before(
moduleId: string,
inputs: Record<string, unknown>,
context: Context,
): null {
console.log(`[${context.traceId}] Calling ${moduleId}`);
// Use context.redactedInputs to avoid leaking sensitive data
console.log(` Inputs:`, context.redactedInputs);
return null;
}
override after(
moduleId: string,
_inputs: Record<string, unknown>,
output: Record<string, unknown>,
context: Context,
): null {
console.log(`[${context.traceId}] ${moduleId} completed`);
console.log(` Output:`, output);
return null;
}
override onError(
moduleId: string,
_inputs: Record<string, unknown>,
error: Error,
context: Context,
): null {
console.error(`[${context.traceId}] ${moduleId} failed: ${error}`);
return null;
}
}
```
=== "Rust"
```rust
use apcore::context::Context;
use apcore::errors::ModuleError;
use apcore::middleware::Middleware;
use async_trait::async_trait;
use serde_json::Value;
#[derive(Debug)]
struct SimpleLoggingMiddleware;
#[async_trait]
impl Middleware for SimpleLoggingMiddleware {
fn name(&self) -> &str {
"simple_logging"
}
async fn before(
&self,
module_id: &str,
_inputs: Value,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
println!("[{}] Calling {}", ctx.trace_id, module_id);
// Use ctx.redacted_inputs to avoid leaking sensitive data
println!(" Inputs: {:?}", ctx.redacted_inputs);
Ok(None)
}
async fn after(
&self,
module_id: &str,
_inputs: Value,
output: Value,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
println!("[{}] {} completed", ctx.trace_id, module_id);
println!(" Output: {}", output);
Ok(None)
}
async fn on_error(
&self,
module_id: &str,
_inputs: Value,
error: &ModuleError,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
eprintln!("[{}] {} failed: {}", ctx.trace_id, module_id, error);
Ok(None)
}
}
```
=== "Python"
```python
from apcore import Registry, Executor
registry = Registry(extensions_dir="./extensions")
registry.discover()
executor = Executor(
registry=registry,
middlewares=[SimpleLoggingMiddleware()],
)
# Middleware automatically executes when calling modules
result = executor.call(
module_id="executor.email.send_email",
inputs={"to": "user@example.com", "subject": "Hi", "body": "Hello"},
)
```
=== "TypeScript"
```typescript
import { Registry, Executor } from 'apcore-js';
const registry = new Registry({ extensionsDir: './extensions' });
await registry.discover();
const executor = new Executor({
registry,
middlewares: [new SimpleLoggingMiddleware()],
});
// Middleware automatically executes when calling modules
const result = await executor.call(
'executor.email.send_email',
{ to: 'user@example.com', subject: 'Hi', body: 'Hello' },
);
```
=== "Rust"
```rust
use apcore::executor::Executor;
use apcore::registry::Registry;
use apcore::config::Config;
use serde_json::json;
use std::sync::Arc;
let registry = Arc::new(Registry::from_extensions_dir("./extensions")?);
let executor = Executor::new(registry, Config::default());
// Middleware automatically executes when calling modules
executor.use_middleware(Box::new(SimpleLoggingMiddleware))?;
let result = executor
.call(
"executor.email.send_email",
json!({ "to": "user@example.com", "subject": "Hi", "body": "Hello" }),
None,
)
.await?;
```
Output:
[abc-123] Calling executor.email.send_email
Inputs: {'to': 'user@example.com', 'subject': 'Hi', 'body': 'Hello'}
[abc-123] executor.email.send_email completed
Output: {'success': True, 'message_id': 'msg_456'}
Middleware executes in an onion model:
Request → MW1.before → MW2.before → MW3.before → Module execution
↓
Response ← MW1.after ← MW2.after ← MW3.after ← Output result
=== "Python"
```python
from apcore import Executor
executor = Executor(
registry=registry,
middlewares=[
MiddlewareA(), # First
MiddlewareB(), # Second
MiddlewareC(), # Third
],
)
```
=== "TypeScript"
```typescript
import { Executor } from 'apcore-js';
const executor = new Executor({
registry,
middlewares: [
new MiddlewareA(), // First
new MiddlewareB(), // Second
new MiddlewareC(), // Third
],
});
```
=== "Rust"
```rust
use apcore::executor::Executor;
use apcore::config::Config;
let executor = Executor::new(registry, Config::default());
// Add in registration order; equal priority preserves insertion order.
executor.use_middleware(Box::new(MiddlewareA))?; // First
executor.use_middleware(Box::new(MiddlewareB))?; // Second
executor.use_middleware(Box::new(MiddlewareC))?; // Third
```
Execution order:
1. MiddlewareA.before()
2. MiddlewareB.before()
3. MiddlewareC.before()
4. module.execute()
5. MiddlewareC.after()
6. MiddlewareB.after()
7. MiddlewareA.after()
Request → MW1.before → MW2.before → Module execution (Exception!)
↓
MW1.on_error ← MW2.on_error ← Exception propagation
If any on_error returns non-None, the exception is "swallowed" and that value is returned as the result.
┌──────┐ ┌────────────────┐ ┌──────────┐ ┌───────────────┐ ┌──────┐
│ init │───▶│ before (order) │───▶│ execute │───▶│ after (reverse)│───▶│ done │
└──────┘ └───────┬────────┘ └────┬─────┘ └───────┬───────┘ └──────┘
│ │ │
│ exception │ exception │ exception
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ skip rest│ │ on_error │ │ on_error │
│ go error │ │ (reverse)│ │ (reverse)│
└──────────┘ └──────────┘ └──────────┘
Precise exception handling semantics:
| Stage | Exception Behavior | Description |
|---|---|---|
before() stage |
Skip remaining before and execute, enter on_error chain | e.g., rate limit rejection |
execute() stage |
Enter on_error chain (reverse order) | Module execution failure |
after() stage |
Enter on_error chain (reverse order); remaining after hooks are skipped | After-stage errors should be handled uniformly |
on_error() stage |
Log error but should continue executing remaining on_error | Error handling should not fail again |
Thread safety requirements:
- Middleware instances can be shared by multiple Executors
- Middleware's
before()/after()/on_error()must be thread-safe - When using instance variables to store state, must use thread-safe data structures (each SDK provides its own primitives — e.g.,
threading.Lockin Python,Mutexin Rust) - Using
context.datato pass middleware state is the recommended thread-safe approach
=== "Python"
```python
import logging
from datetime import datetime
from apcore import Middleware, Context
class DetailedLoggingMiddleware(Middleware):
"""Detailed logging middleware"""
def __init__(self, logger: logging.Logger | None = None):
self.logger = logger or logging.getLogger("apcore")
self._start_times: dict[str, datetime] = {}
def before(self, module_id: str, inputs: dict, context: Context) -> None:
self._start_times[context.trace_id] = datetime.now()
# Security: use context.redacted_inputs to avoid leaking sensitive data
self.logger.info(
f"[{context.trace_id}] START {module_id}",
extra={
"trace_id": context.trace_id,
"module_id": module_id,
"caller_id": context.caller_id,
"inputs": context.redacted_inputs,
},
)
def after(self, module_id: str, inputs: dict, output: dict, context: Context) -> None:
start = self._start_times.pop(context.trace_id, datetime.now())
duration = (datetime.now() - start).total_seconds() * 1000
self.logger.info(
f"[{context.trace_id}] END {module_id} ({duration:.2f}ms)",
extra={
"trace_id": context.trace_id,
"module_id": module_id,
"duration_ms": duration,
"output": output,
},
)
def on_error(self, module_id: str, inputs: dict, error: Exception, context: Context) -> None:
# Security: use context.redacted_inputs instead of raw inputs
self.logger.error(
f"[{context.trace_id}] ERROR {module_id}: {error}",
extra={
"trace_id": context.trace_id,
"module_id": module_id,
"error": str(error),
"inputs": context.redacted_inputs,
},
exc_info=True,
)
```
=== "TypeScript"
```typescript
import { Middleware } from 'apcore-js';
import type { Context } from 'apcore-js';
interface Logger {
info(message: string, extra?: Record<string, unknown>): void;
error(message: string, extra?: Record<string, unknown>): void;
}
const defaultLogger: Logger = {
info: (msg, extra) => console.info(msg, extra ?? ''),
error: (msg, extra) => console.error(msg, extra ?? ''),
};
class DetailedLoggingMiddleware extends Middleware {
private readonly logger: Logger;
constructor(logger: Logger = defaultLogger) {
super();
this.logger = logger;
}
override before(
moduleId: string,
_inputs: Record<string, unknown>,
context: Context,
): null {
context.data['_apcore.mw.logging.start_time'] = performance.now();
// Security: use context.redactedInputs to avoid leaking sensitive data
this.logger.info(`[${context.traceId}] START ${moduleId}`, {
traceId: context.traceId,
moduleId,
callerId: context.callerId,
inputs: context.redactedInputs,
});
return null;
}
override after(
moduleId: string,
_inputs: Record<string, unknown>,
output: Record<string, unknown>,
context: Context,
): null {
const start = (context.data['_apcore.mw.logging.start_time'] as number) ?? performance.now();
const durationMs = performance.now() - start;
this.logger.info(
`[${context.traceId}] END ${moduleId} (${durationMs.toFixed(2)}ms)`,
{
traceId: context.traceId,
moduleId,
durationMs,
output,
},
);
return null;
}
override onError(
moduleId: string,
_inputs: Record<string, unknown>,
error: Error,
context: Context,
): null {
// Security: use context.redactedInputs instead of raw inputs
this.logger.error(`[${context.traceId}] ERROR ${moduleId}: ${error}`, {
traceId: context.traceId,
moduleId,
error: String(error),
inputs: context.redactedInputs,
});
return null;
}
}
```
=== "Rust"
```rust
use apcore::context::Context;
use apcore::errors::ModuleError;
use apcore::middleware::Middleware;
use async_trait::async_trait;
use parking_lot::Mutex;
use serde_json::Value;
use std::collections::HashMap;
use std::time::Instant;
#[derive(Debug, Default)]
struct DetailedLoggingMiddleware {
start_times: Mutex<HashMap<String, Instant>>,
}
#[async_trait]
impl Middleware for DetailedLoggingMiddleware {
fn name(&self) -> &str {
"detailed_logging"
}
async fn before(
&self,
module_id: &str,
_inputs: Value,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
self.start_times
.lock()
.insert(ctx.trace_id.to_string(), Instant::now());
// Security: use ctx.redacted_inputs to avoid leaking sensitive data
tracing::info!(
trace_id = %ctx.trace_id,
module_id = %module_id,
caller_id = ?ctx.caller_id(),
inputs = ?ctx.redacted_inputs,
"[{}] START {}",
ctx.trace_id,
module_id,
);
Ok(None)
}
async fn after(
&self,
module_id: &str,
_inputs: Value,
output: Value,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
let start = self
.start_times
.lock()
.remove(ctx.trace_id)
.unwrap_or_else(Instant::now);
let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
tracing::info!(
trace_id = %ctx.trace_id,
module_id = %module_id,
duration_ms = duration_ms,
output = ?output,
"[{}] END {} ({:.2}ms)",
ctx.trace_id,
module_id,
duration_ms,
);
Ok(None)
}
async fn on_error(
&self,
module_id: &str,
_inputs: Value,
error: &ModuleError,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
// Security: use ctx.redacted_inputs instead of raw inputs
tracing::error!(
trace_id = %ctx.trace_id,
module_id = %module_id,
error = %error,
inputs = ?ctx.redacted_inputs,
"[{}] ERROR {}: {}",
ctx.trace_id,
module_id,
error,
);
Ok(None)
}
}
```
=== "Python"
```python
import time
from apcore import Middleware, Context
class MetricsMiddleware(Middleware):
"""Performance metrics collection middleware"""
def __init__(self):
self.call_counts: dict[str, int] = {}
self.durations: dict[str, list[float]] = {}
self.error_counts: dict[str, int] = {}
self._start_times: dict[str, float] = {}
def before(self, module_id: str, inputs: dict, context: Context) -> None:
self._start_times[context.trace_id] = time.perf_counter()
self.call_counts[module_id] = self.call_counts.get(module_id, 0) + 1
def after(self, module_id: str, inputs: dict, output: dict, context: Context) -> None:
start = self._start_times.pop(context.trace_id, time.perf_counter())
duration = time.perf_counter() - start
self.durations.setdefault(module_id, []).append(duration)
def on_error(self, module_id: str, inputs: dict, error: Exception, context: Context) -> None:
self.error_counts[module_id] = self.error_counts.get(module_id, 0) + 1
def get_stats(self, module_id: str) -> dict:
"""Get module statistics"""
durations = self.durations.get(module_id, [])
return {
"call_count": self.call_counts.get(module_id, 0),
"error_count": self.error_counts.get(module_id, 0),
"avg_duration": sum(durations) / len(durations) if durations else 0,
"min_duration": min(durations) if durations else 0,
"max_duration": max(durations) if durations else 0,
}
```
=== "TypeScript"
```typescript
import { Middleware } from 'apcore-js';
import type { Context } from 'apcore-js';
interface Stats {
callCount: number;
errorCount: number;
avgDuration: number;
minDuration: number;
maxDuration: number;
}
class MetricsMiddleware extends Middleware {
private callCounts = new Map<string, number>();
private durations = new Map<string, number[]>();
private errorCounts = new Map<string, number>();
override before(
moduleId: string,
_inputs: Record<string, unknown>,
context: Context,
): null {
context.data['_apcore.mw.metrics.start_time'] = performance.now();
this.callCounts.set(moduleId, (this.callCounts.get(moduleId) ?? 0) + 1);
return null;
}
override after(
moduleId: string,
_inputs: Record<string, unknown>,
_output: Record<string, unknown>,
context: Context,
): null {
const start = (context.data['_apcore.mw.metrics.start_time'] as number) ?? performance.now();
const duration = (performance.now() - start) / 1000;
const list = this.durations.get(moduleId) ?? [];
list.push(duration);
this.durations.set(moduleId, list);
return null;
}
override onError(
moduleId: string,
_inputs: Record<string, unknown>,
_error: Error,
_context: Context,
): null {
this.errorCounts.set(moduleId, (this.errorCounts.get(moduleId) ?? 0) + 1);
return null;
}
getStats(moduleId: string): Stats {
const durations = this.durations.get(moduleId) ?? [];
const sum = durations.reduce((a, b) => a + b, 0);
return {
callCount: this.callCounts.get(moduleId) ?? 0,
errorCount: this.errorCounts.get(moduleId) ?? 0,
avgDuration: durations.length ? sum / durations.length : 0,
minDuration: durations.length ? Math.min(...durations) : 0,
maxDuration: durations.length ? Math.max(...durations) : 0,
};
}
}
```
=== "Rust"
```rust
use apcore::context::Context;
use apcore::errors::ModuleError;
use apcore::middleware::Middleware;
use async_trait::async_trait;
use parking_lot::Mutex;
use serde_json::Value;
use std::collections::HashMap;
use std::time::Instant;
#[derive(Debug, Default)]
struct MetricsState {
call_counts: HashMap<String, u64>,
durations: HashMap<String, Vec<f64>>,
error_counts: HashMap<String, u64>,
start_times: HashMap<String, Instant>,
}
#[derive(Debug, Default)]
struct MetricsMiddleware {
state: Mutex<MetricsState>,
}
#[derive(Debug)]
pub struct Stats {
pub call_count: u64,
pub error_count: u64,
pub avg_duration: f64,
pub min_duration: f64,
pub max_duration: f64,
}
impl MetricsMiddleware {
pub fn get_stats(&self, module_id: &str) -> Stats {
let s = self.state.lock();
let durations = s.durations.get(module_id).cloned().unwrap_or_default();
let sum: f64 = durations.iter().sum();
Stats {
call_count: *s.call_counts.get(module_id).unwrap_or(&0),
error_count: *s.error_counts.get(module_id).unwrap_or(&0),
avg_duration: if durations.is_empty() {
0.0
} else {
sum / durations.len() as f64
},
min_duration: if durations.is_empty() {
0.0
} else {
durations.iter().copied().fold(f64::INFINITY, f64::min)
},
max_duration: if durations.is_empty() {
0.0
} else {
durations.iter().copied().fold(f64::NEG_INFINITY, f64::max)
},
}
}
}
#[async_trait]
impl Middleware for MetricsMiddleware {
fn name(&self) -> &str {
"metrics"
}
async fn before(
&self,
module_id: &str,
_inputs: Value,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
let mut s = self.state.lock();
s.start_times
.insert(ctx.trace_id.to_string(), Instant::now());
*s.call_counts.entry(module_id.to_string()).or_insert(0) += 1;
Ok(None)
}
async fn after(
&self,
module_id: &str,
_inputs: Value,
_output: Value,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
let mut s = self.state.lock();
let start = s
.start_times
.remove(ctx.trace_id)
.unwrap_or_else(Instant::now);
let duration = start.elapsed().as_secs_f64();
s.durations
.entry(module_id.to_string())
.or_default()
.push(duration);
Ok(None)
}
async fn on_error(
&self,
module_id: &str,
_inputs: Value,
_error: &ModuleError,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
let mut s = self.state.lock();
*s.error_counts.entry(module_id.to_string()).or_insert(0) += 1;
Ok(None)
}
}
```
!!! note "Returning cached output from before()"
Returning a non-None value from before() modifies the inputs that the
module will receive. To short-circuit execution and return a cached
output as the final result, use a custom pipeline step or signal it via
context.data and let an outer wrapper handle it. The example below
illustrates the timing/key-derivation pattern; production caching should
use a dedicated step or a recovery in on_error.
=== "Python"
```python
import hashlib
import json
import time
from apcore import Middleware, Context
class CacheMiddleware(Middleware):
"""Simple cache middleware (illustrative)"""
def __init__(self, ttl_seconds: int = 300):
self.cache: dict[str, tuple[dict, float]] = {}
self.ttl = ttl_seconds
def _cache_key(self, module_id: str, inputs: dict) -> str:
content = json.dumps({"module_id": module_id, "inputs": inputs}, sort_keys=True)
return hashlib.md5(content.encode()).hexdigest()
def before(self, module_id: str, inputs: dict, context: Context) -> dict | None:
key = self._cache_key(module_id, inputs)
if key in self.cache:
cached_output, cached_time = self.cache[key]
if time.time() - cached_time < self.ttl:
# Cache hit; record for after() and downstream consumers.
context.data["_apcore.mw.cache.hit"] = True
context.data["_apcore.mw.cache.value"] = cached_output
context.data["_apcore.mw.cache.key"] = key
return None
def after(self, module_id: str, inputs: dict, output: dict, context: Context) -> None:
if context.data.get("_apcore.mw.cache.hit"):
return
key = context.data.get("_apcore.mw.cache.key")
if key:
self.cache[key] = (output, time.time())
```
=== "TypeScript"
```typescript
import { createHash } from 'node:crypto';
import { Middleware } from 'apcore-js';
import type { Context } from 'apcore-js';
interface CacheEntry {
output: Record<string, unknown>;
time: number;
}
class CacheMiddleware extends Middleware {
private cache = new Map<string, CacheEntry>();
constructor(private readonly ttlSeconds: number = 300) {
super();
}
private cacheKey(moduleId: string, inputs: Record<string, unknown>): string {
const content = JSON.stringify({ moduleId, inputs }, Object.keys({ moduleId, inputs }).sort());
return createHash('md5').update(content).digest('hex');
}
override before(
moduleId: string,
inputs: Record<string, unknown>,
context: Context,
): null {
const key = this.cacheKey(moduleId, inputs);
const entry = this.cache.get(key);
if (entry && Date.now() / 1000 - entry.time < this.ttlSeconds) {
context.data['_apcore.mw.cache.hit'] = true;
context.data['_apcore.mw.cache.value'] = entry.output;
}
context.data['_apcore.mw.cache.key'] = key;
return null;
}
override after(
_moduleId: string,
_inputs: Record<string, unknown>,
output: Record<string, unknown>,
context: Context,
): null {
if (context.data['_apcore.mw.cache.hit']) return null;
const key = context.data['_apcore.mw.cache.key'] as string | undefined;
if (key) {
this.cache.set(key, { output, time: Date.now() / 1000 });
}
return null;
}
}
```
=== "Rust"
```rust
use apcore::context::Context;
use apcore::errors::ModuleError;
use apcore::middleware::Middleware;
use async_trait::async_trait;
use md5::{Digest, Md5};
use parking_lot::Mutex;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug)]
struct CacheMiddleware {
cache: Mutex<HashMap<String, (Value, f64)>>,
ttl_seconds: f64,
}
impl CacheMiddleware {
pub fn new(ttl_seconds: f64) -> Self {
Self {
cache: Mutex::new(HashMap::new()),
ttl_seconds,
}
}
fn cache_key(module_id: &str, inputs: &Value) -> String {
let canonical = json!({ "module_id": module_id, "inputs": inputs }).to_string();
let mut hasher = Md5::new();
hasher.update(canonical.as_bytes());
format!("{:x}", hasher.finalize())
}
fn now_secs() -> f64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64()
}
}
#[async_trait]
impl Middleware for CacheMiddleware {
fn name(&self) -> &str {
"cache"
}
async fn before(
&self,
module_id: &str,
inputs: Value,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
let key = Self::cache_key(module_id, &inputs);
if let Some((cached_output, cached_time)) = self.cache.lock().get(&key).cloned() {
if Self::now_secs() - cached_time < self.ttl_seconds {
ctx.data.insert("_apcore.mw.cache.hit", json!(true));
ctx.data.insert("_apcore.mw.cache.value", cached_output);
}
}
ctx.data.insert("_apcore.mw.cache.key", json!(key));
Ok(None)
}
async fn after(
&self,
_module_id: &str,
_inputs: Value,
output: Value,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
if ctx.data.get("_apcore.mw.cache.hit").is_some() {
return Ok(None);
}
if let Some(Value::String(key)) = ctx.data.get("_apcore.mw.cache.key") {
self.cache.lock().insert(key, (output, Self::now_secs()));
}
Ok(None)
}
async fn on_error(
&self,
_module_id: &str,
_inputs: Value,
_error: &ModuleError,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
Ok(None)
}
}
```
apcore provides a built-in RetryMiddleware with configurable backoff strategies. It only retries errors marked retryable=True.
=== "Python"
```python
from apcore import RetryMiddleware, RetryConfig
# Default: 3 retries, exponential backoff, jitter enabled
executor.use(RetryMiddleware())
# Custom configuration
executor.use(RetryMiddleware(RetryConfig(
max_retries=5,
strategy="fixed", # "exponential" (default) or "fixed"
base_delay_ms=200, # Base delay between retries (default: 100)
max_delay_ms=10000, # Cap for exponential growth (default: 5000)
jitter=True, # Add 0.5-1.5x random multiplier (default: True)
)))
```
=== "TypeScript"
```typescript
import { RetryHintMiddleware } from 'apcore-js';
// Default: 3 retries, exponential backoff, jitter enabled.
// Note: in apcore-js this middleware writes retry hints to context.data
// (CTX_RETRY_COUNT_PREFIX / CTX_RETRY_DELAY_PREFIX) for an outer retry
// loop; the error still propagates so the caller controls the retry.
executor.use(new RetryHintMiddleware());
// Custom configuration
executor.use(new RetryHintMiddleware({
maxRetries: 5,
strategy: 'fixed', // 'exponential' (default) or 'fixed'
baseDelayMs: 200, // Base delay between retries (default: 100)
maxDelayMs: 10000, // Cap for exponential growth (default: 5000)
jitter: true, // Add 0.5-1.5x random multiplier (default: true)
}));
```
=== "Rust"
<!-- apcore-example: fragment -->
```rust
use apcore::middleware::{RetryConfig, RetryMiddleware};
// Default: 3 retries, exponential backoff, jitter enabled
executor.use_middleware(Box::new(RetryMiddleware::new(RetryConfig::default())))?;
// Custom configuration. `RetryConfig` is `#[non_exhaustive]`, so a downstream
// crate builds it from `Default::default()` and assigns fields — a struct
// literal, with or without `..RetryConfig::default()`, is E0639. See
// spec/api-surface-conventions.md §9.1.
let mut config = RetryConfig::default();
config.max_retries = 5;
config.strategy = "fixed".to_string(); // "exponential" (default) or "fixed"
config.base_delay_ms = 200; // Base delay between retries (default: 100)
config.max_delay_ms = 10_000; // Cap for exponential growth (default: 5000)
config.jitter = true; // Add 0.5-1.5x random multiplier (default: true)
executor.use_middleware(Box::new(RetryMiddleware::new(config)))?;
```
RetryConfig fields:
| Field | Type | Default | Description |
|---|---|---|---|
max_retries |
int | 3 | Maximum retry attempts |
strategy |
str | "exponential" |
"exponential" (base × 2^attempt) or "fixed" (constant delay) |
base_delay_ms |
int | 100 | Base delay in milliseconds |
max_delay_ms |
int | 5000 | Maximum delay cap (exponential only) |
jitter |
bool | True | Add random 0.5–1.5x multiplier to prevent thundering herd |
Retry logic:
- Only retries errors where
error.retryable is True(each error code has a default, see PROTOCOL_SPEC §8.6). - Tracks retry count per module in
context.datausing key_retry_count_{module_id}. - In async pipelines,
on_erroris dispatched to a worker thread so the event loop is not blocked.
Records ModuleError instances into ErrorHistory for health monitoring. Automatically registered by register_sys_modules(). See Observability.
Records call counts and latency into UsageCollector for usage analytics. Automatically registered by register_sys_modules(). See Observability.
Emits events when module error rates or p99 latency exceed configured thresholds. Automatically registered when sys_modules.events.enabled: true. See Event System.
=== "Python"
```python
import time
from collections import deque
from apcore import Middleware, Context, ModuleError
class RateLimitMiddleware(Middleware):
"""Rate limiting middleware"""
def __init__(self, max_calls: int = 100, window_seconds: int = 60):
self.max_calls = max_calls
self.window = window_seconds
self._call_times: dict[str, deque] = {}
def before(self, module_id: str, inputs: dict, context: Context) -> None:
now = time.time()
calls = self._call_times.setdefault(module_id, deque())
# Remove calls outside the window
while calls and calls[0] < now - self.window:
calls.popleft()
# Check if limit is exceeded
if len(calls) >= self.max_calls:
raise ModuleError(
f"Rate limit exceeded for {module_id}",
module_id=module_id,
trace_id=context.trace_id,
)
# Record current call
calls.append(now)
```
=== "TypeScript"
```typescript
import { Middleware, ModuleError } from 'apcore-js';
import type { Context } from 'apcore-js';
class RateLimitMiddleware extends Middleware {
private readonly callTimes = new Map<string, number[]>();
constructor(
private readonly maxCalls: number = 100,
private readonly windowSeconds: number = 60,
) {
super();
}
override before(
moduleId: string,
_inputs: Record<string, unknown>,
context: Context,
): null {
const now = Date.now() / 1000;
let calls = this.callTimes.get(moduleId);
if (!calls) {
calls = [];
this.callTimes.set(moduleId, calls);
}
// Remove calls outside the window
while (calls.length > 0 && calls[0]! < now - this.windowSeconds) {
calls.shift();
}
// Check if limit is exceeded
if (calls.length >= this.maxCalls) {
throw new ModuleError(`Rate limit exceeded for ${moduleId}`, {
moduleId,
traceId: context.traceId,
});
}
calls.push(now);
return null;
}
}
```
=== "Rust"
```rust
use apcore::context::Context;
use apcore::errors::{ErrorCode, ModuleError};
use apcore::middleware::Middleware;
use async_trait::async_trait;
use parking_lot::Mutex;
use serde_json::Value;
use std::collections::{HashMap, VecDeque};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug)]
struct RateLimitMiddleware {
max_calls: usize,
window_seconds: f64,
call_times: Mutex<HashMap<String, VecDeque<f64>>>,
}
impl RateLimitMiddleware {
pub fn new(max_calls: usize, window_seconds: f64) -> Self {
Self {
max_calls,
window_seconds,
call_times: Mutex::new(HashMap::new()),
}
}
fn now_secs() -> f64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64()
}
}
#[async_trait]
impl Middleware for RateLimitMiddleware {
fn name(&self) -> &str {
"rate_limit"
}
async fn before(
&self,
module_id: &str,
_inputs: Value,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
let now = Self::now_secs();
let mut map = self.call_times.lock();
let calls = map.entry(module_id.to_string()).or_default();
// Remove calls outside the window
while let Some(&front) = calls.front() {
if front < now - self.window_seconds {
calls.pop_front();
} else {
break;
}
}
if calls.len() >= self.max_calls {
return Err(ModuleError::new(
ErrorCode::ModuleExecuteError,
format!("Rate limit exceeded for {}", module_id),
));
}
calls.push_back(now);
Ok(None)
}
async fn after(
&self,
_module_id: &str,
_inputs: Value,
_output: Value,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
Ok(None)
}
async fn on_error(
&self,
_module_id: &str,
_inputs: Value,
_error: &ModuleError,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
Ok(None)
}
}
```
=== "Python"
```python
from apcore import Middleware, Context
class InputTransformMiddleware(Middleware):
"""Input transformation middleware"""
def before(self, module_id: str, inputs: dict, context: Context) -> dict:
modified = inputs.copy()
# Add default values
if "timeout" not in modified:
modified["timeout"] = 30
# Add trace information
modified["_trace_id"] = context.trace_id
return modified
```
=== "TypeScript"
```typescript
import { Middleware } from 'apcore-js';
import type { Context } from 'apcore-js';
class InputTransformMiddleware extends Middleware {
override before(
_moduleId: string,
inputs: Record<string, unknown>,
context: Context,
): Record<string, unknown> {
const modified = { ...inputs };
// Add default values
if (!('timeout' in modified)) {
modified['timeout'] = 30;
}
// Add trace information
modified['_trace_id'] = context.traceId;
return modified;
}
}
```
=== "Rust"
```rust
use apcore::context::Context;
use apcore::errors::ModuleError;
use apcore::middleware::Middleware;
use async_trait::async_trait;
use serde_json::{json, Value};
#[derive(Debug)]
struct InputTransformMiddleware;
#[async_trait]
impl Middleware for InputTransformMiddleware {
fn name(&self) -> &str {
"input_transform"
}
async fn before(
&self,
_module_id: &str,
inputs: Value,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
let mut modified = inputs.clone();
if let Some(obj) = modified.as_object_mut() {
obj.entry("timeout".to_string()).or_insert(json!(30));
obj.insert("_trace_id".to_string(), json!(ctx.trace_id));
}
Ok(Some(modified))
}
async fn after(
&self,
_module_id: &str,
_inputs: Value,
_output: Value,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
Ok(None)
}
async fn on_error(
&self,
_module_id: &str,
_inputs: Value,
_error: &ModuleError,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
Ok(None)
}
}
```
=== "Python"
```python
from datetime import datetime
from apcore import Middleware, Context
class OutputEnrichMiddleware(Middleware):
"""Output enrichment middleware"""
def after(self, module_id: str, inputs: dict, output: dict, context: Context) -> dict:
enriched = output.copy()
enriched["_metadata"] = {
"module_id": module_id,
"trace_id": context.trace_id,
"timestamp": datetime.now().isoformat(),
}
return enriched
```
=== "TypeScript"
```typescript
import { Middleware } from 'apcore-js';
import type { Context } from 'apcore-js';
class OutputEnrichMiddleware extends Middleware {
override after(
moduleId: string,
_inputs: Record<string, unknown>,
output: Record<string, unknown>,
context: Context,
): Record<string, unknown> {
return {
...output,
_metadata: {
moduleId,
traceId: context.traceId,
timestamp: new Date().toISOString(),
},
};
}
}
```
=== "Rust"
```rust
use apcore::context::Context;
use apcore::errors::ModuleError;
use apcore::middleware::Middleware;
use async_trait::async_trait;
use chrono::Utc;
use serde_json::{json, Value};
#[derive(Debug)]
struct OutputEnrichMiddleware;
#[async_trait]
impl Middleware for OutputEnrichMiddleware {
fn name(&self) -> &str {
"output_enrich"
}
async fn before(
&self,
_module_id: &str,
_inputs: Value,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
Ok(None)
}
async fn after(
&self,
module_id: &str,
_inputs: Value,
output: Value,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
let mut enriched = output.clone();
if let Some(obj) = enriched.as_object_mut() {
obj.insert(
"_metadata".to_string(),
json!({
"module_id": module_id,
"trace_id": ctx.trace_id,
"timestamp": Utc::now().to_rfc3339(),
}),
);
}
Ok(Some(enriched))
}
async fn on_error(
&self,
_module_id: &str,
_inputs: Value,
_error: &ModuleError,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
Ok(None)
}
}
```
=== "Python"
```python
from apcore import Middleware, Context
class FallbackMiddleware(Middleware):
"""Fallback middleware"""
def __init__(self, fallback_values: dict[str, dict]):
"""
Args:
fallback_values: {module_id: default_output}
"""
self.fallback_values = fallback_values
def on_error(
self,
module_id: str,
inputs: dict,
error: Exception,
context: Context,
) -> dict | None:
if module_id in self.fallback_values:
# Return fallback value, "swallow" exception
return self.fallback_values[module_id]
return None # Continue raising exception
```
=== "TypeScript"
```typescript
import { Middleware } from 'apcore-js';
import type { Context } from 'apcore-js';
class FallbackMiddleware extends Middleware {
constructor(
private readonly fallbackValues: Record<string, Record<string, unknown>>,
) {
super();
}
override onError(
moduleId: string,
_inputs: Record<string, unknown>,
_error: Error,
_context: Context,
): Record<string, unknown> | null {
const fallback = this.fallbackValues[moduleId];
if (fallback !== undefined) {
// Return fallback value, "swallow" the error
return fallback;
}
return null; // Let the error propagate
}
}
```
=== "Rust"
```rust
use apcore::context::Context;
use apcore::errors::ModuleError;
use apcore::middleware::Middleware;
use async_trait::async_trait;
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug)]
struct FallbackMiddleware {
fallback_values: HashMap<String, Value>,
}
impl FallbackMiddleware {
pub fn new(fallback_values: HashMap<String, Value>) -> Self {
Self { fallback_values }
}
}
#[async_trait]
impl Middleware for FallbackMiddleware {
fn name(&self) -> &str {
"fallback"
}
async fn before(
&self,
_module_id: &str,
_inputs: Value,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
Ok(None)
}
async fn after(
&self,
_module_id: &str,
_inputs: Value,
_output: Value,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
Ok(None)
}
async fn on_error(
&self,
module_id: &str,
_inputs: Value,
_error: &ModuleError,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
// Some(value) returns fallback and swallows the error;
// None lets the error propagate.
Ok(self.fallback_values.get(module_id).cloned())
}
}
```
In addition to class-based middleware, you can directly register functions using use_before() / use_after() (Python/Rust) or useBefore() / useAfter() (TypeScript):
=== "Python"
```python
from apcore import Executor, Context
executor = Executor(registry=registry)
# Register before hook
executor.use_before(
lambda module_id, inputs, ctx: print(f"Calling {module_id}")
)
# Register after hook
executor.use_after(
lambda module_id, inputs, output, ctx: print(f"Done {module_id}")
)
# Can also use regular functions
def log_before(module_id: str, inputs: dict, context: Context) -> None:
context.logger.info(f"START {module_id}")
def log_after(module_id: str, inputs: dict, output: dict, context: Context) -> None:
context.logger.info(f"END {module_id}")
executor.use_before(log_before)
executor.use_after(log_after)
```
=== "TypeScript"
```typescript
import { Executor } from 'apcore-js';
import type { Context } from 'apcore-js';
const executor = new Executor({ registry });
// Register before hook
executor.useBefore((moduleId, _inputs, _ctx) => {
console.log(`Calling ${moduleId}`);
return null;
});
// Register after hook
executor.useAfter((moduleId, _inputs, _output, _ctx) => {
console.log(`Done ${moduleId}`);
return null;
});
// Can also use regular functions
function logBefore(
moduleId: string,
_inputs: Record<string, unknown>,
_context: Context,
) {
console.info(`START ${moduleId}`);
return null;
}
function logAfter(
moduleId: string,
_inputs: Record<string, unknown>,
_output: Record<string, unknown>,
_context: Context,
) {
console.info(`END ${moduleId}`);
return null;
}
executor.useBefore(logBefore);
executor.useAfter(logAfter);
```
=== "Rust"
```rust
use apcore::context::Context;
use apcore::errors::ModuleError;
use apcore::middleware::adapters::{BeforeAdapter, AfterAdapter};
use serde_json::Value;
// Register a before hook with an async closure adapter
executor.use_middleware(Box::new(BeforeAdapter::new(
"log_before",
|module_id: String, _inputs: Value, _ctx: Context<Value>| async move {
println!("Calling {}", module_id);
Ok::<Option<Value>, ModuleError>(None)
},
)))?;
// Register an after hook with an async closure adapter
executor.use_middleware(Box::new(AfterAdapter::new(
"log_after",
|module_id: String, _inputs: Value, _output: Value, _ctx: Context<Value>| async move {
println!("Done {}", module_id);
Ok::<Option<Value>, ModuleError>(None)
},
)))?;
```
Use cases:
| Approach | Use Case |
|---|---|
Class-based (Middleware) |
Need on_error, need shared state, complex logic |
Function-first (use_before/use_after) |
Simple hooks, rapid prototyping, single concern |
=== "Python"
```python
from apcore import Executor, RetryMiddleware, RetryConfig
executor = Executor(registry=registry)
# Chain middleware
executor \
.use(LoggingMiddleware()) \
.use(MetricsMiddleware()) \
.use(CacheMiddleware(ttl_seconds=60)) \
.use(RetryMiddleware(RetryConfig(max_retries=3)))
```
=== "TypeScript"
```typescript
import { Executor, RetryHintMiddleware } from 'apcore-js';
const executor = new Executor({ registry });
// Chain middleware
executor
.use(new LoggingMiddleware())
.use(new MetricsMiddleware())
.use(new CacheMiddleware(60))
.use(new RetryHintMiddleware({ maxRetries: 3 }));
```
=== "Rust"
<!-- apcore-example: fragment -->
```rust
use apcore::executor::Executor;
use apcore::middleware::{RetryConfig, RetryMiddleware};
// Chain middleware (Rust uses sequential calls — `use_middleware`
// returns `Result<MiddlewareHandle, ModuleError>`, not `&Self`; the handle
// identifies exactly this registration for `remove_handle`, and `?;`
// discards it when you do not need removal).
executor.use_middleware(Box::new(LoggingMiddleware::default()))?;
executor.use_middleware(Box::new(MetricsMiddleware::default()))?;
executor.use_middleware(Box::new(CacheMiddleware::new(60.0)))?;
// `RetryConfig` is `#[non_exhaustive]` — see spec/api-surface-conventions.md §9.1.
let mut retry = RetryConfig::default();
retry.max_retries = 3;
executor.use_middleware(Box::new(RetryMiddleware::new(retry)))?;
```
=== "Python"
```python
import fnmatch
from apcore import Middleware, Context
class ConditionalMiddleware(Middleware):
"""Conditional middleware: only applies to specific modules"""
def __init__(self, inner: Middleware, pattern: str):
self.inner = inner
self.pattern = pattern
def _matches(self, module_id: str) -> bool:
return fnmatch.fnmatch(module_id, self.pattern)
def before(self, module_id: str, inputs: dict, context: Context):
if self._matches(module_id):
return self.inner.before(module_id, inputs, context)
return None
def after(self, module_id: str, inputs: dict, output: dict, context: Context):
if self._matches(module_id):
return self.inner.after(module_id, inputs, output, context)
return None
def on_error(self, module_id: str, inputs: dict, error: Exception, context: Context):
if self._matches(module_id):
return self.inner.on_error(module_id, inputs, error, context)
return None
# Usage: enable cache only for executor.* modules
executor.use(
ConditionalMiddleware(CacheMiddleware(), "executor.*")
)
```
=== "TypeScript"
```typescript
import { Middleware } from 'apcore-js';
import type { Context } from 'apcore-js';
function fnmatch(name: string, pattern: string): boolean {
const escaped = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
return new RegExp(`^${escaped}$`).test(name);
}
class ConditionalMiddleware extends Middleware {
constructor(
private readonly inner: Middleware,
private readonly pattern: string,
) {
super();
}
private matches(moduleId: string): boolean {
return fnmatch(moduleId, this.pattern);
}
override before(
moduleId: string,
inputs: Record<string, unknown>,
context: Context,
) {
if (this.matches(moduleId)) {
return this.inner.before(moduleId, inputs, context);
}
return null;
}
override after(
moduleId: string,
inputs: Record<string, unknown>,
output: Record<string, unknown>,
context: Context,
) {
if (this.matches(moduleId)) {
return this.inner.after(moduleId, inputs, output, context);
}
return null;
}
override onError(
moduleId: string,
inputs: Record<string, unknown>,
error: Error,
context: Context,
) {
if (this.matches(moduleId)) {
return this.inner.onError(moduleId, inputs, error, context);
}
return null;
}
}
// Usage: enable cache only for executor.* modules
executor.use(new ConditionalMiddleware(new CacheMiddleware(60), 'executor.*'));
```
=== "Rust"
```rust
use apcore::context::Context;
use apcore::errors::ModuleError;
use apcore::middleware::Middleware;
use async_trait::async_trait;
use serde_json::Value;
/// Glob match supporting `*` and `?` (sufficient for module-id patterns).
fn fnmatch(name: &str, pattern: &str) -> bool {
let mut re = String::with_capacity(pattern.len() + 4);
re.push('^');
for c in pattern.chars() {
match c {
'*' => re.push_str(".*"),
'?' => re.push('.'),
'.' | '+' | '(' | ')' | '|' | '[' | ']' | '{' | '}' | '\\' | '^' | '$' => {
re.push('\\');
re.push(c);
}
_ => re.push(c),
}
}
re.push('$');
regex::Regex::new(&re).map(|r| r.is_match(name)).unwrap_or(false)
}
#[derive(Debug)]
struct ConditionalMiddleware {
inner: Box<dyn Middleware>,
pattern: String,
}
impl ConditionalMiddleware {
pub fn new(inner: Box<dyn Middleware>, pattern: impl Into<String>) -> Self {
Self {
inner,
pattern: pattern.into(),
}
}
fn matches(&self, module_id: &str) -> bool {
fnmatch(module_id, &self.pattern)
}
}
#[async_trait]
impl Middleware for ConditionalMiddleware {
fn name(&self) -> &str {
"conditional"
}
async fn before(
&self,
module_id: &str,
inputs: Value,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
if self.matches(module_id) {
self.inner.before(module_id, inputs, ctx).await
} else {
Ok(None)
}
}
async fn after(
&self,
module_id: &str,
inputs: Value,
output: Value,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
if self.matches(module_id) {
self.inner.after(module_id, inputs, output, ctx).await
} else {
Ok(None)
}
}
async fn on_error(
&self,
module_id: &str,
inputs: Value,
error: &ModuleError,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
if self.matches(module_id) {
self.inner.on_error(module_id, inputs, error, ctx).await
} else {
Ok(None)
}
}
}
```
=== "Python"
```python
from apcore import Middleware, Context
class MiddlewareGroup(Middleware):
"""Middleware group: use multiple middleware as one"""
def __init__(self, middlewares: list[Middleware]):
self.middlewares = middlewares
def before(self, module_id: str, inputs: dict, context: Context):
result = inputs
for mw in self.middlewares:
new_result = mw.before(module_id, result, context)
if new_result is not None:
result = new_result
return result if result is not inputs else None
def after(self, module_id: str, inputs: dict, output: dict, context: Context):
result = output
for mw in reversed(self.middlewares):
new_result = mw.after(module_id, inputs, result, context)
if new_result is not None:
result = new_result
return result if result is not output else None
# Usage
production_middlewares = MiddlewareGroup([
LoggingMiddleware(),
MetricsMiddleware(),
RateLimitMiddleware(),
])
executor.use(production_middlewares)
```
=== "TypeScript"
```typescript
import { Middleware } from 'apcore-js';
import type { Context } from 'apcore-js';
class MiddlewareGroup extends Middleware {
constructor(private readonly middlewares: Middleware[]) {
super();
}
override async before(
moduleId: string,
inputs: Record<string, unknown>,
context: Context,
): Promise<Record<string, unknown> | null> {
let result: Record<string, unknown> = inputs;
let modified = false;
for (const mw of this.middlewares) {
const next = await mw.before(moduleId, result, context);
if (next != null) {
result = next;
modified = true;
}
}
return modified ? result : null;
}
override async after(
moduleId: string,
inputs: Record<string, unknown>,
output: Record<string, unknown>,
context: Context,
): Promise<Record<string, unknown> | null> {
let result: Record<string, unknown> = output;
let modified = false;
for (const mw of [...this.middlewares].reverse()) {
const next = await mw.after(moduleId, inputs, result, context);
if (next != null) {
result = next;
modified = true;
}
}
return modified ? result : null;
}
}
// Usage
const productionMiddlewares = new MiddlewareGroup([
new LoggingMiddleware(),
new MetricsMiddleware(),
new RateLimitMiddleware(),
]);
executor.use(productionMiddlewares);
```
=== "Rust"
```rust
use apcore::context::Context;
use apcore::errors::ModuleError;
use apcore::middleware::Middleware;
use async_trait::async_trait;
use serde_json::Value;
#[derive(Debug)]
struct MiddlewareGroup {
middlewares: Vec<Box<dyn Middleware>>,
}
impl MiddlewareGroup {
pub fn new(middlewares: Vec<Box<dyn Middleware>>) -> Self {
Self { middlewares }
}
}
#[async_trait]
impl Middleware for MiddlewareGroup {
fn name(&self) -> &str {
"middleware_group"
}
async fn before(
&self,
module_id: &str,
inputs: Value,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
let mut result = inputs;
let mut modified = false;
for mw in &self.middlewares {
if let Some(next) = mw.before(module_id, result.clone(), ctx).await? {
result = next;
modified = true;
}
}
Ok(if modified { Some(result) } else { None })
}
async fn after(
&self,
module_id: &str,
inputs: Value,
output: Value,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
let mut result = output;
let mut modified = false;
for mw in self.middlewares.iter().rev() {
if let Some(next) = mw
.after(module_id, inputs.clone(), result.clone(), ctx)
.await?
{
result = next;
modified = true;
}
}
Ok(if modified { Some(result) } else { None })
}
async fn on_error(
&self,
_module_id: &str,
_inputs: Value,
_error: &ModuleError,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
Ok(None)
}
}
// Usage
let production_middlewares = MiddlewareGroup::new(vec![
Box::new(LoggingMiddleware::default()),
Box::new(MetricsMiddleware::default()),
Box::new(RateLimitMiddleware::new(100, 60.0)),
]);
executor.use_middleware(Box::new(production_middlewares))?;
```
The standard Middleware base class supports async before(), after(), and on_error() methods. There is no separate AsyncMiddleware class — simply define async methods on a regular Middleware subclass. In Rust, all hooks are already async by trait definition.
=== "Python"
```python
from apcore.middleware import Middleware
from apcore.context import Context
class AsyncLoggingMiddleware(Middleware):
"""Middleware with async hooks"""
async def before(self, module_id: str, inputs: dict, context: Context) -> None:
await self._async_log(f"Calling {module_id}")
async def after(self, module_id: str, inputs: dict, output: dict, context: Context) -> None:
await self._async_log(f"Completed {module_id}")
async def _async_log(self, message: str) -> None:
import aiohttp
async with aiohttp.ClientSession() as session:
await session.post(
"https://logs.example.com/api/log",
json={"message": message},
)
```
=== "TypeScript"
```typescript
import { Middleware } from 'apcore-js';
import type { Context } from 'apcore-js';
class AsyncLoggingMiddleware extends Middleware {
override async before(
moduleId: string,
_inputs: Record<string, unknown>,
_context: Context,
): Promise<null> {
await this.asyncLog(`Calling ${moduleId}`);
return null;
}
override async after(
moduleId: string,
_inputs: Record<string, unknown>,
_output: Record<string, unknown>,
_context: Context,
): Promise<null> {
await this.asyncLog(`Completed ${moduleId}`);
return null;
}
private async asyncLog(message: string): Promise<void> {
await fetch('https://logs.example.com/api/log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }),
});
}
}
```
=== "Rust"
```rust
use apcore::context::Context;
use apcore::errors::ModuleError;
use apcore::middleware::Middleware;
use async_trait::async_trait;
use serde_json::{json, Value};
#[derive(Debug)]
struct AsyncLoggingMiddleware {
client: reqwest::Client,
}
impl AsyncLoggingMiddleware {
pub fn new() -> Self {
Self {
client: reqwest::Client::new(),
}
}
async fn async_log(&self, message: &str) -> Result<(), reqwest::Error> {
self.client
.post("https://logs.example.com/api/log")
.json(&json!({ "message": message }))
.send()
.await?;
Ok(())
}
}
#[async_trait]
impl Middleware for AsyncLoggingMiddleware {
fn name(&self) -> &str {
"async_logging"
}
async fn before(
&self,
module_id: &str,
_inputs: Value,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
let _ = self.async_log(&format!("Calling {}", module_id)).await;
Ok(None)
}
async fn after(
&self,
module_id: &str,
_inputs: Value,
_output: Value,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
let _ = self.async_log(&format!("Completed {}", module_id)).await;
Ok(None)
}
async fn on_error(
&self,
_module_id: &str,
_inputs: Value,
_error: &ModuleError,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
Ok(None)
}
}
```
When middleware records logs, it is forbidden to directly log raw inputs parameters. Must use context.redacted_inputs (Python/Rust) or context.redactedInputs (TypeScript), which is automatically generated by Executor based on x-sensitive: true markers in the Schema.
=== "Python"
```python
# Dangerous: may leak passwords, API keys, and other sensitive data
self.logger.info(f"inputs: {inputs}")
# Safe: sensitive fields are replaced with ***REDACTED***
self.logger.info(f"inputs: {context.redacted_inputs}")
```
=== "TypeScript"
```typescript
// Dangerous: may leak passwords, API keys, and other sensitive data
this.logger.info('inputs:', inputs);
// Safe: sensitive fields are replaced with ***REDACTED***
this.logger.info('inputs:', context.redactedInputs);
```
=== "Rust"
```rust
// Dangerous: may leak passwords, API keys, and other sensitive data
tracing::info!(inputs = ?inputs, "inputs logged");
// Safe: sensitive fields are replaced with ***REDACTED***
tracing::info!(inputs = ?ctx.redacted_inputs, "inputs logged");
```
When passing middleware state via context.data, if it contains sensitive information, use the _secret_ prefix:
=== "Python"
```python
# Set sensitive data
context.data["_secret_auth_token"] = "Bearer sk-..."
# Framework guarantee: keys starting with _secret_ are automatically
# redacted in log serialization.
```
=== "TypeScript"
```typescript
// Set sensitive data
context.data['_secret_auth_token'] = 'Bearer sk-...';
// Framework guarantee: keys starting with _secret_ are automatically
// redacted in log serialization.
```
=== "Rust"
```rust
use serde_json::json;
// Set sensitive data
ctx.data.insert("_secret_auth_token", json!("Bearer sk-..."));
// Framework guarantee: keys starting with _secret_ are automatically
// redacted in log serialization.
```
| Layer | Mechanism | Description |
|---|---|---|
| L1 | Schema x-sensitive: true |
Mark sensitive fields in Schema |
| L2 | Executor auto-generates redacted_inputs |
Based on A13 redact_sensitive() algorithm |
| L3 | _secret_ prefix convention |
Sensitive keys in context.data |
| L4 | Error message sanitization | propagate_error() does not include raw inputs in error messages |
| L5 | context.logger |
Auto-inject trace_id/module_id, unified log format |
=== "Python"
```python
from apcore import Executor, RetryMiddleware, RetryConfig
# Recommended order
executor = Executor(
registry=registry,
middlewares=[
RateLimitMiddleware(), # 1. Rate limiting (check first)
LoggingMiddleware(), # 2. Logging (record all requests)
MetricsMiddleware(), # 3. Performance monitoring
CacheMiddleware(), # 4. Cache (avoid duplicate execution)
RetryMiddleware(RetryConfig()), # 5. Retry (retry on failure)
FallbackMiddleware(fallback_values={}), # 6. Fallback (last resort)
],
)
```
=== "TypeScript"
```typescript
import { Executor, RetryHintMiddleware } from 'apcore-js';
// Recommended order
const executor = new Executor({
registry,
middlewares: [
new RateLimitMiddleware(), // 1. Rate limiting (check first)
new LoggingMiddleware(), // 2. Logging (record all requests)
new MetricsMiddleware(), // 3. Performance monitoring
new CacheMiddleware(60), // 4. Cache (avoid duplicate execution)
new RetryHintMiddleware(), // 5. Retry hints (retry on failure)
new FallbackMiddleware({}), // 6. Fallback (last resort)
],
});
```
=== "Rust"
```rust
use apcore::executor::Executor;
use apcore::config::Config;
use apcore::middleware::{RetryConfig, RetryMiddleware};
use std::collections::HashMap;
// Recommended order
let executor = Executor::new(registry, Config::default());
executor.use_middleware(Box::new(RateLimitMiddleware::new(100, 60.0)))?; // 1. Rate limiting
executor.use_middleware(Box::new(LoggingMiddleware::default()))?; // 2. Logging
executor.use_middleware(Box::new(MetricsMiddleware::default()))?; // 3. Metrics
executor.use_middleware(Box::new(CacheMiddleware::new(60.0)))?; // 4. Cache
executor.use_middleware(Box::new(RetryMiddleware::new(RetryConfig::default())))?; // 5. Retry
executor.use_middleware(Box::new(FallbackMiddleware::new(HashMap::new())))?; // 6. Fallback
```
=== "Python"
```python
import time
from apcore import Middleware, Context
class EfficientMiddleware(Middleware):
"""Efficient middleware"""
def _should_process(self, module_id: str) -> bool:
return module_id.startswith("executor.")
def before(self, module_id: str, inputs: dict, context: Context) -> None:
# Quick check
if not self._should_process(module_id):
return
# Avoid time-consuming operations in before; use context.data
# to pass timing state to after().
context.data["_apcore.mw.start_time"] = time.perf_counter()
def after(self, module_id: str, inputs: dict, output: dict, context: Context) -> None:
start = context.data.get("_apcore.mw.start_time")
if start is not None:
duration = time.perf_counter() - start
# Process duration...
```
=== "TypeScript"
```typescript
import { Middleware } from 'apcore-js';
import type { Context } from 'apcore-js';
class EfficientMiddleware extends Middleware {
private shouldProcess(moduleId: string): boolean {
return moduleId.startsWith('executor.');
}
override before(
moduleId: string,
_inputs: Record<string, unknown>,
context: Context,
): null {
// Quick check
if (!this.shouldProcess(moduleId)) return null;
// Avoid time-consuming operations in before; use context.data
// to pass timing state to after().
context.data['_apcore.mw.start_time'] = performance.now();
return null;
}
override after(
_moduleId: string,
_inputs: Record<string, unknown>,
_output: Record<string, unknown>,
context: Context,
): null {
const start = context.data['_apcore.mw.start_time'] as number | undefined;
if (start !== undefined) {
const duration = performance.now() - start;
// Process duration...
void duration;
}
return null;
}
}
```
=== "Rust"
```rust
use apcore::context::Context;
use apcore::errors::ModuleError;
use apcore::middleware::Middleware;
use async_trait::async_trait;
use serde_json::{json, Value};
use std::time::Instant;
#[derive(Debug)]
struct EfficientMiddleware;
impl EfficientMiddleware {
fn should_process(&self, module_id: &str) -> bool {
module_id.starts_with("executor.")
}
}
#[async_trait]
impl Middleware for EfficientMiddleware {
fn name(&self) -> &str {
"efficient"
}
async fn before(
&self,
module_id: &str,
_inputs: Value,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
if !self.should_process(module_id) {
return Ok(None);
}
// Store an opaque token in context.data so after() can read it.
// (Using elapsed-millis-from-now as a portable u128 stored as string.)
ctx.data.insert(
"_apcore.mw.start_time_ns",
json!(Instant::now().elapsed().as_nanos().to_string()),
);
Ok(None)
}
async fn after(
&self,
_module_id: &str,
_inputs: Value,
_output: Value,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
if let Some(Value::String(_token)) = ctx.data.get("_apcore.mw.start_time_ns") {
// Process duration here using your timing source of choice.
}
Ok(None)
}
async fn on_error(
&self,
_module_id: &str,
_inputs: Value,
_error: &ModuleError,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
Ok(None)
}
}
```
=== "Python"
```python
import logging
from apcore import Middleware, Context
class SafeMiddleware(Middleware):
"""Safe middleware: own errors don't affect main flow"""
def before(self, module_id: str, inputs: dict, context: Context) -> None:
try:
self._do_something()
except Exception as e:
# Log error but don't raise
logging.warning(f"Middleware error: {e}")
def after(self, module_id: str, inputs: dict, output: dict, context: Context) -> None:
try:
self._do_something_else()
except Exception as e:
logging.warning(f"Middleware error: {e}")
def _do_something(self) -> None:
...
def _do_something_else(self) -> None:
...
```
=== "TypeScript"
```typescript
import { Middleware } from 'apcore-js';
import type { Context } from 'apcore-js';
class SafeMiddleware extends Middleware {
override before(
_moduleId: string,
_inputs: Record<string, unknown>,
_context: Context,
): null {
try {
this.doSomething();
} catch (e) {
console.warn(`Middleware error: ${e}`);
}
return null;
}
override after(
_moduleId: string,
_inputs: Record<string, unknown>,
_output: Record<string, unknown>,
_context: Context,
): null {
try {
this.doSomethingElse();
} catch (e) {
console.warn(`Middleware error: ${e}`);
}
return null;
}
private doSomething(): void {}
private doSomethingElse(): void {}
}
```
=== "Rust"
```rust
use apcore::context::Context;
use apcore::errors::ModuleError;
use apcore::middleware::Middleware;
use async_trait::async_trait;
use serde_json::Value;
#[derive(Debug)]
struct SafeMiddleware;
impl SafeMiddleware {
fn do_something(&self) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
fn do_something_else(&self) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
}
#[async_trait]
impl Middleware for SafeMiddleware {
fn name(&self) -> &str {
"safe"
}
async fn before(
&self,
_module_id: &str,
_inputs: Value,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
if let Err(e) = self.do_something() {
tracing::warn!("Middleware error: {}", e);
}
Ok(None)
}
async fn after(
&self,
_module_id: &str,
_inputs: Value,
_output: Value,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
if let Err(e) = self.do_something_else() {
tracing::warn!("Middleware error: {}", e);
}
Ok(None)
}
async fn on_error(
&self,
_module_id: &str,
_inputs: Value,
_error: &ModuleError,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
Ok(None)
}
}
```
The executor has two extension mechanisms. Choosing the wrong one leads to awkward workarounds.
- Logging / metrics / tracing — You need to see both inputs and outputs as a pair
- Retry logic —
on_error()lets you return a recovery value - Input enrichment — Add headers, inject defaults, normalize formats
- Output transformation — Redact fields, add metadata, reshape responses
Middleware runs at fixed positions (before validation, after execution) and participates in the onion model automatically.
- Rate limiting — Needs to run early (e.g., right after ACL), not at the fixed middleware position
- Cost budgeting — Must gate execution before it happens, at a specific point
- Custom validation — Schema validation isn't enough; you need semantic checks
- Feature flags — Conditionally skip execution based on external config
Custom steps can be inserted at any position and appear individually in PipelineTrace.
Does your logic need to wrap execution (see inputs AND outputs)?
→ Yes: Use Middleware (before/after pair)
→ No: Does it need to run at a specific pipeline position?
→ Yes: Use Custom Step (insert_before/insert_after)
→ No: Use Middleware (simpler registration)
=== "Python" ```python from apcore.pipeline import BaseStep, StepResult, PipelineContext
class RateLimiterStep(BaseStep):
def __init__(self, max_rps: int = 100):
super().__init__(
name="rate_limiter",
description="Per-module rate limiting",
removable=True,
replaceable=True,
pure=True,
)
self.max_rps = max_rps
async def execute(self, ctx: PipelineContext) -> StepResult:
if self._over_limit(ctx.module_id):
return StepResult(action="abort", explanation="Rate limit exceeded")
return StepResult(action="continue")
# Insert after ACL check — before middleware and validation
strategy.insert_after("acl_check", RateLimiterStep(max_rps=50))
```
=== "TypeScript" ```typescript import type { Step, StepResult, PipelineContext } from 'apcore-js';
// `Step` is an interface, not a base class — implement it.
class RateLimiterStep implements Step {
readonly name = 'rate_limiter';
readonly description = 'Per-module rate limiting';
readonly removable = true;
readonly replaceable = true;
readonly pure = true;
constructor(private maxRps: number = 100) {}
async execute(ctx: PipelineContext): Promise<StepResult> {
if (this.overLimit(ctx.moduleId)) {
return { action: 'abort', explanation: 'Rate limit exceeded' };
}
return { action: 'continue' };
}
}
strategy.insertAfter('acl_check', new RateLimiterStep(50));
```
=== "Rust" ```rust use apcore::{PipelineContext, Step, StepResult}; use apcore::errors::ModuleError; use async_trait::async_trait;
struct RateLimiterStep { max_rps: u32 }
#[async_trait]
impl Step for RateLimiterStep {
fn name(&self) -> &str { "rate_limiter" }
fn description(&self) -> &str { "Per-module rate limiting" }
fn removable(&self) -> bool { true }
fn replaceable(&self) -> bool { true }
fn pure(&self) -> bool { true }
async fn execute(&self, ctx: &mut PipelineContext) -> Result<StepResult, ModuleError> {
if self.over_limit(&ctx.module_id) {
return Ok(StepResult::abort("Rate limit exceeded"));
}
Ok(StepResult::continue_step())
}
}
strategy.insert_after("acl_check", Box::new(RateLimiterStep { max_rps: 50 }))?;
```
=== "Python"
```python
import logging
import time
from apcore import Registry, Executor, Middleware, Context
# 1. Custom logging middleware
class AppLoggingMiddleware(Middleware):
def __init__(self):
self.logger = logging.getLogger("app")
self._times: dict[str, float] = {}
def before(self, module_id: str, inputs: dict, context: Context) -> None:
self._times[context.trace_id] = time.perf_counter()
self.logger.info(f"[{context.trace_id}] -> {module_id}")
def after(self, module_id: str, inputs: dict, output: dict, context: Context) -> None:
duration = (time.perf_counter() - self._times.pop(context.trace_id, 0)) * 1000
success = output.get("success", True)
status = "OK" if success else "FAIL"
self.logger.info(f"[{context.trace_id}] {status} {module_id} ({duration:.1f}ms)")
def on_error(self, module_id: str, inputs: dict, error: Exception, context: Context) -> None:
self.logger.error(f"[{context.trace_id}] FAIL {module_id}: {error}")
# 2. Simple metrics middleware
class SimpleMetricsMiddleware(Middleware):
def __init__(self):
self.calls = 0
self.errors = 0
def before(self, module_id: str, inputs: dict, context: Context) -> None:
self.calls += 1
def on_error(self, module_id: str, inputs: dict, error: Exception, context: Context) -> None:
self.errors += 1
# 3. Setup
logging.basicConfig(level=logging.INFO, format="%(message)s")
registry = Registry(extensions_dir="./extensions")
registry.discover()
metrics = SimpleMetricsMiddleware()
executor = Executor(
registry=registry,
middlewares=[AppLoggingMiddleware(), metrics],
)
# 4. Usage
result = executor.call(
module_id="executor.email.send_email",
inputs={"to": "user@example.com", "subject": "Hi", "body": "Hello"},
)
print(f"\nTotal calls: {metrics.calls}")
print(f"Total errors: {metrics.errors}")
```
=== "TypeScript"
```typescript
import { Registry, Executor, Middleware } from 'apcore-js';
import type { Context } from 'apcore-js';
// 1. Custom logging middleware
class AppLoggingMiddleware extends Middleware {
private readonly times = new Map<string, number>();
override before(
moduleId: string,
_inputs: Record<string, unknown>,
context: Context,
): null {
this.times.set(context.traceId, performance.now());
console.info(`[${context.traceId}] -> ${moduleId}`);
return null;
}
override after(
moduleId: string,
_inputs: Record<string, unknown>,
output: Record<string, unknown>,
context: Context,
): null {
const start = this.times.get(context.traceId) ?? performance.now();
this.times.delete(context.traceId);
const duration = performance.now() - start;
const success = (output['success'] as boolean | undefined) ?? true;
const status = success ? 'OK' : 'FAIL';
console.info(`[${context.traceId}] ${status} ${moduleId} (${duration.toFixed(1)}ms)`);
return null;
}
override onError(
moduleId: string,
_inputs: Record<string, unknown>,
error: Error,
context: Context,
): null {
console.error(`[${context.traceId}] FAIL ${moduleId}: ${error}`);
return null;
}
}
// 2. Simple metrics middleware
class SimpleMetricsMiddleware extends Middleware {
calls = 0;
errors = 0;
override before(
_moduleId: string,
_inputs: Record<string, unknown>,
_context: Context,
): null {
this.calls += 1;
return null;
}
override onError(
_moduleId: string,
_inputs: Record<string, unknown>,
_error: Error,
_context: Context,
): null {
this.errors += 1;
return null;
}
}
// 3. Setup
const registry = new Registry({ extensionsDir: './extensions' });
await registry.discover();
const metrics = new SimpleMetricsMiddleware();
const executor = new Executor({
registry,
middlewares: [new AppLoggingMiddleware(), metrics],
});
// 4. Usage
const result = await executor.call(
'executor.email.send_email',
{ to: 'user@example.com', subject: 'Hi', body: 'Hello' },
);
console.log(`\nTotal calls: ${metrics.calls}`);
console.log(`Total errors: ${metrics.errors}`);
```
=== "Rust"
```rust
use apcore::config::Config;
use apcore::context::Context;
use apcore::errors::ModuleError;
use apcore::executor::Executor;
use apcore::middleware::Middleware;
use apcore::registry::Registry;
use async_trait::async_trait;
use parking_lot::Mutex;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
// 1. Custom logging middleware
#[derive(Debug, Default)]
struct AppLoggingMiddleware {
times: Mutex<HashMap<String, Instant>>,
}
#[async_trait]
impl Middleware for AppLoggingMiddleware {
fn name(&self) -> &str {
"app_logging"
}
async fn before(
&self,
module_id: &str,
_inputs: Value,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
self.times
.lock()
.insert(ctx.trace_id.to_string(), Instant::now());
tracing::info!("[{}] -> {}", ctx.trace_id, module_id);
Ok(None)
}
async fn after(
&self,
module_id: &str,
_inputs: Value,
output: Value,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
let start = self
.times
.lock()
.remove(ctx.trace_id)
.unwrap_or_else(Instant::now);
let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
let success = output
.get("success")
.and_then(Value::as_bool)
.unwrap_or(true);
let status = if success { "OK" } else { "FAIL" };
tracing::info!(
"[{}] {} {} ({:.1}ms)",
ctx.trace_id,
status,
module_id,
duration_ms,
);
Ok(None)
}
async fn on_error(
&self,
module_id: &str,
_inputs: Value,
error: &ModuleError,
ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
tracing::error!("[{}] FAIL {}: {}", ctx.trace_id, module_id, error);
Ok(None)
}
}
// 2. Simple metrics middleware
#[derive(Debug, Default)]
struct SimpleMetricsMiddleware {
calls: Mutex<u64>,
errors: Mutex<u64>,
}
#[async_trait]
impl Middleware for SimpleMetricsMiddleware {
fn name(&self) -> &str {
"simple_metrics"
}
async fn before(
&self,
_module_id: &str,
_inputs: Value,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
*self.calls.lock() += 1;
Ok(None)
}
async fn after(
&self,
_module_id: &str,
_inputs: Value,
_output: Value,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
Ok(None)
}
async fn on_error(
&self,
_module_id: &str,
_inputs: Value,
_error: &ModuleError,
_ctx: &Context<Value>,
) -> Result<Option<Value>, ModuleError> {
*self.errors.lock() += 1;
Ok(None)
}
}
// 3. Setup
let registry = Arc::new(Registry::from_extensions_dir("./extensions")?);
let executor = Executor::new(registry, Config::default());
let metrics = Arc::new(SimpleMetricsMiddleware::default());
executor.use_middleware(Box::new(AppLoggingMiddleware::default()))?;
executor.use_middleware(Box::new(Arc::clone(&metrics)) as Box<dyn Middleware>)?;
// 4. Usage
let _result = executor
.call(
"executor.email.send_email",
json!({ "to": "user@example.com", "subject": "Hi", "body": "Hello" }),
None,
)
.await?;
println!("\nTotal calls: {}", *metrics.calls.lock());
println!("Total errors: {}", *metrics.errors.lock());
```
Output:
[abc-123] → executor.email.send_email
[abc-123] ✓ executor.email.send_email (45.2ms)
Total calls: 1
Total errors: 0
- Core Executor - Executor feature spec
- ACL Configuration Guide - Access control configuration
- Architecture Design - Overall system architecture