Skip to main content
Learn how to query recorded logs using Naite.get() and chaining methods.

Naite.get() Overview

Wildcard Patterns

Flexible key matchingHierarchical queries

Chaining Queries

fromFile, fromFunctionwhere conditions

Various Results

result, first, lastat(index)

Callstack Usage

Call location trackingDebugging support

Basic Usage

Query by Key

import { Naite } from "sonamu";

// Record log
Naite.t("user:create", { userId: 123, username: "john" });

// Query by exact key
const data = Naite.get("user:create").result();
// [{ userId: 123, username: "john" }]

// First data
const first = Naite.get("user:create").first();
// { userId: 123, username: "john" }

Wildcard Patterns

// Record multiple logs
Naite.t("user:create", { action: "create" });
Naite.t("user:update", { action: "update" });
Naite.t("user:delete", { action: "delete" });
Naite.t("post:create", { action: "create" });

// user:* pattern
const userLogs = Naite.get("user:*").result();
// Matches user:create, user:update, user:delete
// [{ action: "create" }, { action: "update" }, { action: "delete" }]

// *:create pattern
const createLogs = Naite.get("*:create").result();
// Matches user:create, post:create

Wildcard Pattern Rules

1. Prefix Matching

When ending with *, prefix matching:
Naite.t("syncer:renderTemplate", { /* ... */ });
Naite.t("syncer:renderTemplate:user", { /* ... */ });
Naite.t("syncer:writeFile", { /* ... */ });

// "syncer:*" β†’ All keys starting with syncer: regardless of length
const logs = Naite.get("syncer:*").result();
// syncer:renderTemplate
// syncer:renderTemplate:user
// syncer:writeFile
// All matched

2. Middle Wildcard

When * is in the middle:
Naite.t("syncer:renderTemplate:user", { /* ... */ });
Naite.t("syncer:writeFile:user", { /* ... */ });
Naite.t("syncer:renderTemplate:post", { /* ... */ });

// "syncer:*:user" β†’ Middle part is * (same length)
const logs = Naite.get("syncer:*:user").result();
// syncer:renderTemplate:user
// syncer:writeFile:user
// Matched

// syncer:renderTemplate:post not matched (last is not user)

3. Exact Matching

Without wildcard, only exact key matches:
Naite.t("user:create", { /* ... */ });
Naite.t("user:create:validation", { /* ... */ });

// "user:create" β†’ Exact match only
const logs = Naite.get("user:create").result();
// Only user:create matched
// user:create:validation not matched

Chaining Methods

fromFile() - Filter by Filename

Query only logs called from a specific file:
test("file filtering", async () => {
  // Logs recorded from multiple files
  // From user.model.test.ts:
  Naite.t("test:log", { file: "user.model.test.ts" });

  // From post.model.test.ts:
  Naite.t("test:log", { file: "post.model.test.ts" });

  // Query only logs from user.model.test.ts
  const userLogs = Naite.get("test:log")
    .fromFile("user.model.test.ts")
    .result();

  expect(userLogs).toHaveLength(1);
  expect(userLogs[0].file).toBe("user.model.test.ts");
});
How it works:
// Checks if callstack's filePath ends with /.../{fileName}
fromFile(fileName: string): NaiteQuery {
  const filtered = this.traces.filter((t) =>
    t.stack.some((frame) => frame.filePath.endsWith(`/${fileName}`))
  );
  return new NaiteQuery(filtered);
}

fromFunction() - Filter by Function Name

Query only logs called from a specific function:
async function renderTemplate(template: string) {
  Naite.t("syncer:render", { template });
  // ...
}

async function writeFile(path: string) {
  Naite.t("syncer:write", { path });
  // ...
}

test("function filtering", async () => {
  await renderTemplate("user.model.ts");
  await writeFile("output.ts");

  // Only logs called from renderTemplate
  const renderLogs = Naite.get("syncer:*")
    .fromFunction("renderTemplate")
    .result();

  expect(renderLogs).toHaveLength(1);
  expect(renderLogs[0].template).toBe("user.model.ts");
});
Options:
// Direct calls only (stack[0])
fromFunction("renderTemplate", { from: "direct" })

