Telemetry & Monitoring
Radish CLI generates a complete telemetry system that tracks HTTP requests, service calls, external API calls, and application health — all with zero dependencies.
Overview
Every generated app includes:
- Request tracking — inbound HTTP requests (method, path, duration, status)
- Service instrumentation — all CRUD operations timed and reported
- External API tracking — third-party calls (Dynamics 365, BigCommerce, Stripe, etc.)
- Correlation IDs — trace a single request across the full stack
- Structured logging — timestamped, correlation-ID-aware log entries
- Health endpoint —
GET /api/healthwith aggregated stats - Admin monitoring page — live dashboard at
/admin/health - Hub reporting — push metrics to Radish Hub for historical dashboards
Quick Start
Telemetry is always-on. No configuration needed for basic monitoring.
Track external API calls
import { trackedFetch } from '$lib/telemetry';
// Wrap any outbound fetch — identical API to fetch() plus service name
const res = await trackedFetch('dynamics365',
'https://org.api.crm.dynamics.com/api/data/v9.2/accounts',
{
method: 'GET',
headers: { 'Authorization': `Bearer ${token}` }
},
{ correlationId: locals.correlationId }
);
That's it. The call is now tracked — duration, status, success/error — and visible in:
/api/healthresponse (externalCallMetricsarray)- Admin panel at
/admin/health(External API Calls table) - Radish Hub dashboard (if Hub reporting is enabled)
Enable Hub reporting
Add to .env:
RADISH_HUB_URL=https://hub.radishplatform.com
RADISH_HUB_TOKEN=rhub_your_token_here
# Optional: identify this app
APP_SLUG=commercehub
APP_NAME=CommerceHub
APP_URL=https://commercehub.radishplatform.com
The app auto-registers with Hub on first push. No manual linking needed. Works with any deployment method (Hub, Coolify, Docker, bare metal).
Hub collects metrics every 60 seconds and provides:
- Historical trend charts (response time, error rate, external call duration over days/weeks)
- Cross-app comparison (compare D365 performance across instances)
- Alerting (error rate spikes)
Architecture
Inbound Request
│
├─ telemetryHandle (middleware)
│ ├─ Generate correlation ID
│ ├─ Track request duration + status
│ └─ Set x-correlation-id response header
│
├─ Service Layer
│ └─ instrument() wraps every CRUD method
│ └─ trackServiceCall(entity, action, duration, success)
│
├─ External API Calls (opt-in)
│ └─ trackedFetch('service', url, init, { correlationId })
│ └─ trackExternalCall(service, endpoint, duration, status)
│
└─ All metrics flow to TelemetryAdapter
├─ ConsoleAdapter (default) — in-memory ring buffer
├─ Hub Reporter — pushes stats every 60s
└─ Custom adapters — Prometheus, Datadog, etc.
Tracking External API Calls
trackedFetch(service, input, init?, context?)
Drop-in replacement for fetch() with telemetry. Use it for any outbound HTTP call you want to monitor.
import { trackedFetch } from '$lib/telemetry';
// Basic usage
const res = await trackedFetch('stripe', 'https://api.stripe.com/v1/charges', {
method: 'POST',
headers: { 'Authorization': `Bearer ${stripeKey}` },
body: new URLSearchParams({ amount: '2000', currency: 'usd' })
});
// With correlation ID (recommended — enables cross-service tracing)
const res = await trackedFetch('bigcommerce', url, init, {
correlationId: locals.correlationId
});
// Disable correlation ID propagation (if the external API doesn't support it)
const res = await trackedFetch('legacy-api', url, init, {
correlationId: locals.correlationId,
propagateCorrelationId: false
});
What gets tracked per call:
| Field | Description |
|---|---|
service | Name you provide ('dynamics365', 'bigcommerce', etc.) |
endpoint | URL pathname (query params stripped for grouping) |
method | HTTP method |
status | HTTP status code |
duration | Time in milliseconds |
success | true if status < 400 |
error | Error message (if fetch throws) |
correlationId | Links to the inbound request that triggered this call |
Slow call logging: External calls >2 seconds are automatically logged as warnings with the structured logger.
Where to use trackedFetch
Use it anywhere you call a third-party API from server-side code:
// In a service method
class OrderService extends BaseService {
async syncToErp(orderId: string) {
const order = await this.get(orderId);
const res = await trackedFetch('dynamics365',
`${D365_URL}/api/data/v9.2/salesorders`,
{
method: 'POST',
headers: { 'Authorization': `Bearer ${await getD365Token()}` },
body: JSON.stringify(mapOrderToD365(order))
},
{ correlationId: this.auth.correlationId }
);
if (!res.ok) throw new Error(`D365 sync failed: ${res.status}`);
return res.json();
}
}
// In a SvelteKit server route
export const POST: RequestHandler = async ({ request, locals }) => {
const body = await request.json();
const webhookRes = await trackedFetch('slack',
'https://hooks.slack.com/services/xxx',
{
method: 'POST',
body: JSON.stringify({ text: body.message })
},
{ correlationId: locals.correlationId }
);
return json({ sent: webhookRes.ok });
};
// In a background job handler
async function processWebhook(payload, context) {
const res = await trackedFetch('payment-gateway',
`${GATEWAY_URL}/api/v1/verify`,
{ method: 'POST', body: JSON.stringify(payload) }
// No correlationId needed — background job has no inbound request
);
}
Raw Time-Series Data
For custom dashboards or exports, getExternalCallTimeline() returns the raw ring buffer of external call metrics:
import { getTelemetry } from '@core/telemetry';
const telemetry = getTelemetry();
// All external calls
const timeline = telemetry.getExternalCallTimeline();
// Filter by service
const d365Calls = telemetry.getExternalCallTimeline('dynamics365');
Each entry in the timeline is an ExternalCallMetric:
{
service: string; // 'dynamics365', 'bigcommerce', 'stripe'
endpoint: string; // URL pathname (query stripped)
method: string; // 'GET', 'POST', etc.
status: number; // HTTP status code
duration: number; // Milliseconds
success: boolean;
error?: string;
correlationId?: string;
timestamp: Date;
}
Use cases:
- Export to CSV for analysis
- Build custom latency charts
- Compare performance across API versions
- Debug slow requests by correlation ID
Structured Logger
The structured logger produces timestamped, correlation-ID-aware log entries:
import { createLogger } from '@core/telemetry/logger';
// Create a logger with a correlation ID
const logger = createLogger(correlationId);
logger.info('Order processed', { orderId: '123', total: 49.99 });
logger.warn('Inventory low', { sku: 'ABC-001', remaining: 3 });
logger.error('Payment failed', { provider: 'stripe', code: 'card_declined' });
Output format:
[2026-07-21T14:30:00.000Z] INFO [a1b2c3d4-e5f6-7890] Order processed {"orderId":"123","total":49.99}
The logger replaces scattered console.log/error/warn calls with structured output that includes timestamps, log levels, and correlation IDs for filtering.
Service Instrumentation
Every generated service method is wrapped with this.instrument(), which:
- Records the operation start time
- Calls the telemetry adapter's
trackService()with entity, action, and duration - Logs errors with the correlation-aware logger
- Reports success/failure status
// Inside a generated service (simplified):
async list(criteria) {
return this.instrument('list', async () => {
// ... actual business logic
});
}
Instrumentation is transparent — it adds no behavior changes, only observability. The telemetry adapter receives:
- entity — which entity was operated on (e.g., 'Product')
- action — which method was called (e.g., 'list', 'create', 'update')
- duration — milliseconds elapsed
- success — whether the operation completed without throwing
Health Endpoint
GET /api/health returns current telemetry stats:
{
"uptime": 86400,
"requestCount": 15230,
"errorCount": 12,
"avgResponseTime": 145,
"errorRate": 0.08,
"dbStatus": "connected",
"memoryUsage": { "heapUsed": 85, "heapTotal": 120, "rss": 145 },
"slowestEndpoints": [
{ "path": "/api/v1/orders", "avgMs": 890, "count": 342 },
{ "path": "/api/v1/products/:id", "avgMs": 420, "count": 1205 }
],
"serviceMetrics": [
{ "entity": "order", "action": "list", "avgMs": 45, "count": 342, "errorCount": 0 },
{ "entity": "product", "action": "get", "avgMs": 12, "count": 1205, "errorCount": 2 }
],
"externalCallMetrics": [
{ "service": "dynamics365", "endpoint": "/api/data/v9.2/accounts", "avgMs": 820, "count": 47, "errorCount": 2 },
{ "service": "bigcommerce", "endpoint": "/v3/products", "avgMs": 150, "count": 312, "errorCount": 0 }
],
"recentErrors": [
{ "message": "D365 timeout", "path": "/api/v1/sync", "timestamp": "2026-07-09T..." }
]
}
Admin Monitoring Page
The generated admin panel includes a live monitoring dashboard at /admin/health:
- Stat cards — uptime, request count, avg response time, error rate, DB status, memory
- Slowest endpoints table — which API routes are slowest
- Service metrics table — entity CRUD operation performance
- External API calls table — third-party API performance by service and endpoint
- Recent errors — last 10 errors with path context
Auto-refreshes every 30 seconds. No charting library needed — DaisyUI tables and stat cards.
Hub Reporting
When RADISH_HUB_URL and RADISH_HUB_TOKEN are set in .env, the app pushes telemetry snapshots to Radish Hub every 60 seconds.
How it works
startHubReporter()is called inhooks.server.ts(auto-generated)- Every 60 seconds, it calls
getStatsAndReset()on the telemetry adapter getStatsAndReset()returns stats for that interval and resets counters (no double-counting)- Stats are POSTed to
POST /api/telemetry/pushon Hub - Hub stores the snapshot with a TTL index (auto-deletes after 30 days)
Auto-registration
The first push auto-registers the app with Hub. No manual linking needed. Set APP_SLUG, APP_NAME, and APP_URL in .env for proper identification.
What Hub provides
- Historical trends — response time, error rate, external call duration over days/weeks
- Cross-app comparison — compare D365 performance across CommerceHub, finances, etc.
- External call isolation — "is the app slow, or is D365 slow?"
- Time range queries — 1h, 6h, 24h, 7d, 30d views
Configuration
# .env — add these to enable Hub reporting
# Required
RADISH_HUB_URL=https://hub.radishplatform.com
RADISH_HUB_TOKEN=rhub_your_token
# Recommended — identifies this app in Hub's dashboard
APP_SLUG=commercehub
APP_NAME=CommerceHub
APP_URL=https://commercehub.radishplatform.com
If these env vars are not set, the reporter silently does nothing. Zero impact on apps that don't use Hub.
Correlation IDs
Every inbound request gets a unique correlation ID (UUID v4). It flows through the entire stack:
Request → x-correlation-id header (or auto-generated)
→ event.locals.correlationId
→ RequestContext.auth.correlationId
→ ServiceAuth.correlationId
→ BaseService.instrument() → telemetry
→ trackedFetch() → outbound x-correlation-id header
→ Response x-correlation-id header
Inbound: If the request includes an x-correlation-id header (from an API gateway or upstream service), it's used. Otherwise, a new UUID is generated.
Outbound: trackedFetch() auto-sets x-correlation-id on outbound requests, enabling cross-service tracing.
In logs: Every structured log entry includes the correlation ID:
[2026-07-09T14:30:00.123Z] ERROR [abc-123-def] D365 sync failed {"status": 500, "entity": "order"}
Custom Adapters
The telemetry system uses an adapter pattern. The default ConsoleAdapter works with zero dependencies. You can swap or combine adapters.
Using Multiple Adapters
// hooks.server.ts
import { setTelemetryAdapter } from '$lib/telemetry';
import { ConsoleAdapter } from '$lib/telemetry/adapters/console.adapter';
import { MultiAdapter } from '$lib/telemetry/adapters/multi.adapter';
setTelemetryAdapter(new MultiAdapter([
new ConsoleAdapter(), // In-memory stats + health endpoint
new MyCustomAdapter() // Your own backend
]));
Writing a Custom Adapter
Implement the TelemetryAdapter interface:
import type { TelemetryAdapter, RequestMetric, ServiceMetric, ExternalCallMetric, TelemetryStats } from '$lib/telemetry';
export class DatadogAdapter implements TelemetryAdapter {
trackRequest(data: RequestMetric): void {
// Send to Datadog
}
trackServiceCall(data: ServiceMetric): void {
// Send to Datadog
}
trackExternalCall(data: ExternalCallMetric): void {
// Send to Datadog — track D365 calls separately
}
trackError(error: Error, context?: Record<string, any>): void {
// Report to Datadog error tracking
}
trackEvent(name: string, data?: Record<string, any>): void {
// Custom events
}
async flush(): Promise<void> {
// Flush buffered metrics
}
async getStats(): Promise<TelemetryStats> {
// Return aggregated stats for /api/health
}
}
Next Steps
- Services — service layer with instrumented CRUD methods
- Routes — HTTP API with correlation ID headers
- Access Control — permissions and authorization