Debugging
Advanced debugging tools, tracing, and logging strategies
JustScale provides comprehensive debugging tools including custom debugger formatters, distributed tracing support, and structured logging. The @justscale/debugger package enhances your debugging experience in JetBrains IDEs and Chrome DevTools.
Enhanced Debugging with @justscale/debugger
The debugger package provides custom object formatters and inspector proxies that make debugging JustScale applications significantly easier.
Installation
pnpm add --save-dev @justscale/debuggerUsage with JetBrains IDEs
Enable custom formatters in WebStorm, IntelliJ IDEA, or other JetBrains IDEs by adding the debugger setup to your Node options.
# Option A: Automatic spawn wrapper (recommended for JetBrains)
# In Run/Debug Configuration > Node options:
--import @justscale/debugger/setup
# Option B: Bypass JetBrains injection (simpler)
# In Run/Debug Configuration > Node options:
--inspect=9229 --import @justscale/debugger/setup
# Option C: Terminal/programmatic
npx tsx --import @justscale/debugger/setup src/index.tsHow It Works
The debugger package creates an inspector proxy that intercepts the Chrome DevTools Protocol and enables custom object formatters:
- Detects JetBrains debugger injection and spawns a clean child process
- Opens Node inspector on an internal port (default: 9339)
- Creates a proxy on the debugger port (default: 9229)
- Intercepts
Runtime.enableand injectssetCustomObjectFormatterEnabled(true) - Transforms
customPreviewresponses into description fields
Programmatic Setup
You can also set up the inspector proxy programmatically:
import { setupInspectorProxy } from "@justscale/debugger";
import { createCluster } from "@justscale/cluster";
// Set up at the start of your application
await setupInspectorProxy({
inspectorPort: 9339, // Internal inspector port
waitForDebugger: false, // Wait for debugger to attach
verbose: true, // Enable logging
});
// Your app code
const cluster = createCluster({ /* ... */ });
await cluster.serve({ http: 3000 });Custom Object Formatters
With the debugger enabled, you can define custom formatters for your objects to improve the debugging experience:
// Define custom formatter for User objects
if (typeof globalThis.devtoolsFormatters === "undefined") {
globalThis.devtoolsFormatters = [];
}
globalThis.devtoolsFormatters.push({
header(obj: any) {
if (!obj || typeof obj !== "object") return null;
if (!("__userBrand" in obj)) return null;
// Return JsonML format
return ["div", {}, `User(${obj.email})`];
},
hasBody() {
return true;
},
body(obj: any) {
return [
"div",
{},
["div", {}, `ID: ${obj.id}`],
["div", {}, `Email: ${obj.email}`],
["div", {}, `Role: ${obj.role}`],
];
},
});Structured Logging
JustScale provides a built-in structured logging system with context propagation and observability integration.
Using the Logger
Every route handler receives a logger instance with automatic context (controller name, route, request ID).
import { createController } from "@justscale/core";
import { Get } from '@justscale/http';
import { UserService } from "../services/user-service";
const UsersController = createController("/users", {
inject: { users: UserService },
routes: (services) => ({
list: Get("/").handle(({ logger, res }) => {
logger.info("Fetching all users");
const result = services.users.findAll();
logger.debug("Found users", { count: result.length });
res.json({ users: result });
}),
}),
});
// Output:
// [INFO] [/users] Fetching all users { requestId: "a1b2c3d4" }
// [DEBUG] [/users] Found users { count: 42, requestId: "a1b2c3d4" }Logger Methods
| Method | Level | Use Case |
|---|---|---|
logger.debug(msg, attrs?) | DEBUG | Detailed diagnostic information |
logger.info(msg, attrs?) | INFO | General informational messages |
logger.warn(msg, attrs?) | WARN | Warning messages for unexpected situations |
logger.error(msg, attrs?) | ERROR | Error conditions that need attention |
Custom Logger Factory
Override the default console logger with your own implementation:
import { Container } from "@justscale/core";
import type { Logger, LoggerFactory, LogLevel, LogAttributes } from "@justscale/core";
import pino from "pino";
class PinoLoggerFactory implements LoggerFactory {
private pino = pino();
create(context: string): Logger {
return {
debug: (message: string, attributes?: LogAttributes) => {
this.pino.debug({ context, ...attributes }, message);
},
info: (message: string, attributes?: LogAttributes) => {
this.pino.info({ context, ...attributes }, message);
},
warn: (message: string, attributes?: LogAttributes) => {
this.pino.warn({ context, ...attributes }, message);
},
error: (message: string, attributes?: LogAttributes) => {
this.pino.error({ context, ...attributes }, message);
},
};
}
}
// Register in your container
const container = new Container();
container.setLoggerFactory(new PinoLoggerFactory());Distributed Tracing
JustScale includes built-in support for distributed tracing through observability context propagation and instrumentation hooks.
Observability Context
Every request runs within an observability scope that propagates context through async operations:
import { getContext, runWithContext } from "@justscale/core";
import { createController } from "@justscale/core";
import { Get } from "@justscale/http";
const UsersController = createController("/users", {
routes: () => ({
getOne: Get("/:id").handle(async ({ params, res }) => {
// Get current context
const ctx = getContext();
console.log(ctx);
// {
// requestId: "a1b2c3d4",
// route: "getUser",
// method: "GET",
// path: "/users/:id"
// }
// Run code in a nested scope
await runWithContext(
{ userId: params.id },
async () => {
const nested = getContext();
console.log(nested);
// {
// requestId: "a1b2c3d4",
// route: "getUser",
// method: "GET",
// path: "/users/:id",
// userId: "123"
// }
}
);
res.json({ user: { id: params.id } });
}),
}),
});Instrumentation Hooks
Integrate with OpenTelemetry, Datadog, or other observability tools by registering instrumentation hooks:
import { registerInstrumentation } from "@justscale/core";
import type { Instrumentation, ScopeInfo, ObservabilityContext } from "@justscale/core";
import { trace } from "@opentelemetry/api";
const OpenTelemetryInstrumentation: Instrumentation = {
name: "opentelemetry",
onScopeStart(info: ScopeInfo, context: ObservabilityContext) {
const tracer = trace.getTracer("justscale");
const span = tracer.startSpan(info.name, {
attributes: {
...info.attributes,
...context,
},
});
// Store span in context for later
return { span };
},
onScopeEnd(info: ScopeInfo, context: ObservabilityContext, data?: any) {
if (data?.span) {
data.span.end();
}
},
onScopeError(info: ScopeInfo, context: ObservabilityContext, error: Error, data?: any) {
if (data?.span) {
data.span.recordException(error);
data.span.setStatus({ code: 2 }); // ERROR
data.span.end();
}
},
};
// Register the instrumentation
registerInstrumentation(OpenTelemetryInstrumentation);Automatic Request Tracing
JustScale automatically creates a trace scope for each request with HTTP method, route, and request ID:
import { runInScopeAsync } from "@justscale/core";
// Internally, every request runs in a scope like this:
await runInScopeAsync(
{
type: "request",
name: `${route.method} ${route.path}`,
attributes: {
"http.method": route.method,
"http.route": route.path,
"route.name": route.name,
},
},
{
requestId: "a1b2c3d4",
route: route.name,
method: route.method,
path: route.path,
},
async () => {
// Middleware, guards, handler all run here
// Context is automatically propagated
}
);Debugging Strategies
Service Resolution Issues
If you encounter dependency resolution errors, check:
- All services are registered in the correct order
- Circular dependencies are avoided (use lazy resolution if needed)
- Service tokens match exactly (import from the same file)
- TypeScript compilation succeeds (type errors indicate missing deps)
import { Container } from "@justscale/core";
// Enable verbose DI logging
const container = new Container();
// Register with logging
container.register(MyService);
console.log("Registered:", container.has(MyService));
// Resolve with logging
try {
const instance = container.resolve(MyService);
console.log("Resolved:", instance);
} catch (error) {
console.error("Resolution failed:", error);
}Route Matching Issues
Debug route matching by inspecting the compiled routes:
import { createCluster } from "@justscale/cluster";
const cluster = createCluster({
controllers: [UsersController],
});
// Access the underlying app
const app = cluster.app;
// Inspect compiled routes
for (const controller of app.controllers) {
console.log(`Controller: ${controller.prefix}`);
for (const route of controller.routes) {
console.log(
` ${route.method} ${route.path} (pattern: ${route.pattern})`
);
}
}
// Test route matching
const matched = app.match("GET", "/users/123");
if (matched) {
console.log("Matched route:", matched.route.name);
console.log("Params:", matched.params);
} else {
console.log("No route matched");
}Middleware Execution Order
Add logging middleware to trace execution order:
import { createMiddleware } from "@justscale/core";
import { Get } from "@justscale/http";
const LoggingMiddleware = createMiddleware({
inject: {},
handler: () => async (ctx) => {
console.log("[MIDDLEWARE] Before handler", {
route: ctx.logger,
params: ctx.params,
});
// Return empty object (adds nothing to context)
return {};
},
});
// Use in routes
Get("/users/:id")
.use(LoggingMiddleware)
.use(parseAuth)
.handle(({ user, params, res }) => {
console.log("[HANDLER] Executing");
res.json({ user });
});
// Output:
// [MIDDLEWARE] Before handler
// [AUTH] Validating token
// [HANDLER] ExecutingCluster Communication Issues
Debug cluster socket communication:
# Check if socket exists
ls -la /tmp/justscale/
# Test socket connection
justscale --help
# Enable verbose logging
DEBUG=justscale:* node src/index.tsPerformance Profiling
Use Node.js built-in profiling tools with JustScale:
# CPU profiling
node --cpu-prof --import @justscale/debugger/setup src/index.ts
# Heap snapshot
node --heap-prof --import @justscale/debugger/setup src/index.ts
# Inspect with Chrome DevTools
node --inspect --import @justscale/debugger/setup src/index.ts
# Open chrome://inspect in ChromeAdd performance marks in your code:
import { performance } from "perf_hooks";
import { createController } from "@justscale/core";
import { Get } from "@justscale/http";
const PerformanceController = createController("/api", {
routes: () => ({
slow: Get("/slow").handle(({ logger, res }) => {
performance.mark("handler-start");
// Do expensive work
const result = expensiveOperation();
performance.mark("handler-end");
performance.measure("handler", "handler-start", "handler-end");
const measure = performance.getEntriesByName("handler")[0];
logger.info("Handler performance", { duration: measure.duration });
res.json({ result, duration: measure.duration });
}),
}),
});Info
Pro Tip: The @justscale/debugger package's inspector proxy is completely transparent - your app doesn't need any code changes. Just add the import flag and debug as usual!