drtrace 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,7 @@ This runs an interactive setup wizard that creates the `_drtrace/` configuration
23
23
  - Configuration guide (`README.md`)
24
24
  - Default agent specification
25
25
 
26
- ### Manual Client Integration
26
+ ### Node.js Usage
27
27
 
28
28
  ```typescript
29
29
  import { DrTrace } from 'drtrace';
@@ -39,7 +39,6 @@ const client = DrTrace.init();
39
39
  // flushIntervalMs: 1000,
40
40
  // maxRetries: 3,
41
41
  // maxQueueSize: 10000,
42
- // timeoutMs: 5000,
43
42
  // });
44
43
 
45
44
  // Attach to console
@@ -50,6 +49,48 @@ console.log('This is captured by DrTrace');
50
49
  console.error('Errors are also captured');
51
50
  ```
52
51
 
52
+ ### Browser Usage (React, Vue, etc.)
53
+
54
+ In browser environments, you must provide `applicationId` and `daemonUrl` explicitly since the browser cannot read config files from the filesystem.
55
+
56
+ ```typescript
57
+ import { DrTrace } from 'drtrace'; // Auto-selects browser entry point
58
+
59
+ // Browser requires explicit options (no config file loading)
60
+ const client = DrTrace.init({
61
+ applicationId: 'my-react-app',
62
+ daemonUrl: 'http://localhost:8001',
63
+ moduleName: 'frontend', // Optional: helps identify log source
64
+ batchSize: 50,
65
+ flushIntervalMs: 1000,
66
+ });
67
+
68
+ // Attach to console
69
+ client.attachToConsole();
70
+
71
+ // Objects are serialized properly
72
+ console.log({ user: 'john', action: 'login' }); // Logs as JSON
73
+ console.error('Something went wrong', new Error('Details'));
74
+ ```
75
+
76
+ **Important for Browser:**
77
+ - `applicationId` is **required** - throws error if missing
78
+ - `daemonUrl` is **required** - throws error if missing
79
+ - CORS must be enabled on the daemon (enabled by default)
80
+ - Objects and errors are automatically serialized to JSON
81
+
82
+ #### CORS Configuration
83
+
84
+ The DrTrace daemon allows all origins by default. To restrict origins:
85
+
86
+ ```bash
87
+ # Allow specific origins
88
+ export DRTRACE_CORS_ORIGINS="http://localhost:3000,https://myapp.com"
89
+
90
+ # Start daemon
91
+ uvicorn drtrace_service.api:app --host localhost --port 8001
92
+ ```
93
+
53
94
  ## Starting the DrTrace Daemon
54
95
 
55
96
  **Important**: The DrTrace daemon must be running before your application can send logs.
@@ -155,13 +196,13 @@ Create a new DrTrace client instance.
155
196
 
156
197
  **Options:**
157
198
  - `applicationId` (required) - Unique application identifier
158
- - `daemonUrl` (optional) - DrTrace daemon URL (default: `http://localhost:8001`)
199
+ - `daemonUrl` (required in browser, optional in Node.js) - DrTrace daemon URL (default: `http://localhost:8001`)
200
+ - `moduleName` (optional) - Module/component name for log grouping (default: `'default'`)
159
201
  - `enabled` (optional) - Enable/disable DrTrace (default: `true`)
160
202
  - `batchSize` (optional) - Batch size for log batching (default: `50`)
161
203
  - `flushIntervalMs` (optional) - Flush interval in milliseconds (default: `1000`)
162
204
  - `maxRetries` (optional) - Retry attempts for failed sends with exponential backoff (default: `3`)
163
205
  - `maxQueueSize` (optional) - Maximum queued log entries before oldest entries are dropped (default: `10000`)
164
- - `timeoutMs` (optional) - Request timeout per batch in milliseconds (default: `5000`)
165
206
 
166
207
  ### `client.attachToConsole()`
167
208
 
@@ -288,6 +288,78 @@ def interact_with_daemon(query: str) -> str:
288
288
 
289
289
  ---
290
290
 