// Indirect calls only (stack[1+])
fromFunction("renderTemplate", { from: "indirect" })

// Both (default)
fromFunction("renderTemplate", { from: "both" })
fromFunction("renderTemplate") // Same
Practical example:
async function processUser(userId: number) {
  Naite.t("user:process:start", { userId });

  await createUser(userId);

  Naite.t("user:process:done", { userId });
}

async function createUser(userId: number) {
  Naite.t("user:create", { userId });
}

test("call relationship tracking", async () => {
  await processUser(123);

  // Only logs directly called from processUser
  const directLogs = Naite.get("user:*")
    .fromFunction("processUser", { from: "direct" })
    .result();

  // user:process:start, user:process:done
  expect(directLogs).toHaveLength(2);

  // All logs in processUser's call chain
  const allLogs = Naite.get("user:*")
    .fromFunction("processUser", { from: "both" })
    .result();

  // user:process:start, user:create, user:process:done
  expect(allLogs).toHaveLength(3);
});

where() - Filter by Data Conditions

Filter by specific fields in log data:
test("condition filtering", async () => {
  Naite.t("user:create", { userId: 1, status: "active" });
  Naite.t("user:create", { userId: 2, status: "inactive" });
  Naite.t("user:create", { userId: 3, status: "active" });

  // userId > 1
  const logs = Naite.get("user:create")
    .where("data.userId", ">", 1)
    .result();

  expect(logs).toHaveLength(2);
  expect(logs[0].userId).toBe(2);
  expect(logs[1].userId).toBe(3);

  // Only where status is active
  const activeLogs = Naite.get("user:create")
    .where("data.status", "=", "active")
    .result();

  expect(activeLogs).toHaveLength(2);
});
Supported operators:
where("data.userId", ">", 10)   // Greater than
where("data.userId", "<", 100)  // Less than
where("data.userId", ">=", 10)  // Greater than or equal
where("data.userId", "<=", 100) // Less than or equal
where("data.status", "=", "active")   // Equal
where("data.status", "!=", "deleted") // Not equal
where("data.username", "includes", "john") // Contains (string)
Path specification:
// Access data field
where("data.userId", "=", 123)

// Access nested objects (uses radashi's get)
where("data.user.profile.age", ">", 18)

Chaining Combinations

Write complex queries by chaining multiple methods:
test("complex query", async () => {
  // Chain multiple conditions
  const logs = Naite.get("user:*")
    .fromFile("user.model.test.ts")
    .fromFunction("createUser")
    .where("data.userId", ">", 100)
    .where("data.status", "=", "active")
    .result();

  // From user.model.test.ts file
  // Called by createUser function
  // userId greater than 100
  // status is active
});

Result Query Methods

result() - Full Array

const logs = Naite.get("user:*").result();
// [{ ... }, { ... }, { ... }]

first() - First

const first = Naite.get("user:create").first();
// { userId: 123, username: "john" }

// undefined if not found
const empty = Naite.get("nonexistent").first();
// undefined

last() - Last

Naite.t("user:create", { userId: 1 });
Naite.t("user:create", { userId: 2 });
Naite.t("user:create", { userId: 3 });

const last = Naite.get("user:create").last();
// { userId: 3 }

at(index) - nth

Naite.t("user:create", { userId: 1 });
Naite.t("user:create", { userId: 2 });
Naite.t("user:create", { userId: 3 });

const second = Naite.get("user:create").at(1);
// { userId: 2 }

// undefined if out of range
const outOfRange = Naite.get("user:create").at(10);
// undefined

Practical Examples

1. Complex Flow Verification

test("post creation flow", async () => {
  const postModel = new PostModel();

  const { post } = await postModel.create({
    title: "Test Post",
    content: "Content",
    author_id: 1,
  });

  // Verify entire flow
  const allLogs = Naite.get("post:create:*").result();
  expect(allLogs.length).toBeGreaterThan(0);

  // Verify input data
  const input = Naite.get("post:create:input").first();
  expect(input.title).toBe("Test Post");

  // Verify output data
  const output = Naite.get("post:create:output").first();
  expect(output.postId).toBeGreaterThan(0);

  // Verify DB query
  const dbLogs = Naite.get("post:create:*")
    .fromFunction("insertInto")
    .result();
  expect(dbLogs.length).toBeGreaterThan(0);
});

