<!-- Markdown mirror of https://justscale.sh/docs/advanced/debugging -->

# 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

```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

```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

```typescript
import { setupInspectorProxy } from "@justscale/debugger";
import JustScale from "@justscale/core";

// 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 = JustScale().build();
await app.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

```typescript
// 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

```typescript
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:

pino-logger.tsTypeScript

```typescript
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

```typescript
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

srcinstrumentationopentelemetry.ts

index.ts

srcinstrumentationopentelemetry.ts

index.ts

src/instrumentation/opentelemetry.tsTypeScript

```typescript
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

```typescript
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

```typescript
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

```typescript
import JustScale from "@justscale/core";

const app = JustScale()
  .add(UsersController)
  .build();

// 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

```typescript
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

```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

```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

```typescript
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!

## Next Steps

- Plugins
- Type Utilities
- Testing