291
+ ## Important: Timestamps Are UTC
292
+
293
+ All timestamps in DrTrace API are **UTC Unix timestamps**:
294
+
295
+ | Field/Param | Format | Timezone |
296
+ |-------------|--------|----------|
297
+ | `ts` (in logs) | Unix float | UTC |
298
+ | `start_ts`, `end_ts` | Unix float | UTC |
299
+ | `since`, `until` | Relative or ISO 8601 | UTC |
300
+
301
+ **Key Rules**:
302
+ - ISO 8601 without timezone (e.g., `2025-12-31T02:44:03`) is interpreted as **UTC**, not local time
303
+ - Relative times (`5m`, `1h`) are relative to server's current UTC time
304
+ - To query with local time, include timezone offset: `2025-12-31T09:44:03+07:00`
305
+
306
+ ### Converting Local Time to UTC
307
+
308
+ **Python:**
309
+ ```python
310
+ from datetime import datetime, timezone, timedelta
311
+
312
+ # Local time (e.g., 2025-12-31 09:44:03 in GMT+7)
313
+ local_time = datetime(2025, 12, 31, 9, 44, 3)
314
+
315
+ # Method 1: If you know your timezone offset
316
+ local_tz = timezone(timedelta(hours=7)) # GMT+7
317
+ local_aware = local_time.replace(tzinfo=local_tz)
318
+ utc_time = local_aware.astimezone(timezone.utc)
319
+ unix_ts = utc_time.timestamp()
320
+ print(f"UTC Unix timestamp: {unix_ts}")
321
+
322
+ # Method 2: Using system timezone
323
+ import time
324
+ unix_ts = time.mktime(local_time.timetuple())
325
+ ```
326
+
327
+ **Bash:**
328
+ ```bash
329
+ # Convert local time to Unix timestamp
330
+ date -d "2025-12-31 09:44:03" +%s
331
+
332
+ # Convert with explicit timezone
333
+ TZ=UTC date -d "2025-12-31T02:44:03" +%s
334
+ ```
335
+
336
+ ### API Query Examples with Timezone
337
+
338
+ ```bash
339
+ # Option 1: Include timezone in ISO 8601 (recommended for local time)
340
+ curl "http://localhost:8001/logs/query?since=2025-12-31T09:44:03%2B07:00"
341
+
342
+ # Option 2: Use relative time (always relative to server UTC time)
343
+ curl "http://localhost:8001/logs/query?since=5m"
344
+
345
+ # Option 3: Convert to UTC Unix timestamp first
346
+ UTC_TS=$(TZ=UTC date -d "2025-12-31T02:44:03" +%s)
347
+ curl "http://localhost:8001/logs/query?start_ts=$UTC_TS"
348
+
349
+ # Option 4: Use ISO 8601 in UTC (note: no timezone = UTC)
350
+ curl "http://localhost:8001/logs/query?since=2025-12-31T02:44:03"
351
+ ```
352
+
353
+ ### Common Timezone Pitfalls
354
+
355
+ 1. **ISO 8601 Without Timezone**: Interpreted as UTC, not local time. If you pass `2025-12-31T09:44:03` thinking it's local 9:44 AM, it will be treated as 9:44 AM UTC.
356
+
357
+ 2. **Server vs Client Timezone**: If your server is in a different timezone than your client, always use explicit UTC timestamps or include timezone offsets.
358
+
359
+ 3. **Relative Times**: `since=5m` means 5 minutes before server's current UTC time, regardless of your local timezone.
360
+
361
+ ---
362
+
291
363
  ## Related Documentation
292
364
 
293
365
  - `docs/daemon-interaction-guide.md` - OpenAPI discovery details
@@ -295,4 +367,4 @@ def interact_with_daemon(query: str) -> str:
295
367
 
296
368
  ---
297
369
 
298
- **Last Updated**: 2025-12-29
370
+ **Last Updated**: 2025-12-31
@@ -11,24 +11,25 @@ You must fully embody this agent's persona and follow all activation instruction
11
11
  <step n="1">Load persona from this current agent file (already in context)</step>
12
12
  <step n="2">Remember: You are a Log Analysis Specialist</step>
13
13
  <step n="3">READ the entire story file BEFORE any analysis - understand the query parsing rules</step>
14
- <step n="4">When processing a user query, try methods in this order:
15
-
16
- **Method 1 (Preferred)**: Python code
14
+ <step n="4">When processing a user query, try methods in this order (see `agents/daemon-method-selection.md` for details):
15
+
16
+ **Method 1 (Preferred)**: HTTP/curl
17
+ - Simple, no Python dependencies, works in any environment
18
+ - Check status: `GET http://localhost:8001/status`
19
+ - Query logs: `GET http://localhost:8001/logs/query?since=5m&application_id=X`
20
+ - Analysis: `GET http://localhost:8001/analysis/why?application_id=X&since=5m`
21
+ - **CRITICAL**: First fetch `/openapi.json` to discover field names (e.g., timestamp is `ts`, NOT `timestamp`)
22
+
23
+ **Method 2 (Fallback)**: Python SDK
24
+ - Use when HTTP is blocked or you need async features
17
25
  - Try: `from drtrace_service.agent_interface import process_agent_query, check_daemon_status`
