logquill 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
@@ -26,7 +26,9 @@ platforms — are implemented; non-blocking async dispatch is not yet — see
26
26
  - **Child loggers** — `.child()` inherits level, transports, and plugins, and merges its own `meta` on top
27
27
  - **Typed throughout** — TypeScript strict mode, no `any` in the public API
28
28
  - **Dual package** — works via both `require()` (CJS) and `import` (ESM) from the same published package
29
- - *(planned)* non-blocking async dispatch, `AsyncLocalStorage`-based context propagation — see `CHANGELOG.md`
29
+ - **Tracing & agentic logging** — `.thought()/.action()/.observation()/.decision()`, `Logger.span()` for nested/durationMs-stamped spans, `RunPlugin` (per-run id + step counter), and `TraceContextPlugin` (cross-service `traceId`, OTel-aware) — see [Tracing & agentic logging](#tracing--agentic-logging)
30
+ - **LangChain.js / LangGraph.js adapter** — `LangChainAdapter`, a `BaseCallbackHandler` that maps chain/LLM/tool/agent events onto the calls above with zero manual instrumentation, from a separate `logquill/langchain` entry point — see [Agentic framework adapters](#agentic-framework-adapters)
31
+ - *(planned)* non-blocking async dispatch, `AsyncLocalStorage`-based general context propagation — see `CHANGELOG.md`
30
32
 
31
33
  ## Contents
32
34
 
@@ -42,6 +44,8 @@ platforms — are implemented; non-blocking async dispatch is not yet — see
42
44
  - [PII redaction](#pii-redaction)
43
45
  - [Tamper-evident logs](#tamper-evident-logs)
44
46
  - [Alerting](#alerting)
47
+ - [Tracing & agentic logging](#tracing--agentic-logging)
48
+ - [Agentic framework adapters](#agentic-framework-adapters)
45
49
  - [Development](#development)
46
50
  - [License](#license)
47
51
 
@@ -502,6 +506,121 @@ logger.error("payment webhook failed", { orderId: "o-123" });
502
506
  inject a `sender`) follow the same shape. Write your own by extending
503
507
  `AlertingPlugin` and implementing `sendAlert(record, occurrences)`.
504
508
 
509
+ ## Tracing & agentic logging
510
+
511
+ `.thought()/.action()/.observation()/.decision()` are `.info()` with
512
+ `meta.kind` pre-set, for tagging steps of an agent's reasoning loop:
513
+
514
+ ```ts
515
+ import { Logger } from "logquill";
516
+
517
+ const logger = new Logger("agent");
518
+ logger.thought("deciding which tool to call", { candidates: ["search", "calculator"] });
519
+ logger.action("calling search", { query: "current weather in nyc" });
520
+ logger.observation("search returned 3 results");
521
+ logger.decision("using search result #1");
522
+ ```
523
+
524
+ `Logger.span()` wraps an operation, emitting one record on completion with
525
+ `meta.spanId`/`meta.durationMs` — every record logged inside it, including
526
+ across an `await`, is automatically stamped with `meta.parentSpanId`, so a
527
+ full run's nesting can be reconstructed by sorting on `spanId`/`parentSpanId`.
528
+ It still emits (at `ERROR`, with `meta.error`) and rethrows if the block
529
+ throws:
530
+
531
+ ```ts
532
+ import { Logger } from "logquill";
533
+
534
+ async function callLlm(prompt: string): Promise<string> {
535
+ return `response to: ${prompt}`;
536
+ }
537
+
538
+ const logger = new Logger("agent");
539
+
540
+ const answer = await logger.span(
541
+ "callLlm",
542
+ async () => {
543
+ logger.action("requesting completion", { model: "gpt-4" });
544
+ const response = await callLlm("current weather in nyc");
545
+ logger.observation("received completion");
546
+ return response;
547
+ },
548
+ { model: "gpt-4" },
549
+ );
550
+ ```
551
+
552
+ `RunPlugin` stamps `meta.runId` (generated, or given explicitly) plus an
553
+ incrementing `meta.step` — attach a fresh instance per run so concurrent
554
+ runs don't share a counter:
555
+
556
+ ```ts
557
+ import { Logger, RunPlugin } from "logquill";
558
+
559
+ const runLogger = new Logger("app").child("agent").use(new RunPlugin());
560
+ runLogger.thought("step one"); // meta: { kind: "thought", runId: "...", step: 0 }
561
+ runLogger.action("step two"); // meta: { kind: "action", runId: "...", step: 1 }
562
+ ```
563
+
564
+ `TraceContextPlugin` stamps `meta.traceId`, for correlating one request
565
+ across services — distinct from `runId`, which scopes one agent run. It
566
+ resolves, in priority order: an active OpenTelemetry span (if
567
+ `@opentelemetry/api` is installed — never a required dependency), an
568
+ inbound `traceparent`/X-Ray/GCP trace header, or a freshly generated id:
569
+
570
+ ```ts
571
+ import { Logger, setTraceparent, TraceContextPlugin } from "logquill";
572
+
573
+ const logger = new Logger("app", { plugins: [new TraceContextPlugin()] });
574
+
575
+ // in HTTP middleware, before the handler runs:
576
+ const reset = setTraceparent(req.headers["traceparent"]);
577
+ try {
578
+ logger.info("handling request"); // meta.traceId resolved from the inbound header
579
+ } finally {
580
+ reset();
581
+ }
582
+ ```
583
+
584
+ ## Agentic framework adapters
585
+
586
+ `LangChainAdapter` implements LangChain.js's `BaseCallbackHandler`, mapping
587
+ chain/LLM/tool/agent events onto `.action()/.observation()/.decision()` and
588
+ `span()`-shaped records — pass it into `callbacks: [...]` and a chain's
589
+ full call tree is captured with zero manual instrumentation:
590
+
591
+ ```ts
592
+ import { RunnableLambda } from "@langchain/core/runnables";
593
+ import { Logger, RunPlugin } from "logquill";
594
+ import { LangChainAdapter } from "logquill/langchain";
595
+
596
+ const logger = new Logger("agent").use(new RunPlugin());
597
+ const handler = new LangChainAdapter(logger);
598
+
599
+ const answerQuestion = RunnableLambda.from((question: string) => `answer: ${question}`);
600
+ await answerQuestion.invoke("what is 2+2?", { callbacks: [handler] });
601
+ // logs one record: { message: "RunnableLambda", meta: { kind: "span", spanId: "...", durationMs: ... } }
602
+ ```
603
+
604
+ `LangGraphAdapter` is the same handler under its own name, for LangGraph.js
605
+ graphs — its nodes run as ordinary LangChain `Runnable`s, so no extra
606
+ mapping is needed; pass it the same way:
607
+
608
+ ```ts
609
+ import { LangGraphAdapter } from "logquill/langchain";
610
+
611
+ const handler = new LangGraphAdapter(logger);
612
+ // const graph = builder.compile({ checkpointer });
613
+ // await graph.invoke(input, { callbacks: [handler], configurable: { thread_id: "1" } });
614
+ ```
615
+
616
+ This is a **separate entry point** — `import ... from "logquill/langchain"`,
617
+ not the main `"logquill"` import — because `LangChainAdapter` has to
618
+ `extends BaseCallbackHandler`, LangChain's own class. Importing plain
619
+ `logquill` never touches `@langchain/core`; only importing
620
+ `logquill/langchain` does. Install `@langchain/core` yourself (it's an
621
+ optional peer dependency) — no separate `@langchain/langgraph` dependency
622
+ is needed for `LangGraphAdapter`.
623
+
505
624
  ## Development
506
625
 
507
626
  ```sh
package/dist/index.cjs CHANGED
@@ -1,10 +1,13 @@
1
1
  'use strict';
2
2
 
3
3
  var crypto = require('crypto');
4
+ var async_hooks = require('async_hooks');
5
+ var module$1 = require('module');
4
6
  var zlib = require('zlib');
5
7
  var fs = require('fs');
6
8
  var path = require('path');
7
9
 
10
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
8
11
  // src/core/levels.ts
9
12
  var Level = /* @__PURE__ */ ((Level2) => {
10
13
  Level2[Level2["TRACE"] = 5] = "TRACE";
@@ -82,6 +85,99 @@ var ContextPlugin = class {
82
85
  return { ...record, meta: { ...this.context, ...record.meta } };
83
86
  }
84
87
  };
88
+ var RunPlugin = class {
89
+ runId;
90
+ step = 0;
91
+ constructor(options = {}) {
92
+ this.runId = options.runId ?? crypto.randomUUID();
93
+ }
94
+ beforeLog(record) {
95
+ record.meta.runId ??= this.runId;
96
+ record.meta.step = this.step++;
97
+ return record;
98
+ }
99
+ };
100
+ var traceparentStore = new async_hooks.AsyncLocalStorage();
101
+ function setTraceparent(value) {
102
+ const previous = traceparentStore.getStore();
103
+ traceparentStore.enterWith(value);
104
+ return () => {
105
+ traceparentStore.enterWith(previous);
106
+ };
107
+ }
108
+ function getTraceparent() {
109
+ return traceparentStore.getStore();
110
+ }
111
+ function generateTraceId() {
112
+ return crypto.randomBytes(16).toString("hex");
113
+ }
114
+ var W3C_TRACEPARENT_RE = /^[0-9a-f]{2}-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$/;
115
+ var XRAY_ROOT_RE = /Root=1-([0-9a-f]{8})-([0-9a-f]{24})/;
116
+ var GCP_TRACE_RE = /^([0-9a-f]{32})\/\d+(;o=\d)?$/;
117
+ function parseTraceHeader(header) {
118
+ const trimmed = header.trim();
119
+ const w3c = W3C_TRACEPARENT_RE.exec(trimmed);
120
+ if (w3c?.[1]) {
121
+ return w3c[1];
122
+ }
123
+ const xray = XRAY_ROOT_RE.exec(trimmed);
124
+ if (xray?.[1] && xray[2]) {
125
+ return xray[1] + xray[2];
126
+ }
127
+ const gcp = GCP_TRACE_RE.exec(trimmed);
128
+ if (gcp?.[1]) {
129
+ return gcp[1];
130
+ }
131
+ return void 0;
132
+ }
133
+ function defaultResolveActiveOtelTraceId() {
134
+ try {
135
+ const require2 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
136
+ const otel = require2("@opentelemetry/api");
137
+ const span = otel.trace.getActiveSpan();
138
+ if (!span) {
139
+ return void 0;
140
+ }
141
+ const spanContext = span.spanContext();
142
+ if (!otel.trace.isSpanContextValid(spanContext)) {
143
+ return void 0;
144
+ }
145
+ return spanContext.traceId;
146
+ } catch {
147
+ return void 0;
148
+ }
149
+ }
150
+ var TraceContextPlugin = class {
151
+ traceKey;
152
+ explicitTraceparent;
153
+ resolveActiveOtelTraceId;
154
+ constructor(options = {}) {
155
+ this.traceKey = options.traceKey ?? "traceId";
156
+ this.explicitTraceparent = options.traceparent;
157
+ this.resolveActiveOtelTraceId = options.resolveActiveOtelTraceId ?? defaultResolveActiveOtelTraceId;
158
+ }
159
+ beforeLog(record) {
160
+ if (record.meta[this.traceKey] != null) {
161
+ return record;
162
+ }
163
+ record.meta[this.traceKey] = this.resolveTraceId();
164
+ return record;
165
+ }
166
+ resolveTraceId() {
167
+ const fromOtel = this.resolveActiveOtelTraceId();
168
+ if (fromOtel) {
169
+ return fromOtel;
170
+ }
171
+ const header = this.explicitTraceparent ?? getTraceparent();
172
+ if (header) {
173
+ const parsed = parseTraceHeader(header);
174
+ if (parsed) {
175
+ return parsed;
176
+ }
177
+ }
178
+ return generateTraceId();
179
+ }
180
+ };
85
181
 
86
182
  // src/plugins/redact-plugin.ts
87
183
  var DEFAULT_REDACTED_KEYS = ["password", "token", "secret", "api_key", "authorization"];
@@ -1710,8 +1806,24 @@ var HTTPTransport = class extends Transport {
1710
1806
  this.flush();
1711
1807
  }
1712
1808
  };
1809
+ var spanIdStore = new async_hooks.AsyncLocalStorage();
1810
+ function currentSpanId() {
1811
+ return spanIdStore.getStore();
1812
+ }
1813
+ function newSpanId() {
1814
+ return crypto.randomBytes(8).toString("hex");
1815
+ }
1816
+ function runInSpan(spanId, fn) {
1817
+ return spanIdStore.run(spanId, fn);
1818
+ }
1713
1819
 
1714
1820
  // src/core/logger.ts
1821
+ function formatSpanError(error) {
1822
+ if (error instanceof Error) {
1823
+ return `${error.name}: ${error.message}`;
1824
+ }
1825
+ return String(error);
1826
+ }
1715
1827
  var Logger = class _Logger {
1716
1828
  name;
1717
1829
  transports;
@@ -1775,6 +1887,10 @@ var Logger = class _Logger {
1775
1887
  message,
1776
1888
  meta: { ...this.baseMeta, ...meta }
1777
1889
  });
1890
+ const parentSpanId = currentSpanId();
1891
+ if (parentSpanId !== void 0) {
1892
+ record.meta.parentSpanId ??= parentSpanId;
1893
+ }
1778
1894
  for (const plugin of this.plugins) {
1779
1895
  let result;
1780
1896
  try {
@@ -1789,7 +1905,11 @@ var Logger = class _Logger {
1789
1905
  record = result;
1790
1906
  }
1791
1907
  for (const transport of this.transports) {
1792
- transport.write(transport.format(record), record);
1908
+ try {
1909
+ transport.write(transport.format(record), record);
1910
+ } catch (error) {
1911
+ console.error(`${transport.constructor.name}: failed to write a log record`, error);
1912
+ }
1793
1913
  }
1794
1914
  for (const plugin of this.plugins) {
1795
1915
  try {
@@ -1818,6 +1938,66 @@ var Logger = class _Logger {
1818
1938
  fatal(message, meta = {}) {
1819
1939
  return this.dispatch(50 /* FATAL */, message, meta);
1820
1940
  }
1941
+ /** `.info()` tagged `meta.kind = "thought"` — an agent's internal reasoning step, for harness/agentic tracing. */
1942
+ thought(message, meta = {}) {
1943
+ return this.dispatch(20 /* INFO */, message, { kind: "thought", ...meta });
1944
+ }
1945
+ /** `.info()` tagged `meta.kind = "action"` — an agent taking an action (a tool call, an LLM request), for harness/agentic tracing. */
1946
+ action(message, meta = {}) {
1947
+ return this.dispatch(20 /* INFO */, message, { kind: "action", ...meta });
1948
+ }
1949
+ /** `.info()` tagged `meta.kind = "observation"` — the result an agent observed from an action, for harness/agentic tracing. */
1950
+ observation(message, meta = {}) {
1951
+ return this.dispatch(20 /* INFO */, message, { kind: "observation", ...meta });
1952
+ }
1953
+ /** `.info()` tagged `meta.kind = "decision"` — an agent's concluding decision for a step or run, for harness/agentic tracing. */
1954
+ decision(message, meta = {}) {
1955
+ return this.dispatch(20 /* INFO */, message, { kind: "decision", ...meta });
1956
+ }
1957
+ /**
1958
+ * `await logger.span("callLlm", async () => {...})` — runs `fn`, and on
1959
+ * settling (success or throw) emits one record for the span itself
1960
+ * carrying `meta.spanId` and `meta.durationMs`. Every record logged
1961
+ * inside `fn` — through any method, and through any further `await` —
1962
+ * is automatically stamped with `meta.parentSpanId` pointing at this
1963
+ * span, so nested/sub-agent calls reconstruct their exact nesting when
1964
+ * sorted by `spanId`/`parentSpanId`.
1965
+ *
1966
+ * Still emits its record — at `ERROR`, with `meta.error` set — if `fn`
1967
+ * throws; the error itself propagates unchanged to the caller.
1968
+ *
1969
+ * `spanId`/`parentSpanId` normally auto-generate/auto-nest; pass them in
1970
+ * `options` to adopt an id handed in from elsewhere (e.g. a framework
1971
+ * adapter translating an id it already received).
1972
+ */
1973
+ async span(name, fn, options = {}) {
1974
+ const { spanId: explicitSpanId, parentSpanId: explicitParentSpanId, ...meta } = options;
1975
+ const spanId = explicitSpanId ?? newSpanId();
1976
+ const start = performance.now();
1977
+ try {
1978
+ const result = await runInSpan(spanId, () => fn());
1979
+ this.finishSpan(name, spanId, explicitParentSpanId, performance.now() - start, meta);
1980
+ return result;
1981
+ } catch (error) {
1982
+ this.finishSpan(name, spanId, explicitParentSpanId, performance.now() - start, meta, error);
1983
+ throw error;
1984
+ }
1985
+ }
1986
+ finishSpan(name, spanId, explicitParentSpanId, durationMs, meta, error) {
1987
+ const fullMeta = {
1988
+ spanId,
1989
+ durationMs: Math.round(durationMs * 1e3) / 1e3,
1990
+ ...meta
1991
+ };
1992
+ if (explicitParentSpanId !== void 0) {
1993
+ fullMeta.parentSpanId = explicitParentSpanId;
1994
+ }
1995
+ fullMeta.kind ??= "span";
1996
+ if (error !== void 0) {
1997
+ fullMeta.error = formatSpanError(error);
1998
+ }
1999
+ this.dispatch(error !== void 0 ? 40 /* ERROR */ : 20 /* INFO */, name, fullMeta);
2000
+ }
1821
2001
  };
1822
2002
 
1823
2003
  // src/index.ts
@@ -1857,16 +2037,23 @@ exports.PubSubTransport = PubSubTransport;
1857
2037
  exports.RabbitMQTransport = RabbitMQTransport;
1858
2038
  exports.RedactPlugin = RedactPlugin;
1859
2039
  exports.RedisTransport = RedisTransport;
2040
+ exports.RunPlugin = RunPlugin;
1860
2041
  exports.SQLiteTransport = SQLiteTransport;
1861
2042
  exports.SQSTransport = SQSTransport;
1862
2043
  exports.SamplingPlugin = SamplingPlugin;
1863
2044
  exports.SlackAlertPlugin = SlackAlertPlugin;
1864
2045
  exports.TamperEvidentPlugin = TamperEvidentPlugin;
2046
+ exports.TraceContextPlugin = TraceContextPlugin;
1865
2047
  exports.Transport = Transport;
1866
2048
  exports.VERSION = VERSION;
1867
2049
  exports.createRecord = createRecord;
2050
+ exports.defaultResolveActiveOtelTraceId = defaultResolveActiveOtelTraceId;
2051
+ exports.generateTraceId = generateTraceId;
2052
+ exports.getTraceparent = getTraceparent;
1868
2053
  exports.levelName = levelName;
1869
2054
  exports.parseLevel = parseLevel;
2055
+ exports.parseTraceHeader = parseTraceHeader;
2056
+ exports.setTraceparent = setTraceparent;
1870
2057
  exports.utcTimestamp = utcTimestamp;
1871
2058
  //# sourceMappingURL=index.cjs.map
1872
2059
  //# sourceMappingURL=index.cjs.map