2. Syncer Behavior Verification

test("Syncer template rendering", async () => {
  await Sonamu.syncer.generateTemplate("model", {
    entityId: "User"
  });

  // Verify renderTemplate function call
  const renderLogs = Naite.get("syncer:*")
    .fromFunction("renderTemplate")
    .result();

  expect(renderLogs.length).toBeGreaterThan(0);

  // Verify User entity processing
  const userLogs = Naite.get("syncer:*")
    .where("data.entityId", "=", "User")
    .result();

  expect(userLogs.length).toBeGreaterThan(0);
});

3. Error Occurrence Tracking

test("error situation tracking", async () => {
  try {
    await createUser({ username: "" }); // Invalid input
  } catch (error) {
    // Check error logs
    const errorLogs = Naite.get("user:create:error").result();
    expect(errorLogs.length).toBeGreaterThan(0);

    // Check error cause
    const validationErrors = Naite.get("user:*")
      .where("data.error", "includes", "validation")
      .result();

    expect(validationErrors.length).toBeGreaterThan(0);
  }
});

4. Performance Measurement

test("processing time measurement", async () => {
  Naite.t("process:start", { at: new Date() });

  // Long running task
  await longRunningTask();

  Naite.t("process:end", { at: new Date() });

  const logs = Naite.get("process:*").result();
  const start = new Date(logs[0].at);
  const end = new Date(logs[1].at);

  const duration = end.getTime() - start.getTime();
  expect(duration).toBeLessThan(1000); // Within 1 second
});

Internal Structure

NaiteQuery Class

export class NaiteQuery {
  constructor(private traces: NaiteTrace[]) {}

  fromFile(fileName: string): NaiteQuery {
    // Filter and return new NaiteQuery (chainable)
  }

  fromFunction(funcName: string, options?): NaiteQuery {
    // Filter and return new NaiteQuery
  }

  where(path: string, operator: string, value: any): NaiteQuery {
    // Filter and return new NaiteQuery
  }

  result(): any[] {
    // Return final data array
  }

  first(): any | undefined {
    // First data
  }

  last(): any | undefined {
    // Last data
  }

  at(index: number): any | undefined {
    // nth data
  }
}

matchesPattern Logic

function matchesPattern(key: string, pattern: string): boolean {
  const keyParts = key.split(":");
  const patternParts = pattern.split(":");

  // Last is * β†’ prefix matching (any length)
  if (patternParts[patternParts.length - 1] === "*") {
    const prefixParts = patternParts.slice(0, -1);
    return prefixParts.every((part, i) => part === keyParts[i]);
  }

  // Must be same length
  if (patternParts.length !== keyParts.length) {
    return false;
  }

  // Each part is * or exact match
  return patternParts.every((part, i) =>
    part === "*" || part === keyParts[i]
  );
}

Best Practices

1. Clear Key Structure

// βœ… Correct way: Hierarchical structure
Naite.t("module:function:action", { /* ... */ });

// Use wildcards when querying
Naite.get("module:*")           // Entire module
Naite.get("module:function:*")  // Specific function

2. Optimize Chaining Order

// βœ… Good way: Order by most filtering first
Naite.get("user:*")
  .fromFile("user.model.test.ts")  // File filter (filters most)
  .fromFunction("createUser")      // Function filter
  .where("data.userId", ">", 100)  // Data filter
  .result();

3. Result Verification

// βœ… Correct way: Verify expected results
const logs = Naite.get("user:create").result();
expect(logs.length).toBeGreaterThan(0); // Check not empty

// Verify specific values
expect(logs[0].userId).toBeGreaterThan(0);
expect(logs[0].username).toBe("john");

Cautions

Cautions when using Naite.get():
  1. Empty results: Returns empty array if no logs
  2. Order: Returns in recorded order
  3. Chaining: Each method returns a new NaiteQuery
  4. Wildcards: Understand pattern matching rules
  5. where path: Uses radashi’s get path format

Next Steps