18
- - If import succeeds: Use `response = await process_agent_query(user_query)` or `asyncio.run(process_agent_query(user_query))`
26
+ - Use `response = await process_agent_query(user_query)` or `asyncio.run(process_agent_query(user_query))`
19
27
  - Return the response string directly (it's already formatted markdown)
20
-
21
- **Method 2 (Fallback)**: HTTP API
22
- - If Python import fails: Use HTTP requests to call DrTrace API endpoints
23
- - **CRITICAL**: First fetch `/openapi.json` to discover field names (e.g., timestamp is `ts`, NOT `timestamp`)
24
- - Check status: `GET http://localhost:8001/status`
25
- - For analysis: `GET http://localhost:8001/analysis/why?application_id=X&start_ts=Y&end_ts=Z`
26
- - Parse the JSON response using field names from OpenAPI schema
27
-
28
+
28
29
  **Method 3 (Last resort)**: CLI commands
29
- - If both Python and HTTP fail: Execute `python -m drtrace_service why --application-id X --since 5m`
30
+ - If both HTTP and Python fail: Execute `python -m drtrace_service why --application-id X --since 5m`
30
31
  - Parse the CLI output and format for the user
31
-
32
+
32
33
  **Important**: Always check daemon status first. If daemon is unavailable, return clear error message with next steps.
33
34
  </step>
34
35
  <step n="5">If information is missing, ask the user for clarification with helpful suggestions</step>
@@ -11,7 +11,7 @@ You must fully embody this agent's persona and follow all activation instruction
11
11
  <step n="1">Load persona from this current agent file (already in context)</step>
12
12
  <step n="2">Remember: You are a Setup Guide Assistant for DrTrace</step>
13
13
  <step n="3">Your primary mission is to walk users through DrTrace setup step-by-step using the help APIs and setup guide, not to guess or skip steps</step>
14
- <step n="4">ALWAYS prefer Method 1 (Python help APIs) → then Method 2 (HTTP help endpoints) → then Method 3 (CLI) in that exact order</step>
14
+ <step n="4">ALWAYS prefer Method 1 (HTTP/curl) → then Method 2 (Python SDK) → then Method 3 (CLI) in that exact order. See `agents/daemon-method-selection.md` for details.</step>
15
15
  <step n="5">For each user interaction, clearly state the current step, what to do next, and how to verify it worked</step>
16
16
  <step n="6">When calling help APIs, use:
17
17
  - `start_setup_guide(language, project_root)` to begin or restart a guide
@@ -32,7 +32,7 @@ You must fully embody this agent's persona and follow all activation instruction
32
32
  <r>NEVER skip verification or pretend steps are complete; use the setup guide and help APIs as the source of truth</r>
33
33
  <r>ALWAYS explain what you are doing when progressing steps or troubleshooting issues</r>
34
34
  <r>Display menu items exactly as defined in the menu section and in the order given</r>
35
- <r>Prefer Python APIs, then HTTP endpoints, then CLI in that order; explain fallbacks when switching methods</r>
35
+ <r>Prefer HTTP/curl, then Python SDK, then CLI in that order; explain fallbacks when switching methods</r>
36
36
  </rules>
37
37
  </activation>
38
38
 
@@ -0,0 +1,28 @@
1
+ import type { ClientOptions, LogLevel } from './types';
2
+ /**
3
+ * DrTrace client for browser environments.
4
+ *
5
+ * Unlike the Node.js client, this does NOT load configuration from files.
6
+ * All options must be passed explicitly to init().
7
+ */
8
+ export declare class DrTrace {
9
+ private queue;
10
+ private logger;
11
+ private enabled;
12
+ private constructor();
13
+ /**
14
+ * Initialize DrTrace for browser environments.
15
+ * Unlike Node.js, browser does not load config from files.
16
+ * All options must be passed explicitly.
17
+ *
18
+ * @param opts - Client options (applicationId and daemonUrl are required)
19
+ * @throws Error if applicationId or daemonUrl are not provided
20
+ */
21
+ static init(opts: ClientOptions): DrTrace;
22
+ attachToConsole(): void;
23
+ detachFromConsole(): void;
24
+ log(message: string, level?: LogLevel, context?: Record<string, unknown>): void;
25
+ error(message: string, context?: Record<string, unknown>): void;
26
+ shutdown(): Promise<void>;
27
+ }
28
+ export * from './types';
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.DrTrace = void 0;
18
+ const transport_1 = require("./transport");
19
+ const queue_1 = require("./queue");
20
+ const logger_1 = require("./logger");
21
+ /**
22
+ * DrTrace client for browser environments.
23
+ *
24
+ * Unlike the Node.js client, this does NOT load configuration from files.
25
+ * All options must be passed explicitly to init().
26
+ */
27
+ class DrTrace {
28
+ constructor(opts) {
29
+ const transport = new transport_1.Transport({
30
+ daemonUrl: opts.daemonUrl,
31
+ maxRetries: opts.maxRetries ?? 3,
32
+ timeoutMs: 5000,
33
+ });
34
+ this.queue = new queue_1.LogQueue({
35
+ transport,
36
+ batchSize: opts.batchSize ?? 50,
37
+ flushIntervalMs: opts.flushIntervalMs ?? 1000,
38
+ maxQueueSize: opts.maxQueueSize ?? 10000,
39
+ });
40
+ this.queue.start();
41
+ this.enabled = opts.enabled ?? true;
42
+ this.logger = new logger_1.DrTraceLogger({
43
+ queue: this.queue,
44
+ applicationId: opts.applicationId,
45
+ moduleName: opts.moduleName,
46
+ logLevel: (opts.logLevel ?? 'info'),
47
+ });
48
+ }
49
+ /**
50
+ * Initialize DrTrace for browser environments.
51
+ * Unlike Node.js, browser does not load config from files.
52
+ * All options must be passed explicitly.
53
+ *
54
+ * @param opts - Client options (applicationId and daemonUrl are required)
55
+ * @throws Error if applicationId or daemonUrl are not provided
56
+ */
57
+ static init(opts) {
58
+ if (!opts.applicationId) {
59
+ throw new Error('applicationId is required for browser usage');
60
+ }
61
+ if (!opts.daemonUrl) {
62
+ throw new Error('daemonUrl is required for browser usage');
63
+ }
64
+ return new DrTrace(opts);
65
+ }
66
+ attachToConsole() {
67
+ if (!this.enabled)
68
+ return;
69
+ this.logger.attachToConsole();
70
+ }
71
+ detachFromConsole() {
72
+ this.logger.detachFromConsole();
73
+ }
74
+ log(message, level = 'info', context) {
75
+ if (!this.enabled)
76
+ return;
77
+ this.logger.log(level, message, context);
78
+ }
79
+ error(message, context) {
80
+ if (!this.enabled)
81
+ return;
82
+ this.logger.log('error', message, context);
83
+ }
84
+ async shutdown() {
85
+ await this.queue.flush();
86
+ this.queue.stop();
87
+ this.detachFromConsole();
88
+ }
89
+ }
90
+ exports.DrTrace = DrTrace;
91
+ __exportStar(require("./types"), exports);
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { DrTrace } from './client';
1
+ export { DrTrace } from './node';
2
2
  export { loadConfig } from './config';
3
3
  export * from './types';
package/dist/index.js CHANGED
@@ -15,8 +15,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.loadConfig = exports.DrTrace = void 0;
18
- var client_1 = require("./client");
19
- Object.defineProperty(exports, "DrTrace", { enumerable: true, get: function () { return client_1.DrTrace; } });
18
+ var node_1 = require("./node");
19
+ Object.defineProperty(exports, "DrTrace", { enumerable: true, get: function () { return node_1.DrTrace; } });
20
20
  var config_1 = require("./config");
21
21
  Object.defineProperty(exports, "loadConfig", { enumerable: true, get: function () { return config_1.loadConfig; } });
22
22
  __exportStar(require("./types"), exports);
package/dist/init.d.ts CHANGED
@@ -12,6 +12,7 @@ export declare class ProjectInitializer {
12
12
  private projectRoot;
13
13
  private drtraceDir;
14
14
  private configPath;
15
+ private copiedAgentFiles;
15
16
  constructor(projectRoot?: string);
16
17
  /**
17
18
  * Run the interactive initialization workflow
@@ -57,6 +58,7 @@ export declare class ProjectInitializer {
57
58
  private copyAgentSpec;
58
59
  /**
59
60
  * Recursively copy all files from sourceDir to targetDir
61
+ * Returns list of relative file paths copied (for summary display)
60
62
  */
61
63
  private copyAgentsRecursive;
62
64
  /**
package/dist/init.js CHANGED
@@ -49,6 +49,8 @@ const prompts_1 = __importDefault(require("prompts"));
49
49
  const config_schema_1 = require("./config-schema");
50
50
  class ProjectInitializer {
51
51
  constructor(projectRoot = process.cwd()) {
52
+ // Track copied agent files for summary display
53
+ this.copiedAgentFiles = [];
52
54
  this.projectRoot = projectRoot;
53
55
  this.drtraceDir = path.join(projectRoot, "_drtrace");
54
56
  this.configPath = path.join(this.drtraceDir, "config.json");
@@ -260,8 +262,9 @@ class ProjectInitializer {
260
262
  console.warn("⚠️ Could not find agents directory");
261
263
  return;
262
264
  }
263
- // Copy all files recursively
264
- this.copyAgentsRecursive(agentsDir, path.join(this.drtraceDir, "agents"));
265
+ // Copy all files recursively and track copied files
266
+ const copiedFiles = this.copyAgentsRecursive(agentsDir, path.join(this.drtraceDir, "agents"));
267
+ this.copiedAgentFiles.push(...copiedFiles);
265
268
  }
266
269
  catch (error) {
267
270
  console.warn(`⚠️ Could not copy agent files: ${error}`);
@@ -269,34 +272,35 @@ class ProjectInitializer {
269
272
  }
270
273
  /**
271
274
  * Recursively copy all files from sourceDir to targetDir
275
+ * Returns list of relative file paths copied (for summary display)
272
276
  */
273
- copyAgentsRecursive(sourceDir, targetDir) {
277
+ copyAgentsRecursive(sourceDir, targetDir, basePath = "") {
274
278
  if (!fs.existsSync(targetDir)) {
275
279
  fs.mkdirSync(targetDir, { recursive: true });
276
280
  }
277
281
  const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
278
- let copiedCount = 0;
282
+ const copiedFiles = [];
279
283
  for (const entry of entries) {
280
284
  const srcPath = path.join(sourceDir, entry.name);
281
285
  const destPath = path.join(targetDir, entry.name);
286
+ const relativePath = basePath ? path.join(basePath, entry.name) : entry.name;
282
287
  if (entry.isDirectory()) {
283
288
  // Recursively copy directories
284
- this.copyAgentsRecursive(srcPath, destPath);
289
+ const subCopied = this.copyAgentsRecursive(srcPath, destPath, relativePath);
290
+ copiedFiles.push(...subCopied);
285
291
  }
286
292
  else {
287
293
  // Copy file (no renaming needed - files are already named correctly)
288
294
  fs.copyFileSync(srcPath, destPath);
289
- copiedCount++;
290
- console.log(`✓ Copied ${entry.name}`);
295
+ copiedFiles.push(relativePath);
296
+ console.log(`✓ Copied ${relativePath}`);
291
297
  }
292
298
  }
293
- if (copiedCount > 0 && !fs.existsSync(path.join(targetDir, "..", ".."))) {
294
- // Only print summary if we're at the top level
295
- const relativeSource = path.relative(process.cwd(), sourceDir);
296
- if (!relativeSource.includes("..")) {
297
- console.log(`✓ Successfully copied ${copiedCount} file(s) from agents/`);
298
- }
299
+ // Print summary only at top level
300
+ if (!basePath && copiedFiles.length > 0) {
301
+ console.log(`✓ Successfully copied ${copiedFiles.length} file(s) from agents/`);
299
302
  }
303
+ return copiedFiles;
300
304
  }
301
305
  /**
302
306
  * Copy framework-specific integration guides to _drtrace/agents/integration-guides/
@@ -781,11 +785,12 @@ python -m drtrace_service status
781
785
  }
782
786
  console.log(` • ${path.join(this.drtraceDir, ".env.example")}`);
783
787
  console.log(` • ${path.join(this.drtraceDir, "README.md")}`);
784
- if (config.agent?.enabled) {
785
- console.log(` • ${path.join(this.drtraceDir, "agents", "log-analysis.md")}`);
786
- console.log(` • ${path.join(this.drtraceDir, "agents", "log-it.md")}`);
787
- console.log(` • ${path.join(this.drtraceDir, "agents", "log-init.md")}`);
788
- console.log(` • ${path.join(this.drtraceDir, "agents", "log-help.md")}`);
788
+ // List all copied agent files (includes integration-guides/)
789
+ if (this.copiedAgentFiles.length > 0) {
790
+ console.log("\n📋 Agent Files:");
791
+ for (const agentFile of this.copiedAgentFiles.sort()) {
792
+ console.log(` • ${path.join(this.drtraceDir, "agents", agentFile)}`);
793
+ }
789
794
  }
790
795
  console.log("\n📖 Next Steps:");
791
796
  console.log(` 1. Review ${this.configPath}`);
package/dist/logger.d.ts CHANGED
@@ -3,13 +3,20 @@ import { LogQueue } from './queue';
3
3
  export declare class DrTraceLogger {
4
4
  private queue;
5
5
  private applicationId;
6
+ private moduleName;
6
7
  private logLevel;
7
8
  private originalConsole?;
8
9
  constructor(opts: {
9
10
  queue: LogQueue;
10
11
  applicationId: string;
12
+ moduleName?: string;
11
13
  logLevel: LogLevel;
12
14
  });
15
+ /**
16
+ * Serialize a value to a string suitable for logging.
17
+ * Handles objects, arrays, errors, and primitives correctly.
18
+ */
19
+ private serialize;
13
20
  attachToConsole(): void;
14
21
  detachFromConsole(): void;
15
22
  log(level: LogLevel, message: string, context?: Record<string, unknown>): void;
package/dist/logger.js CHANGED
@@ -5,15 +5,40 @@ class DrTraceLogger {
5
5
  constructor(opts) {
6
6
  this.queue = opts.queue;
7
7
  this.applicationId = opts.applicationId;
8
+ this.moduleName = opts.moduleName || 'default';
8
9
  this.logLevel = opts.logLevel || 'info';
9
10
  }
11
+ /**
12
+ * Serialize a value to a string suitable for logging.
13
+ * Handles objects, arrays, errors, and primitives correctly.
14
+ */
15
+ serialize(value) {
16
+ if (value === null)
17
+ return 'null';
18
+ if (value === undefined)
19
+ return 'undefined';
20
+ if (typeof value === 'string')
21
+ return value;
22
+ if (typeof value === 'number' || typeof value === 'boolean')
23
+ return String(value);
24
+ if (value instanceof Error) {
25
+ return value.stack || `${value.name}: ${value.message}`;
26
+ }
27
+ try {
28
+ return JSON.stringify(value);
29
+ }
30
+ catch {
31
+ // Handle circular references gracefully
32
+ return String(value);
33
+ }
34
+ }
10
35
  attachToConsole() {
11
36
  if (this.originalConsole)
12
37
  return;
13
38
  this.originalConsole = { log: console.log, error: console.error };
14
39
  console.log = (...args) => {
15
40
  try {
16
- this.log('info', args.map(String).join(' '));
41
+ this.log('info', args.map((arg) => this.serialize(arg)).join(' '));
17
42
  }
18
43
  catch {
19
44
  // Silently ignore logging errors
@@ -22,7 +47,7 @@ class DrTraceLogger {
22
47
  };
23
48
  console.error = (...args) => {
24
49
  try {
25
- this.log('error', args.map(String).join(' '));
50
+ this.log('error', args.map((arg) => this.serialize(arg)).join(' '));
26
51
  }
27
52
  catch {
28
53
  // Silently ignore logging errors
@@ -39,8 +64,9 @@ class DrTraceLogger {
39
64
  }
40
65
  log(level, message, context) {
41
66
  const event = {
42
- timestamp: new Date().toISOString(),
43
- applicationId: this.applicationId,
67
+ ts: Date.now() / 1000, // Unix timestamp as float (seconds.milliseconds)
68
+ application_id: this.applicationId,
69
+ module_name: this.moduleName,
44
70
  level,
45
71
  message,
46
72
  context,
package/dist/node.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { ClientOptions, LogLevel } from './types';
2
+ export declare class DrTrace {
3
+ private queue;
4
+ private logger;
5
+ private enabled;
6
+ private constructor();
7
+ static init(opts?: Partial<ClientOptions>): DrTrace;
8
+ attachToConsole(): void;
9
+ detachFromConsole(): void;
10
+ log(message: string, level?: LogLevel, context?: Record<string, unknown>): void;
11
+ error(message: string, context?: Record<string, unknown>): void;
12
+ shutdown(): Promise<void>;
13
+ }
package/dist/node.js ADDED
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DrTrace = void 0;
4
+ const config_1 = require("./config");
5
+ const transport_1 = require("./transport");
6
+ const queue_1 = require("./queue");
7
+ const logger_1 = require("./logger");
8
+ class DrTrace {
9
+ constructor(opts) {
10
+ const transport = new transport_1.Transport({
11
+ daemonUrl: opts.daemonUrl,
12
+ maxRetries: opts.maxRetries ?? 3,
13
+ timeoutMs: 5000,
14
+ });
15
+ this.queue = new queue_1.LogQueue({
16
+ transport,
17
+ batchSize: opts.batchSize ?? 50,
18
+ flushIntervalMs: opts.flushIntervalMs ?? 1000,
19
+ maxQueueSize: opts.maxQueueSize ?? 10000,
20
+ });
21
+ this.queue.start();
22
+ this.enabled = opts.enabled ?? true;
23
+ this.logger = new logger_1.DrTraceLogger({
24
+ queue: this.queue,
25
+ applicationId: opts.applicationId,
26
+ moduleName: opts.moduleName,
27
+ logLevel: (opts.logLevel ?? 'info'),
28
+ });
29
+ }
30
+ static init(opts) {
31
+ const cfg = (0, config_1.loadConfig)({ projectRoot: '.', environment: process.env.NODE_ENV });
32
+ const merged = {
33
+ applicationId: cfg.drtrace.applicationId,
34
+ daemonUrl: cfg.drtrace.daemonUrl || 'http://localhost:8001',
35
+ enabled: cfg.drtrace.enabled ?? true,
36
+ logLevel: (cfg.drtrace.logLevel ?? 'info'),
37
+ batchSize: cfg.drtrace.batchSize ?? 50,
38
+ flushIntervalMs: cfg.drtrace.flushIntervalMs ?? 1000,
39
+ ...opts,
40
+ };
41
+ return new DrTrace(merged);
42
+ }
43
+ attachToConsole() {
44
+ if (!this.enabled)
45
+ return;
46
+ this.logger.attachToConsole();
47
+ }
48
+ detachFromConsole() {
49
+ this.logger.detachFromConsole();
50
+ }
51
+ log(message, level = 'info', context) {
52
+ if (!this.enabled)
53
+ return;
54
+ this.logger.log(level, message, context);
55
+ }
56
+ error(message, context) {
57
+ if (!this.enabled)
58
+ return;
59
+ this.logger.log('error', message, context);
60
+ }
61
+ async shutdown() {
62
+ await this.queue.flush();
63
+ this.queue.stop();
64
+ this.detachFromConsole();
65
+ }
66
+ }
67
+ exports.DrTrace = DrTrace;
@@ -288,6 +288,78 @@ def interact_with_daemon(query: str) -> str:
288
288
 
289
289
  ---
290
290
 
291
+ ## Important: Timestamps Are UTC
292
+
293
+ All timestamps in DrTrace API are **UTC Unix timestamps**:
294
+
295
+ | Field/Param | Format | Timezone |
296
+ |-------------|--------|----------|
297
+ | `ts` (in logs) | Unix float | UTC |
298
+ | `start_ts`, `end_ts` | Unix float | UTC |
299
+ | `since`, `until` | Relative or ISO 8601 | UTC |
300
+
301
+ **Key Rules**:
302
+ - ISO 8601 without timezone (e.g., `2025-12-31T02:44:03`) is interpreted as **UTC**, not local time
303
+ - Relative times (`5m`, `1h`) are relative to server's current UTC time
304
+ - To query with local time, include timezone offset: `2025-12-31T09:44:03+07:00`
305
+
306
+ ### Converting Local Time to UTC
307
+
308
+ **Python:**
309
+ ```python
310
+ from datetime import datetime, timezone, timedelta
311
+
312
+ # Local time (e.g., 2025-12-31 09:44:03 in GMT+7)
313
+ local_time = datetime(2025, 12, 31, 9, 44, 3)
314
+
315
+ # Method 1: If you know your timezone offset
316
+ local_tz = timezone(timedelta(hours=7)) # GMT+7
317
+ local_aware = local_time.replace(tzinfo=local_tz)
318
+ utc_time = local_aware.astimezone(timezone.utc)
319
+ unix_ts = utc_time.timestamp()
320
+ print(f"UTC Unix timestamp: {unix_ts}")
321
+
322
+ # Method 2: Using system timezone
323
+ import time
324
+ unix_ts = time.mktime(local_time.timetuple())
325
+ ```
326
+
327
+ **Bash:**
328
+ ```bash
329
+ # Convert local time to Unix timestamp
330
+ date -d "2025-12-31 09:44:03" +%s
331
+
332
+ # Convert with explicit timezone
333
+ TZ=UTC date -d "2025-12-31T02:44:03" +%s
334
+ ```
335
+
336
+ ### API Query Examples with Timezone
337
+
338
+ ```bash
339
+ # Option 1: Include timezone in ISO 8601 (recommended for local time)
340
+ curl "http://localhost:8001/logs/query?since=2025-12-31T09:44:03%2B07:00"
341
+
342
+ # Option 2: Use relative time (always relative to server UTC time)
343
+ curl "http://localhost:8001/logs/query?since=5m"
344
+
345
+ # Option 3: Convert to UTC Unix timestamp first
346
+ UTC_TS=$(TZ=UTC date -d "2025-12-31T02:44:03" +%s)
347
+ curl "http://localhost:8001/logs/query?start_ts=$UTC_TS"
348
+
349
+ # Option 4: Use ISO 8601 in UTC (note: no timezone = UTC)
350
+ curl "http://localhost:8001/logs/query?since=2025-12-31T02:44:03"
351
+ ```
352
+
353
+ ### Common Timezone Pitfalls
354
+
355
+ 1. **ISO 8601 Without Timezone**: Interpreted as UTC, not local time. If you pass `2025-12-31T09:44:03` thinking it's local 9:44 AM, it will be treated as 9:44 AM UTC.
356
+
357
+ 2. **Server vs Client Timezone**: If your server is in a different timezone than your client, always use explicit UTC timestamps or include timezone offsets.
358
+
359
+ 3. **Relative Times**: `since=5m` means 5 minutes before server's current UTC time, regardless of your local timezone.
360
+
361
+ ---
362
+
291
363
  ## Related Documentation
292
364
 
293
365
  - `docs/daemon-interaction-guide.md` - OpenAPI discovery details
@@ -295,4 +367,4 @@ def interact_with_daemon(query: str) -> str:
295
367
 
296
368
  ---
297
369
 
298
- **Last Updated**: 2025-12-29
370
+ **Last Updated**: 2025-12-31
@@ -11,24 +11,25 @@ You must fully embody this agent's persona and follow all activation instruction
11
11
  <step n="1">Load persona from this current agent file (already in context)</step>
12
12
  <step n="2">Remember: You are a Log Analysis Specialist</step>
13
13
  <step n="3">READ the entire story file BEFORE any analysis - understand the query parsing rules</step>
14
- <step n="4">When processing a user query, try methods in this order:
15
-
16
- **Method 1 (Preferred)**: Python code
14
+ <step n="4">When processing a user query, try methods in this order (see `agents/daemon-method-selection.md` for details):
15
+
16
+ **Method 1 (Preferred)**: HTTP/curl
17
+ - Simple, no Python dependencies, works in any environment
18
+ - Check status: `GET http://localhost:8001/status`
19
+ - Query logs: `GET http://localhost:8001/logs/query?since=5m&application_id=X`
20
+ - Analysis: `GET http://localhost:8001/analysis/why?application_id=X&since=5m`
21
+ - **CRITICAL**: First fetch `/openapi.json` to discover field names (e.g., timestamp is `ts`, NOT `timestamp`)
22
+
23
+ **Method 2 (Fallback)**: Python SDK
24
+ - Use when HTTP is blocked or you need async features
17
25
  - Try: `from drtrace_service.agent_interface import process_agent_query, check_daemon_status`
18
- - If import succeeds: Use `response = await process_agent_query(user_query)` or `asyncio.run(process_agent_query(user_query))`
26
+ - Use `response = await process_agent_query(user_query)` or `asyncio.run(process_agent_query(user_query))`
19
27
  - Return the response string directly (it's already formatted markdown)
20
-
21
- **Method 2 (Fallback)**: HTTP API
22
- - If Python import fails: Use HTTP requests to call DrTrace API endpoints
23
- - **CRITICAL**: First fetch `/openapi.json` to discover field names (e.g., timestamp is `ts`, NOT `timestamp`)
24
- - Check status: `GET http://localhost:8001/status`
25
- - For analysis: `GET http://localhost:8001/analysis/why?application_id=X&start_ts=Y&end_ts=Z`
26
- - Parse the JSON response using field names from OpenAPI schema
27
-
28
+
28
29
  **Method 3 (Last resort)**: CLI commands
29
- - If both Python and HTTP fail: Execute `python -m drtrace_service why --application-id X --since 5m`
30
+ - If both HTTP and Python fail: Execute `python -m drtrace_service why --application-id X --since 5m`
30
31
  - Parse the CLI output and format for the user
31
-
32
+
32
33
  **Important**: Always check daemon status first. If daemon is unavailable, return clear error message with next steps.
33
34
  </step>
34
35
  <step n="5">If information is missing, ask the user for clarification with helpful suggestions</step>
@@ -11,7 +11,7 @@ You must fully embody this agent's persona and follow all activation instruction
11
11
  <step n="1">Load persona from this current agent file (already in context)</step>
12
12
  <step n="2">Remember: You are a Setup Guide Assistant for DrTrace</step>
13
13
  <step n="3">Your primary mission is to walk users through DrTrace setup step-by-step using the help APIs and setup guide, not to guess or skip steps</step>
14
- <step n="4">ALWAYS prefer Method 1 (Python help APIs) → then Method 2 (HTTP help endpoints) → then Method 3 (CLI) in that exact order</step>
14
+ <step n="4">ALWAYS prefer Method 1 (HTTP/curl) → then Method 2 (Python SDK) → then Method 3 (CLI) in that exact order. See `agents/daemon-method-selection.md` for details.</step>
15
15
  <step n="5">For each user interaction, clearly state the current step, what to do next, and how to verify it worked</step>
16
16
  <step n="6">When calling help APIs, use:
17
17
  - `start_setup_guide(language, project_root)` to begin or restart a guide
@@ -32,7 +32,7 @@ You must fully embody this agent's persona and follow all activation instruction
32
32
  <r>NEVER skip verification or pretend steps are complete; use the setup guide and help APIs as the source of truth</r>
33
33
  <r>ALWAYS explain what you are doing when progressing steps or troubleshooting issues</r>
34
34
  <r>Display menu items exactly as defined in the menu section and in the order given</r>
35
- <r>Prefer Python APIs, then HTTP endpoints, then CLI in that order; explain fallbacks when switching methods</r>
35
+ <r>Prefer HTTP/curl, then Python SDK, then CLI in that order; explain fallbacks when switching methods</r>
36
36
  </rules>
37
37
  </activation>
38
38
 
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  #pragma once
9
+ #define DRTRACE_VERSION "0.4.0"
9
10
 
10
11
  // Standard library includes required for header-only implementation
11
12
  #include <atomic>
package/dist/transport.js CHANGED
@@ -15,7 +15,11 @@ class Transport {
15
15
  if (events.length === 0)
16
16
  return;
17
17
  const url = `${this.daemonUrl}/logs/ingest`;
18
- const payload = { logs: events };
18
+ // Wrap batch with application_id at top level as required by daemon API
19
+ const payload = {
20
+ application_id: events[0].application_id,
21
+ logs: events,
22
+ };
19
23
  const fetchImpl = globalThis.fetch;
20
24
  if (!fetchImpl)
21
25
  return; // Non-blocking skip
package/dist/types.d.ts CHANGED
@@ -1,14 +1,20 @@
1
1
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
2
  export interface LogEvent {
3
- timestamp: string;
4
- applicationId: string;
3
+ ts: number;
5
4
  level: LogLevel;
6
5
  message: string;
6
+ application_id: string;
7
+ module_name: string;
7
8
  context?: Record<string, unknown>;
8
9
  }
10
+ export interface LogBatch {
11
+ application_id: string;
12
+ logs: LogEvent[];
13
+ }
9
14
  export interface ClientOptions {
10
15
  applicationId: string;
11
16
  daemonUrl: string;
17
+ moduleName?: string;
12
18
  enabled?: boolean;
13
19
  logLevel?: LogLevel;
14
20
  batchSize?: number;
package/package.json CHANGED
@@ -1,13 +1,20 @@
1
1
  {
2
2
  "name": "drtrace",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "DrTrace JavaScript/TypeScript client",
5
5
  "main": "dist/index.js",
6
+ "browser": "dist/browser.js",
6
7
  "types": "dist/index.d.ts",
7
8
  "exports": {
8
9
  ".": {
9
10
  "types": "./dist/index.d.ts",
11
+ "browser": "./dist/browser.js",
12
+ "node": "./dist/index.js",
10
13
  "default": "./dist/index.js"
14
+ },
15
+ "./browser": {
16
+ "types": "./dist/browser.d.ts",
17
+ "default": "./dist/browser.js"
11
18
  }
12
19
  },
13
20
  "bin": {
@@ -45,7 +52,10 @@
45
52
  "error-tracking",
46
53
  "debugging"
47
54
  ],
48
- "author": "DrTrace Team",
55
+ "author": {
56
+ "name": "Thanh Cao",
57
+ "email": "caoduythanhcantho@gmail.com"
58
+ },
49
59
  "license": "MIT",
50
60
  "files": [
51
61
  "dist/",
@@ -55,7 +65,14 @@
55
65
  ],
56
66
  "repository": {
57
67
  "type": "git",
58
- "url": "https://github.com/example/drtrace.git",
68
+ "url": "git+https://github.com/CaoDuyThanh/drtrace.git",
59
69
  "directory": "packages/javascript/drtrace-client"
70
+ },
71
+ "homepage": "https://github.com/CaoDuyThanh/drtrace#readme",
72
+ "bugs": {
73
+ "url": "https://github.com/CaoDuyThanh/drtrace/issues"
74
+ },
75
+ "engines": {
76
+ "node": ">=16.0.0"
60
77
  }
61
78
  }