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

Bash
pnpm add --save-dev @justscale/debugger

Usage with JetBrains IDEs

Enable custom formatters in WebStorm, IntelliJ IDEA, or other JetBrains IDEs by adding the debugger setup to your Node options.

Bash
# 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.ts

How 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.enable and injects setCustomObjectFormatterEnabled(true)
  • Transforms customPreview responses into description fields

Programmatic Setup

You can also set up the inspector proxy programmatically:

debug.tsTypeScript
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:

formatters.tsTypeScript
// 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).

src/controllers/users.tsTypeScript
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

MethodLevelUse Case
logger.debug(msg, attrs?)DEBUGDetailed diagnostic information
logger.info(msg, attrs?)INFOGeneral informational messages
logger.warn(msg, attrs?)WARNWarning messages for unexpected situations
logger.error(msg, attrs?)ERRORError conditions that need attention

Custom Logger Factory

Override the default console logger with your own implementation:

pino-logger.tsTypeScript
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:

context-example.tsTypeScript
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:

Files
src/instrumentation/opentelemetry.tsTypeScript
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:

internal-tracing.tsTypeScript
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)
di-debugging.tsTypeScript
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:

route-debugging.tsTypeScript
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:

middleware-debugging.tsTypeScript
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] Executing

Cluster Communication Issues

Debug cluster socket communication:

Bash
# Check if socket exists
ls -la /tmp/justscale/

# Test socket connection
justscale --help

# Enable verbose logging
DEBUG=justscale:* node src/index.ts

Performance Profiling

Use Node.js built-in profiling tools with JustScale:

Bash
# 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 Chrome

Add performance marks in your code:

performance-profiling.tsTypeScript
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!