@lensmcp/node-instrumentation 1.24.5 → 1.25.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/lib/http-capture.d.ts +31 -0
- package/lib/http-capture.js +2 -0
- package/lib/http-store.d.ts +23 -0
- package/lib/http-store.js +1 -0
- package/lib/tap-egress.d.ts +2 -0
- package/lib/tap-egress.js +1 -1
- package/lib/tap-fs.d.ts +36 -0
- package/lib/tap-fs.js +1 -1
- package/lib/tap-pg.js +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { HttpExchange, HttpExchangeBody } from '@lensmcp/core/http-exchange';
|
|
2
|
+
import { type HttpCaptureMode } from './http-store';
|
|
3
|
+
type FetchInput = Parameters<typeof fetch>[0];
|
|
4
|
+
type FetchInit = Parameters<typeof fetch>[1];
|
|
5
|
+
export interface RequestSide {
|
|
6
|
+
headers: Array<[string, string]>;
|
|
7
|
+
contentType: string;
|
|
8
|
+
/** The body as bytes, when it could be read without consuming it. */
|
|
9
|
+
bytes?: Uint8Array;
|
|
10
|
+
pending?: Promise<Uint8Array | undefined>;
|
|
11
|
+
size?: number;
|
|
12
|
+
omitted?: HttpExchangeBody['omitted'];
|
|
13
|
+
}
|
|
14
|
+
/** The request's headers and body, taken BEFORE the call. A stream or FormData is not read — it would be consumed. */
|
|
15
|
+
export declare function takeRequest(input: FetchInput, init: FetchInit, bodies: boolean): RequestSide;
|
|
16
|
+
export interface ResponseRead {
|
|
17
|
+
body?: HttpExchangeBody;
|
|
18
|
+
stream?: NonNullable<HttpExchange['response']['stream']>;
|
|
19
|
+
endedAt: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Read the response from its clone. MUST be called synchronously after `res.clone()` (see above). An AI provider's
|
|
23
|
+
* stream keeps each event's name, time and size, never its data; its body is not kept at all.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readResponse(res: Response, clone: Response, startedAt: number, opts: {
|
|
26
|
+
ai: boolean;
|
|
27
|
+
paths: string[];
|
|
28
|
+
}): Promise<ResponseRead>;
|
|
29
|
+
/** The request side as kept: headers redacted, the query, the body (or why there is none). */
|
|
30
|
+
export declare function requestPart(side: RequestSide, search: string, mode: HttpCaptureMode, ai: boolean, paths: string[], redacted: string[]): Promise<HttpExchange['request']>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var q=Object.defineProperty;var g=(e,r)=>q(e,"name",{value:r,configurable:!0});var k=Object.defineProperty,l=g((e,r)=>k(e,"name",{value:r,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.takeRequest=takeRequest,exports.readResponse=readResponse,exports.requestPart=requestPart;const node_crypto_1=require("node:crypto"),ai_store_1=require("./ai-store"),http_store_1=require("./http-store"),MAX_EVENTS=2e3,MAX_EVENT_DATA=4*1024,MAX_STREAM_KEPT=256*1024,MAX_STREAM_MS=300*1e3,MAX_HASH_BYTES=1024*1024,pairs=l(e=>{try{if(!e)return[];if(typeof e.forEach=="function"&&typeof e.get=="function"){const r=[];return e.forEach((o,n)=>r.push([n,o])),r}return Array.isArray(e)?e.map(([r,o])=>[String(r),String(o)]):Object.entries(e).map(([r,o])=>[r,Array.isArray(o)?o.join(", "):String(o)])}catch{return[]}},"pairs");function takeRequest(e,r,o){const n=typeof e=="object"&&e!==null&&!(e instanceof URL)?e:void 0,i=[...pairs(n?.headers),...pairs(r?.headers)],c=i.find(([s])=>s.toLowerCase()==="content-type")?.[1]??"",a={headers:i,contentType:c};if(!o)return a;const t=r?.body,d=l(s=>new TextEncoder().encode(s),"enc");if(typeof t=="string")return{...a,bytes:d(t),size:Buffer.byteLength(t)};if(t instanceof URLSearchParams)return{...a,contentType:c||"application/x-www-form-urlencoded",bytes:d(t.toString())};if(t instanceof ArrayBuffer)return{...a,bytes:new Uint8Array(t),size:t.byteLength};if(ArrayBuffer.isView(t))return{...a,bytes:new Uint8Array(t.buffer,t.byteOffset,t.byteLength),size:t.byteLength};if(typeof Blob<"u"&&t instanceof Blob)return{...a,size:t.size,...t.size<=http_store_1.MAX_KEPT_BYTES*32?{pending:t.arrayBuffer().then(s=>new Uint8Array(s)).catch(()=>{})}:{}};if(typeof FormData<"u"&&t instanceof FormData)return{...a,omitted:"form-data"};if(t!=null)return{...a,omitted:"stream"};if(n&&!n.bodyUsed&&n.body)try{return{...a,pending:n.clone().arrayBuffer().then(s=>new Uint8Array(s)).catch(()=>{})}}catch{return a}return a}g(takeRequest,"takeRequest"),l(takeRequest,"takeRequest");function sseEvent(e){let r="message";const o=[];for(const n of e.split(/\r?\n/)){if(!n||n.startsWith(":"))continue;const i=n.indexOf(":"),c=i<0?n:n.slice(0,i),a=i<0?"":n.slice(i+1).replace(/^ /,"");c==="event"?r=a||"message":c==="data"&&o.push(a)}return o.length||r!=="message"?{event:r,data:o.join(`
|
|
2
|
+
`)}:void 0}g(sseEvent,"sseEvent"),l(sseEvent,"sseEvent");function readResponse(e,r,o,n){const i=(()=>{try{return e.headers.get("content-type")??""}catch{return""}})(),c=Number((()=>{try{return e.headers.get("content-length")??""}catch{return""}})()),a=r.body;if(!a)return Promise.resolve({endedAt:Date.now(),...Number.isFinite(c)&&c===0?{}:{}});const t=a.getReader(),d=/text\/event-stream/i.test(i);return(async()=>{let s=0;const m=[];let f=0;const E=(0,node_crypto_1.createHash)("sha256");let v=!/json|^text\/|xml|form/i.test(i);const T=[];let L=0,x=0,_,A=!1,y="";const z=new TextDecoder,B=l(p=>{const u=sseEvent(p);if(!u)return;L++;const h=Date.now()-o;if(_??=h,T.length>=MAX_EVENTS||x>=MAX_STREAM_KEPT){A=!0;return}const b=n.ai?void 0:(0,ai_store_1.redactAiText)(u.data).slice(0,MAX_EVENT_DATA);b&&(x+=b.length),T.push({atMs:h,event:u.event,bytes:Buffer.byteLength(p),...b!==void 0?{data:b}:{}})},"take");try{for(;;){const{done:p,value:u}=await t.read();if(p)break;if(s+=u.byteLength,d){y+=z.decode(u,{stream:!0});let h;for(;(h=y.search(/\r?\n\r?\n/))>=0;){const b=/\r?\n\r?\n/.exec(y.slice(h));B(y.slice(0,h)),y=y.slice(h+b[0].length)}}else n.ai||(f<http_store_1.MAX_KEPT_BYTES*32&&(m.push(u),f+=u.byteLength),v&&(s<=MAX_HASH_BYTES?E.update(u):v=!1));if(Date.now()-o>MAX_STREAM_MS){A=!0,await t.cancel().catch(()=>{});break}}d&&y.trim()&&B(y)}catch{A=!0}const w=Date.now();if(d)return{endedAt:w,stream:{protocol:"sse",..._!==void 0?{firstEventMs:_}:{},events:T,bytes:s,total:L,...A?{cut:!0}:{}}};if(n.ai)return{endedAt:w,body:{contentType:i.split(";")[0].trim(),size:s,kept:0,truncated:!1,omitted:"ai"}};const M=new Uint8Array(f);let S=0;for(const p of m)M.set(p,S),S+=p.byteLength;const R=v&&s<=MAX_HASH_BYTES?E.digest("hex"):void 0;return{endedAt:w,body:(0,http_store_1.keepBody)(i,M,s,n.paths,R)}})()}g(readResponse,"readResponse"),l(readResponse,"readResponse");async function requestPart(e,r,o,n,i,c){const{headers:a,redacted:t}=(0,http_store_1.redactHeaders)(e.headers);c.push(...t);const d=o==="bodies"&&r.length>1?(0,http_store_1.redactQuery)(r):[];let s;if(o==="bodies"){const m=e.contentType.split(";")[0].trim().toLowerCase();if(n)s={contentType:m,size:e.size??e.bytes?.byteLength??0,kept:0,truncated:!1,omitted:"ai"};else if(e.omitted)s={contentType:m,size:e.size??0,kept:0,truncated:!1,omitted:e.omitted};else{const f=e.bytes??await e.pending;f&&f.byteLength&&(s=(0,http_store_1.keepBody)(e.contentType,f,e.size??f.byteLength,i))}}return{headers:a,...d.length?{query:d}:{},...s?{body:s}:{}}}g(requestPart,"requestPart"),l(requestPart,"requestPart");
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type HttpExchange, type HttpExchangeBody } from '@lensmcp/core/http-exchange';
|
|
2
|
+
export type HttpCaptureMode = 'off' | 'headers' | 'bodies';
|
|
3
|
+
/** `LENSMCP_HTTP_CAPTURE`: `off`, `headers`, or `bodies` — the default (hub decision 12). */
|
|
4
|
+
export declare function httpCaptureMode(env?: Record<string, string | undefined>): HttpCaptureMode;
|
|
5
|
+
/** What a removed value reads as. */
|
|
6
|
+
export declare const REMOVED = "\u2022\u2022\u2022\u2022 redacted";
|
|
7
|
+
/** A body keeps its first 64 KB as text. */
|
|
8
|
+
export declare const MAX_KEPT_BYTES: number;
|
|
9
|
+
export declare const newExchangeId: () => string;
|
|
10
|
+
/** Headers as kept: credential values removed (a bearer JWT shown as its claims), the names that were. */
|
|
11
|
+
export declare function redactHeaders(entries: ReadonlyArray<readonly [string, string]>): {
|
|
12
|
+
headers: Array<[string, string]>;
|
|
13
|
+
redacted: string[];
|
|
14
|
+
};
|
|
15
|
+
/** The query string as pairs: a credential-named key's value removed, every value scrubbed of secret shapes. */
|
|
16
|
+
export declare function redactQuery(search: string): Array<[string, string]>;
|
|
17
|
+
/**
|
|
18
|
+
* A body as kept: its text (the first 64 KB), redacted by what it is — JSON and form fields by name, any text by the
|
|
19
|
+
* secret shapes — or, binary, its size (and digest). `bytes` is what was read; `size` every byte it had.
|
|
20
|
+
*/
|
|
21
|
+
export declare function keepBody(contentType: string, bytes: Uint8Array, size: number, paths: string[], sha256?: string): HttpExchangeBody;
|
|
22
|
+
/** Persist one exchange (temp name, then rename). Best-effort: false on any failure — the call goes on regardless. */
|
|
23
|
+
export declare function writeHttpExchange(x: HttpExchange): boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var x=Object.defineProperty;var u=(e,t)=>x(e,"name",{value:t,configurable:!0});var f=Object.defineProperty,s=u((e,t)=>f(e,"name",{value:t,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.newExchangeId=exports.MAX_KEPT_BYTES=exports.REMOVED=void 0,exports.httpCaptureMode=httpCaptureMode,exports.redactHeaders=redactHeaders,exports.redactQuery=redactQuery,exports.keepBody=keepBody,exports.writeHttpExchange=writeHttpExchange;const tslib_1=require("tslib"),nodeFs=tslib_1.__importStar(require("node:fs")),node_crypto_1=require("node:crypto"),http_exchange_1=require("@lensmcp/core/http-exchange"),redact_1=require("@lensmcp/core/redact"),ai_store_1=require("./ai-store"),emit_1=require("./emit"),mkdirSync=nodeFs.mkdirSync,writeFileSync=nodeFs.writeFileSync,renameSync=nodeFs.renameSync,readdirSync=nodeFs.readdirSync,unlinkSync=nodeFs.unlinkSync;function httpCaptureMode(e=process.env){const t=(e.LENSMCP_HTTP_CAPTURE??"").trim().toLowerCase();return t==="off"||t==="0"||t==="false"||t==="none"?"off":t==="headers"?"headers":"bodies"}u(httpCaptureMode,"httpCaptureMode"),s(httpCaptureMode,"httpCaptureMode"),exports.REMOVED="\u2022\u2022\u2022\u2022 redacted",exports.MAX_KEPT_BYTES=64*1024;const MAX_PARSE_BYTES=2*1024*1024,newExchangeId=s(()=>`hx_${Date.now().toString(36)}${(0,node_crypto_1.randomBytes)(5).toString("hex")}`,"newExchangeId");exports.newExchangeId=newExchangeId;const CREDENTIAL_HEADERS=/^(authorization|proxy-authorization|cookie|set-cookie|x-api-key|api-key|openai-organization|openai-project|x-internal-token|x-amz-security-token)$|-api-key$|-token$|-secret$/i,CREDENTIAL_FIELD=/pass(word|wd)?|secret|token|api[-_]?key|authorization|credential|cookie|signature/i;function jwtClaims(e){const t=e.split(".");if(t.length===3)try{const r=JSON.parse(Buffer.from(t[1].replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8"));return`JWT \xB7 ${["iss","aud","sub","exp"].filter(o=>r[o]!==void 0).map(o=>`${o}=${o==="exp"&&typeof r[o]=="number"?new Date(r[o]*1e3).toISOString():String(r[o]).slice(0,80)}`).join(" \xB7 ")||"no claims"} \xB7 ${exports.REMOVED.split(" ")[0]} signature`}catch{return}}u(jwtClaims,"jwtClaims"),s(jwtClaims,"jwtClaims");function redactHeaders(e){const t=[],r=[];for(const[o,p]of e){const n=o.toLowerCase();if(CREDENTIAL_HEADERS.test(n)||(0,redact_1.isSecretParam)(n)){r.push(n);const i=/^bearer\s+(\S+)$/i.exec(p.trim()),c=i?jwtClaims(i[1]):void 0,a=/^(\w+)\s+\S/.exec(p.trim())?.[1];t.push([n,c?`Bearer ${c}`:a&&n.endsWith("authorization")?`${a} ${exports.REMOVED}`:exports.REMOVED])}else t.push([n,(0,ai_store_1.redactAiText)(p).slice(0,2048)])}return{headers:t,redacted:r}}u(redactHeaders,"redactHeaders"),s(redactHeaders,"redactHeaders");function redactQuery(e){const t=[];try{for(const[r,o]of new URLSearchParams(e))t.push([r,(0,redact_1.isSecretParam)(r)||CREDENTIAL_FIELD.test(r)?exports.REMOVED:(0,ai_store_1.redactAiText)(o).slice(0,1024)])}catch{}return t.slice(0,100)}u(redactQuery,"redactQuery"),s(redactQuery,"redactQuery");function redactJson(e,t,r,o=0){if(o>32||e===null||typeof e!="object")return typeof e=="string"?(0,ai_store_1.redactAiText)(e):e;if(Array.isArray(e))return e.map((n,i)=>redactJson(n,`${t}[${i}]`,r,o+1));const p={};for(const[n,i]of Object.entries(e)){const c=t?`${t}.${n}`:n;CREDENTIAL_FIELD.test(n)&&i!==null&&typeof i!="object"?(p[n]=exports.REMOVED,r.length<50&&r.push(c)):p[n]=redactJson(i,c,r,o+1)}return p}u(redactJson,"redactJson"),s(redactJson,"redactJson");const textual=s(e=>/json|^text\/|x-www-form-urlencoded|xml|javascript|graphql|event-stream/i.test(e)||e==="","textual");function keepBody(e,t,r,o,p){const n=e.split(";")[0].trim().toLowerCase();if(!textual(n))return{contentType:n||"application/octet-stream",size:r,kept:0,truncated:!1,omitted:"binary",...p?{sha256:p}:{}};const i=t.byteLength>=r&&r<=MAX_PARSE_BYTES,c=Buffer.from(t.buffer,t.byteOffset,t.byteLength).toString("utf8");let a=c;if(i&&/json/.test(n))try{a=JSON.stringify(redactJson(JSON.parse(c),"",o))}catch{a=(0,ai_store_1.redactAiText)(c)}else i&&/x-www-form-urlencoded/.test(n)?a=new URLSearchParams(redactQuery(c)).toString():a=(0,ai_store_1.redactAiText)(c);const d=Buffer.byteLength(a)>exports.MAX_KEPT_BYTES?Buffer.from(a).subarray(0,exports.MAX_KEPT_BYTES).toString("utf8"):a;return{contentType:n,size:r,kept:Buffer.byteLength(d),text:d,truncated:r>t.byteLength||d.length<a.length}}u(keepBody,"keepBody"),s(keepBody,"keepBody");const maxFiles=s(()=>{const e=Number(process.env.LENSMCP_HTTP_MAX_FILES);return Number.isInteger(e)&&e>=10?e:500},"maxFiles");let writes=0;function prune(e){try{const t=readdirSync(e).filter(r=>/^hx_[0-9a-z]+\.json$/.test(r)).sort();for(let r=0;r<t.length-maxFiles();r+=1)try{unlinkSync(`${e}/${t[r]}`)}catch{}}catch{}}u(prune,"prune"),s(prune,"prune");function writeHttpExchange(e){try{const t=(0,http_exchange_1.httpExchangeDir)(emit_1.EVENT_FILE);mkdirSync(t,{recursive:!0,mode:448});const r=(0,http_exchange_1.httpExchangeFile)(emit_1.EVENT_FILE,e.id);return writeFileSync(`${r}.tmp`,JSON.stringify(e),{mode:384}),renameSync(`${r}.tmp`,r),writes+=1,(writes===1||writes%25===0)&&prune(t),!0}catch{return!1}}u(writeHttpExchange,"writeHttpExchange"),s(writeHttpExchange,"writeHttpExchange");
|
package/lib/tap-egress.d.ts
CHANGED
package/lib/tap-egress.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var F=Object.defineProperty;var l=(t,e)=>F(t,"name",{value:e,configurable:!0});var C=Object.defineProperty,i=l((t,e)=>C(t,"name",{value:e,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.flushEgressCaptureForTests=flushEgressCaptureForTests,exports.tapEgress=tapEgress,exports.resetEgressTapForTests=resetEgressTapForTests;const redact_1=require("@lensmcp/core/redact"),emit_1=require("./emit"),flow_context_1=require("./flow-context"),ai_providers_1=require("./ai-providers"),http_capture_1=require("./http-capture"),http_store_1=require("./http-store"),INTERNAL=[/\.local$/i,/\.internal$/i,/^localhost$/i,/^127\./,/^\[?::1\]?$/,/^0\.0\.0\.0$/,/^10\./,/^192\.168\./,/^172\.(1[6-9]|2\d|3[01])\./,/^169\.254\./,/^\[?[fF][cCdD][0-9a-fA-F]{2}:/,/^\[?[fF][eE]80:/],isInternal=i(t=>t===""||INTERNAL.some(e=>e.test(t)),"isInternal"),CAPTURE_QUERY=process.env.LENSMCP_EGRESS_QUERY==="1",MAX_PATH_CHARS=256,clipPath=i(t=>t.length>MAX_PATH_CHARS?`${t.slice(0,MAX_PATH_CHARS)}\u2026`:t,"clipPath");function capturePath(t,e){const s=t||"/";return!CAPTURE_QUERY||e.length<=1?clipPath(s):(0,redact_1.redactUrl)(`${s}${e}`)}l(capturePath,"capturePath"),i(capturePath,"capturePath");const fingerprintFor=i((t,e)=>e?`external:error:${t}`:`external:${t}`,"fingerprintFor"),pendingExchanges=new Set;async function flushEgressCaptureForTests(){await Promise.allSettled([...pendingExchanges])}l(flushEgressCaptureForTests,"flushEgressCaptureForTests"),i(flushEgressCaptureForTests,"flushEgressCaptureForTests");const headerPairs=i(t=>{const e=[];try{t?.headers?.forEach?.((s,d)=>e.push([d,s]))}catch{}return e},"headerPairs");async function finishExchange(t){const e=[],s=[],d=await(0,http_capture_1.requestPart)(t.request,t.url.search,t.mode,!!t.ai,s,e),r=t.reading?await t.reading.catch(()=>{}):void 0,{headers:c,redacted:a}=(0,http_store_1.redactHeaders)(headerPairs(t.res));e.push(...a);const n=typeof t.res?.status=="number"?t.res.status:void 0,p={schemaVersion:1,id:t.id,capturedAt:Date.now(),side:"egress",service:emit_1.PROJECT,...t.ctx?.flowId?{flowId:t.ctx.flowId}:{},...t.ctx?.requestId?{requestId:t.ctx.requestId}:{},method:t.method,url:`${t.url.protocol}//${t.url.host}${t.path}`,host:t.url.hostname,path:t.path,...n!==void 0?{status:n}:{},...t.res?.statusText?{statusText:t.res.statusText}:{},...t.error?{error:t.error}:{},startedAt:t.startedAt,...t.ttfbMs!==void 0?{ttfbMs:t.ttfbMs}:{},durationMs:(r?.endedAt??Date.now())-t.startedAt,capture:t.mode==="bodies"?"bodies":"headers",request:d,response:{headers:c,...r?.body?{body:r.body}:{},...r?.stream?{stream:r.stream}:{}},redacted:{headers:[...new Set(e)],bodyFields:[...s.map(o=>`request.${o}`),...t.responseFields.map(o=>`response.${o}`)]},...t.ai?{ai:{provider:t.ai}}:{}};(0,http_store_1.writeHttpExchange)(p)}l(finishExchange,"finishExchange"),i(finishExchange,"finishExchange");function trackExchange(t){const e=finishExchange(t).catch(()=>{});pendingExchanges.add(e),e.finally(()=>pendingExchanges.delete(e))}l(trackExchange,"trackExchange"),i(trackExchange,"trackExchange");function tapEgress(){tapFetch(),tapHttpModule("http"),tapHttpModule("https")}l(tapEgress,"tapEgress"),i(tapEgress,"tapEgress");let savedFetch;const httpOriginals=new Map;function resetEgressTapForTests(){const t=globalThis;savedFetch&&(t.fetch=savedFetch),delete t.__lensmcpFetchTap,restoreHttp("http"),restoreHttp("https"),savedFetch=void 0}l(resetEgressTapForTests,"resetEgressTapForTests"),i(resetEgressTapForTests,"resetEgressTapForTests");function restoreHttp(t){const e=httpOriginals.get(t);if(!e)return;const s=require(`node:${t}`);s.request=e.request,s.get=e.get,httpOriginals.delete(t)}l(restoreHttp,"restoreHttp"),i(restoreHttp,"restoreHttp");function tapFetch(){const t=globalThis;if(typeof t.fetch!="function"||t.__lensmcpFetchTap)return;t.__lensmcpFetchTap=!0;const e=t.fetch.bind(globalThis);savedFetch=t.fetch;const s=(0,http_store_1.httpCaptureMode)(),d=(0,ai_providers_1.parseExtraHosts)(process.env.LENSMCP_AI_HOSTS);t.fetch=(async(r,c)=>{let a="",n="/",p=!1,o;try{const h=new URL(typeof r=="string"?r:r instanceof URL?r.href:r.url);o=h,a=h.hostname,p=h.search.length>1,n=capturePath(h.pathname,h.search)}catch{}if(isInternal(a))return e(r,c);const f=Date.now(),u=(0,flow_context_1.flowContext)(),E=(0,flow_context_1.liveFlowContext)();E&&(E.externalCallCount=(E.externalCallCount??0)+1);const b=typeof r=="object"&&r!==null&&!(r instanceof URL)?r.method:void 0,m=String(c?.method??b??"GET").toUpperCase(),g=s!=="off"&&o?(0,http_store_1.newExchangeId)():void 0,v=g?(0,http_capture_1.takeRequest)(r,c,s==="bodies"):void 0,w=g&&o&&m==="POST"?(0,ai_providers_1.matchTarget)(a,o.port,o.pathname,d)?.provider:void 0,_=i(h=>({id:g,mode:s,method:m,url:o,path:o.pathname.length>256?`${o.pathname.slice(0,256)}\u2026`:o.pathname,startedAt:f,ctx:u,request:v,...w?{ai:w}:{},responseFields:[],...h}),"job");try{const h=await e(r,c),x=Date.now()-f,T=h.status>=500,$=[];let y;if(g&&s==="bodies")try{y=(0,http_capture_1.readResponse)(h,h.clone(),f,{ai:!!w,paths:$})}catch{y=void 0}const q=(0,emit_1.lensEmit)("external","info",`egress ${m} ${a} \u2192 ${h.status}`,{name:a,kind:"egress",method:m,host:a,path:n,status:h.status,...p?{hasQuery:!0}:{},startedAt:f,durationMs:x,...g?{exchangeId:g}:{}},fingerprintFor(a,T),u);return g&&q&&trackExchange(_({res:h,ttfbMs:x,responseFields:$,...y?{reading:y}:{}})),h}catch(h){const x=String(h?.message??h),T=(0,emit_1.lensEmit)("external","error",`egress ${m} ${a} failed`,{name:a,kind:"egress",method:m,host:a,path:n,...p?{hasQuery:!0}:{},error:x,startedAt:f,durationMs:Date.now()-f,...g?{exchangeId:g}:{}},fingerprintFor(a,!0),u);throw g&&T&&trackExchange(_({error:x})),h}})}l(tapFetch,"tapFetch"),i(tapFetch,"tapFetch");function tapHttpModule(t){const e=require(`node:${t}`),s=e.request;if(typeof s!="function"||s.__lensmcp)return;httpOriginals.set(t,{request:s,get:e.get});const d=i(function(...r){const c=s.apply(this,r);try{observe(r,c)}catch{}return c},"wrapped");d.__lensmcp=!0,e.request=d,typeof e.get=="function"&&(e.get=function(...r){const c=d.apply(this,r);return c.end(),c})}l(tapHttpModule,"tapHttpModule"),i(tapHttpModule,"tapHttpModule");function observe(t,e){const{host:s,method:d,path:r,hasQuery:c}=parseTarget(t);if(isInternal(s))return;const a=Date.now(),n=(0,flow_context_1.flowContext)(),p=(0,flow_context_1.liveFlowContext)();p&&(p.externalCallCount=(p.externalCallCount??0)+1);let o=!1;e.once("response",(...f)=>{if(o)return;o=!0;const u=f[0]?.statusCode;(0,emit_1.lensEmit)("external","info",`egress ${d} ${s} \u2192 ${u??"?"}`,{name:s,kind:"egress",method:d,host:s,path:r,...u!=null?{status:u}:{},...c?{hasQuery:!0}:{},startedAt:a,durationMs:Date.now()-a},fingerprintFor(s,u!=null&&u>=500),n)}),e.once("error",(...f)=>{if(o)return;o=!0;const u=f[0];(0,emit_1.lensEmit)("external","error",`egress ${d} ${s} failed`,{name:s,kind:"egress",method:d,host:s,path:r,...c?{hasQuery:!0}:{},error:String(u?.message??u),startedAt:a,durationMs:Date.now()-a},fingerprintFor(s,!0),n)})}l(observe,"observe"),i(observe,"observe");function parseTarget(t){let e="",s="GET",d="/",r=!1;const c=i((a,n)=>{r=n.length>1,d=capturePath(a,n)},"take");for(const a of t)if(typeof a=="string")try{const n=new URL(a);e=n.hostname,c(n.pathname,n.search)}catch{}else if(a instanceof URL)e=a.hostname,c(a.pathname,a.search);else if(a&&typeof a=="object"){const n=a;if(typeof n.method=="string"&&(s=n.method.toUpperCase()),typeof n.socketPath=="string")return{host:"",method:s,path:d,hasQuery:r};if(typeof n.path=="string"){const p=n.path,o=p.indexOf("?");c(o===-1?p:p.slice(0,o),o===-1?"":p.slice(o))}typeof n.hostname=="string"&&n.hostname?e=n.hostname:typeof n.host=="string"&&n.host&&(e=n.host.split(":")[0]??"")}return{host:e,method:s,path:d,hasQuery:r}}l(parseTarget,"parseTarget"),i(parseTarget,"parseTarget");
|
package/lib/tap-fs.d.ts
CHANGED
|
@@ -1 +1,37 @@
|
|
|
1
|
+
export type FsOp = 'read' | 'write' | 'append';
|
|
2
|
+
/** One path in a window: its operation, how often and how many bytes, and the flow of its latest call. */
|
|
3
|
+
export interface FsPathOp {
|
|
4
|
+
path: string;
|
|
5
|
+
op: FsOp;
|
|
6
|
+
n: number;
|
|
7
|
+
bytes: number;
|
|
8
|
+
/** Opened as a stream: its bytes are not counted. */
|
|
9
|
+
stream?: true;
|
|
10
|
+
at: number;
|
|
11
|
+
flowId?: string;
|
|
12
|
+
requestId?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface FsOpsPayload {
|
|
15
|
+
reads: number;
|
|
16
|
+
writes: number;
|
|
17
|
+
bytesRead: number;
|
|
18
|
+
bytesWritten: number;
|
|
19
|
+
top: string[];
|
|
20
|
+
paths: FsPathOp[];
|
|
21
|
+
}
|
|
22
|
+
/** One second of fs traffic: counted as it comes, drained into one `fs-ops` event. */
|
|
23
|
+
export declare class FsWindow {
|
|
24
|
+
private reads;
|
|
25
|
+
private writes;
|
|
26
|
+
private bytesRead;
|
|
27
|
+
private bytesWritten;
|
|
28
|
+
private readonly counts;
|
|
29
|
+
private readonly ops;
|
|
30
|
+
record(op: FsOp, path: string, bytes: number, at: number, ctx?: {
|
|
31
|
+
flowId?: string;
|
|
32
|
+
requestId?: string;
|
|
33
|
+
}, stream?: boolean): void;
|
|
34
|
+
/** The window as one event's payload — newest paths first — and a fresh window; nothing when it saw nothing. */
|
|
35
|
+
drain(): FsOpsPayload | undefined;
|
|
36
|
+
}
|
|
1
37
|
export declare function tapFs(): void;
|
package/lib/tap-fs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var h=Object.defineProperty;var l=(e,i)=>h(e,"name",{value:i,configurable:!0});var d=Object.defineProperty,f=l((e,i)=>d(e,"name",{value:i,configurable:!0}),"f");Object.defineProperty(exports,"__esModule",{value:!0}),exports.FsWindow=void 0,exports.tapFs=tapFs;const tslib_1=require("tslib"),os=tslib_1.__importStar(require("node:os")),emit_1=require("./emit"),flow_context_1=require("./flow-context"),PATHS_KEPT=50,PATHS_SENT=20;class FsWindow{static{l(this,"FsWindow")}static{f(this,"FsWindow")}constructor(){this.reads=0,this.writes=0,this.bytesRead=0,this.bytesWritten=0,this.counts=new Map,this.ops=new Map}record(i,n,t,s,r,o=!1){i==="read"?(this.reads+=1,this.bytesRead+=t):(this.writes+=1,this.bytesWritten+=t),(this.counts.size<PATHS_KEPT||this.counts.has(n))&&this.counts.set(n,(this.counts.get(n)??0)+1);const c=`${i}\0${n}\0${r?.flowId??""}`,a=this.ops.get(c);a?(a.n+=1,a.bytes+=t,a.at=s,r?.requestId&&(a.requestId=r.requestId)):this.ops.size<PATHS_KEPT&&this.ops.set(c,{path:n,op:i,n:1,bytes:t,...o?{stream:!0}:{},at:s,...r?.flowId?{flowId:r.flowId}:{},...r?.requestId?{requestId:r.requestId}:{}})}drain(){const{reads:i,writes:n,bytesRead:t,bytesWritten:s}=this;if(i+n===0)return;const r=[...this.counts.entries()].sort((c,a)=>a[1]-c[1]).slice(0,3).map(([c])=>c),o=[...this.ops.values()].sort((c,a)=>a.at-c.at).slice(0,PATHS_SENT);return this.reads=this.writes=this.bytesRead=this.bytesWritten=0,this.counts.clear(),this.ops.clear(),{reads:i,writes:n,bytesRead:t,bytesWritten:s,top:r,paths:o}}}exports.FsWindow=FsWindow;const current=new FsWindow;let timer=null;const TMP=os.tmpdir(),CWD=process.cwd(),SKIP=["node_modules","/.nx/","/dist/","/.git/",".lensmcp"],CODE=/\.(?:[cm]?[jt]sx?|map|node)$/;let appending=0;function trackedPath(e){if((0,emit_1.isLensWriting)())return null;const i=typeof e=="string"?e:Buffer.isBuffer(e)?e.toString():e instanceof URL?e.pathname:null;return!i||i===emit_1.EVENT_FILE||i.startsWith(TMP)||SKIP.some(n=>i.includes(n))?null:i}l(trackedPath,"trackedPath"),f(trackedPath,"trackedPath");function record(e,i,n=0,t,s=!1){const r=trackedPath(i);if(!r||e==="read"&&CODE.test(r))return;const o=r.startsWith(CWD)?r.slice(CWD.length+1):r;current.record(e,o,n,Date.now(),t??(0,flow_context_1.flowContext)(),s),timer||(timer=setTimeout(flush,1e3),timer.unref?.())}l(record,"record"),f(record,"record");function flush(){timer=null;const e=current.drain();e&&(0,emit_1.lensEmit)("fs","info",`fs ${e.reads} reads \xB7 ${e.writes} writes`,{name:"fs",kind:"fs-ops",...e},`fs:${emit_1.PROJECT}`)}l(flush,"flush"),f(flush,"flush");const byteLen=f(e=>typeof e=="string"?Buffer.byteLength(e):Buffer.isBuffer(e)?e.length:0,"byteLen");function wrap(e,i,n){const t=e?.[i];if(!e||typeof t!="function"||t.__lensmcp)return;const s=n(t);s.__lensmcp=!0;try{e[i]=s}catch{}}l(wrap,"wrap"),f(wrap,"wrap");function tapFs(){const e=require("node:fs");wrap(e,"readFileSync",n=>function(t,...s){const r=n.call(this,t,...s);return record("read",t,byteLen(r)),r}),wrap(e,"writeFileSync",n=>function(t,s,...r){const o=appending>0,c=n.call(this,t,s,...r);return o||record("write",t,byteLen(s)),c}),wrap(e,"appendFileSync",n=>function(t,s,...r){appending++;let o;try{o=n.call(this,t,s,...r)}finally{appending--}return record("append",t,byteLen(s)),o}),wrap(e,"readFile",n=>function(t,...s){const r=s[s.length-1];if(typeof r=="function"){const o=(0,flow_context_1.flowContext)();s[s.length-1]=(c,a)=>{c||record("read",t,byteLen(a),o),r(c,a)}}return n.call(this,t,...s)});for(const n of["writeFile","appendFile"])wrap(e,n,t=>function(s,r,...o){const c=o[o.length-1],a=appending>0;if(typeof c=="function"){const p=(0,flow_context_1.flowContext)();o[o.length-1]=u=>{!u&&!a&&record(n==="appendFile"?"append":"write",s,byteLen(r),p),c(u)}}if(n!=="appendFile")return t.call(this,s,r,...o);appending++;try{return t.call(this,s,r,...o)}finally{appending--}});const i=e.promises;wrap(i,"readFile",n=>async function(t,...s){const r=(0,flow_context_1.flowContext)(),o=await n.call(this,t,...s);return record("read",t,byteLen(o),r),o});for(const n of["writeFile","appendFile"])wrap(i,n,t=>async function(s,r,...o){const c=(0,flow_context_1.flowContext)(),a=await t.call(this,s,r,...o);return record(n==="appendFile"?"append":"write",s,byteLen(r),c),a});wrap(e,"createReadStream",n=>function(t,...s){return record("read",t,0,(0,flow_context_1.flowContext)(),!0),n.call(this,t,...s)}),wrap(e,"createWriteStream",n=>function(t,...s){return record("write",t,0,(0,flow_context_1.flowContext)(),!0),n.call(this,t,...s)})}l(tapFs,"tapFs"),f(tapFs,"tapFs");
|
package/lib/tap-pg.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var R=Object.defineProperty;var p=(e,t)=>R(e,"name",{value:t,configurable:!0});var B=Object.defineProperty,o=p((e,t)=>B(e,"name",{value:t,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.pgTap=pgTap;const redact_1=require("@lensmcp/core/redact"),sql_shape_1=require("@lensmcp/core/sql-shape"),emit_1=require("./emit"),flow_context_1=require("./flow-context"),sql_accounting_1=require("./sql-accounting"),SKIP_OPS=new Set(["BEGIN","START","COMMIT","END","ROLLBACK","SAVEPOINT","RELEASE","SET","RESET","SHOW","DEALLOCATE","DISCARD","LISTEN","UNLISTEN"]),STATEMENT_EVERY_MS=10*6e4,STATEMENT_MAX=8*1024,statementSentAt=new Map,MAX_SHAPES_REMEMBERED=2e3,firstWord=o(e=>(/^\s*(?:(?:\/\*[\s\S]*?\*\/|--[^\n]*\n?)\s*)*(\w+)/.exec(e)?.[1]??"query").toUpperCase(),"firstWord");function parseSql(e){const t=(0,sql_shape_1.sqlShape)(e),r=(t.target??t.tables[0])?.split(".").pop();return{op:t.verb,...r?{table:r}:{},sqlId:t.id,tables:t.tables,...t.name?{name:t.name}:{}}}p(parseSql,"parseSql"),o(parseSql,"parseSql");const oneLine=o(e=>e.replace(/\s+/g," ").trim(),"oneLine"),clip=o((e,t)=>e.length>t?e.slice(0,t-1)+"\u2026":e,"clip");function paramPreview(e){if(!(!Array.isArray(e)||e.length===0))return e.slice(0,10).map(t=>{if(t==null)return String(t);const r=typeof t=="string"?t:(()=>{try{return JSON.stringify(t)}catch{return String(t)}})();return clip(r,80)})}p(paramPreview,"paramPreview"),o(paramPreview,"paramPreview");const rowsOf=o(e=>e?.rowCount??e?.rows?.length??0,"rowsOf");function resultPreview(e){const t=e?.rows;if(!(!Array.isArray(t)||t.length===0))try{return clip(JSON.stringify((0,redact_1.redactDeep)(t.slice(0,2))),240)}catch{return}}p(resultPreview,"resultPreview"),o(resultPreview,"resultPreview");function connectionOf(e){const t=e,r=t?.connectionParameters,m=typeof t?.database=="string"?t.database:typeof r?.database=="string"?r.database:void 0,a=typeof t?.host=="string"?t.host:typeof r?.host=="string"?r.host:void 0,s=typeof t?.port=="number"||typeof t?.port=="string"?t.port:typeof r?.port=="number"||typeof r?.port=="string"?r.port:5432;return{...m?{database:m}:{},...a?{server:`${a}:${s}`}:{}}}p(connectionOf,"connectionOf"),o(connectionOf,"connectionOf");function pgTap(e){const t=e?.Client?.prototype,r=t?.query;if(!t||typeof r!="function")return!1;if(r.__lensmcp)return!0;const m=o(function(a,s,b){const u=a,f=typeof u=="string"?u:u?.text;if(!f||u&&typeof u=="object"&&typeof u.submit=="function")return r.call(this,a,s,b);if(SKIP_OPS.has(firstWord(f)))return r.call(this,a,s,b);const{op:L,table:i,sqlId:q,tables:T,name:v}=parseSql(f),x=connectionOf(this),d=L.toLowerCase(),D=`db:${d}:${i??"query"}`,y=Date.now(),A=(0,flow_context_1.flowContext)(),g=(0,flow_context_1.liveFlowContext)();g&&(g.dbCallCount=(g.dbCallCount??0)+1);const w=(0,sql_accounting_1.beginSqlStatement)(g,A,y),O=clip(oneLine(f),500),C=paramPreview(Array.isArray(s)?s:a?.values),E=C&&(0,redact_1.redactSqlParams)(f,C),_=o((n,c)=>{const l=Date.now()-y,$=y-(statementSentAt.get(q)??-1/0)>=STATEMENT_EVERY_MS,P={sqlId:q,...T.length?{tables:T}:{},...v?{sqlName:v}:{},...x,...$?{statement:clip(f.trim(),STATEMENT_MAX)}:{}};let h;if(n)h=(0,emit_1.lensEmitOutcome)("db","error",`${d}${i?" "+i:""} failed (${l}ms)`,{name:"postgres",kind:"db-query",op:d,...i?{table:i}:{},...P,query:O,...E?{params:E}:{},startedAt:y,error:String(n?.message??n),durationMs:l},`db:error:${d}:${i??"query"}`,A);else{const N=rowsOf(c),I=resultPreview(c);h=(0,emit_1.lensEmitOutcome)("db","info",`${d} ${i??"query"} (${N} rows, ${l}ms)`,{name:"postgres",kind:"db-query",op:d,...i?{table:i}:{},...P,query:O,...E?{params:E}:{},...I?{result:I}:{},startedAt:y,rows:N,durationMs:l},D,A)}$&&h==="written"&&(statementSentAt.delete(q),statementSentAt.set(q,y),statementSentAt.size>MAX_SHAPES_REMEMBERED&&statementSentAt.delete(statementSentAt.keys().next().value)),(0,sql_accounting_1.endSqlStatement)(w,{durationMs:l,failed:!!n,emitted:h})},"done"),M=typeof b=="function"?b:typeof s=="function"?s:void 0;if(M){const n=o((c,l)=>{try{_(c,l)}catch{}M(c,l)},"wrappedCb");try{return typeof s=="function"?r.call(this,a,n):r.call(this,a,s,n)}catch(c){throw(0,sql_accounting_1.abandonSqlStatement)(w),c}}let S;try{S=r.call(this,a,s)}catch(n){throw(0,sql_accounting_1.abandonSqlStatement)(w),n}return S&&typeof S.then=="function"?S.then(n=>_(void 0,n),n=>_(n,void 0)):(0,sql_accounting_1.abandonSqlStatement)(w),S},"wrapped");return m.__lensmcp=!0,t.query=m,!0}p(pgTap,"pgTap"),o(pgTap,"pgTap");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/node-instrumentation",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"main": "./index.js",
|
|
5
5
|
"types": "./index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@lensmcp/core": "1.
|
|
19
|
-
"@lensmcp/nest-instrumentation": "1.
|
|
20
|
-
"@lensmcp/protocol-types": "1.
|
|
18
|
+
"@lensmcp/core": "1.25.0",
|
|
19
|
+
"@lensmcp/nest-instrumentation": "1.25.0",
|
|
20
|
+
"@lensmcp/protocol-types": "1.25.0",
|
|
21
21
|
"tslib": "^2.3.0"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|