first commit
This commit is contained in:
commit
ff482ebe23
251 files changed
+19126
No files matched your search
@@ -0,0 +1,56 @@
|
||||
'use strict';
|
||||
const {AsyncLocalStorage}=require('node:async_hooks');
|
||||
const {performance}=require('node:perf_hooks');
|
||||
const {randomInt}=require('node:crypto');
|
||||
const http=require('node:http'),https=require('node:https');
|
||||
const {TracerProvider,BatchSpanProcessor,TraceIdRatioBasedSampler,ParentBasedSampler}=require('@opentelemetry/sdk-trace');
|
||||
const {LoggerProvider,BatchLogRecordProcessor}=require('@opentelemetry/sdk-logs');
|
||||
const {resourceFromAttributes}=require('@opentelemetry/resources');
|
||||
const {OTLPTraceExporter}=require('@opentelemetry/exporter-trace-otlp-http');
|
||||
const {OTLPLogExporter}=require('@opentelemetry/exporter-logs-otlp-http');
|
||||
const {trace,ROOT_CONTEXT,SpanStatusCode}=require('@opentelemetry/api');
|
||||
const names=new Set(['browser.network','browser.ipc','browser.agent','browser.management','browser.navigation','browser.dns','browser.request','browser.inspect.local','browser.inspect.enterprise','browser.download','browser.clipboard','browser.visual','browser.password.fill','browser.ai','browser.policy','browser.startup','browser.parser','browser.semantic','browser.yara','browser.icap','browser.tab','browser.security']);
|
||||
const operations=new Set(['application:state','settings:get','settings:update','bookmarks:list','bookmarks:mutate','tabs:create','tabs:activate','tabs:close','tabs:group','state','authorize-setting','settings-update','window-control','chrome-layout','share-url','bookmarks-mutate','enroll','sso','navigate','back','forward','reload','ai','inspect-file','agent-connection','ai-panel','vault-panel','vault-status','vault-create','vault-unlock','vault-lock','vault-list','vault-save','vault-delete','vault-fill','vault-generate','tab-create','tab-activate','tab-close','tab-group','sessions','read','click','type','screenshot','copy','cut','paste','scan','download','sync','create','activate','close','group','policy','request','dns','parse','embed','yara','icap']);
|
||||
const components=new Set(['browser','ipc','agent','management','navigation','network','dns','dlp','inspector','parser','semantic','yara','icap','download','clipboard','visual','vault','ai','policy','tabs']);
|
||||
const decisions=new Set(['allow','block','deny','unknown']),statuses=new Set(['ok','error','cancelled','timeout']);
|
||||
const requestTypes=new Set(['mainFrame','subFrame','stylesheet','script','image','font','object','xhr','ping','cspReport','media','webSocket','other']);
|
||||
const uuid=/^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
|
||||
function attributes(input){const out={};if(!input||typeof input!=='object')return out;for(const [key,descriptor]of Object.entries(Object.getOwnPropertyDescriptors(input))){if(!('value'in descriptor))continue;const value=descriptor.value;
|
||||
if(['sessionId','deviceId'].includes(key)&&typeof value==='string'&&uuid.test(value))out[key==='sessionId'?'safetynet.session.id':'safetynet.device.id']=value;
|
||||
else if(['policyVersion','bytes','count'].includes(key)&&Number.isSafeInteger(value)&&value>=0)out[key==='policyVersion'?'safetynet.policy.version':`safetynet.${key}`]=value;
|
||||
else if(key==='durationMs'&&Number.isFinite(value)&&value>=0&&value<=86400000)out['safetynet.duration.ms']=value;
|
||||
else if(key==='operation'&&operations.has(value))out['safetynet.operation']=value;
|
||||
else if(key==='component'&&components.has(value))out['safetynet.component']=value;
|
||||
else if(key==='decision'&&decisions.has(value))out['dlp.decision']=value;
|
||||
else if(key==='status'&&statuses.has(value))out['safetynet.status']=value;
|
||||
else if(key==='requestType'&&requestTypes.has(value))out['safetynet.request.type']=value;
|
||||
}return out;}
|
||||
function endpointAllowed(value){try{const u=new URL(value);if(u.username||u.password||u.search||u.hash)return false;const host=u.hostname.replace(/^\[|\]$/g,'');return u.protocol==='https:'||(u.protocol==='http:'&&(host==='::1'||/^127\.(?:\d{1,3}\.){2}\d{1,3}$/.test(host)&&host.split('.').every(p=>+p<=255)));}catch{return false;}}
|
||||
function valid(config){return config&&config.enabled===true&&['operational','debug'].includes(config.mode)&&endpointAllowed(config.endpoint)&&['sampleRate','debugSampleRate'].every(k=>Number.isFinite(config[k])&&config[k]>=0&&config[k]<=1)&&Number.isSafeInteger(config.maxQueueSize)&&config.maxQueueSize>=16&&config.maxQueueSize<=4096&&Number.isSafeInteger(config.flushIntervalMs)&&config.flushIntervalMs>=250&&config.flushIntervalMs<=10000&&Number.isSafeInteger(config.maxRecordAgeMs)&&config.maxRecordAgeMs>=1000&&config.maxRecordAgeMs<=300000&&Number.isSafeInteger(config.exportTimeoutMs)&&config.exportTimeoutMs>=250&&config.exportTimeoutMs<=10000;}
|
||||
const defaults={enabled:false,mode:'operational',sampleRate:0.1,debugSampleRate:1,maxQueueSize:512,flushIntervalMs:2000,maxRecordAgeMs:30000,exportTimeoutMs:2000};
|
||||
function createDiagnostics(){const storage=new AsyncLocalStorage();let generation=null,configurationKey='',closed=false;const retiring=new Set();let activeSpans=0;
|
||||
function retire(current){if(!current)return;current.active=false;for(const agent of current.agents)agent.destroy();const pending=Promise.allSettled([current.tracerProvider?.shutdown(),current.loggerProvider?.shutdown()]);retiring.add(pending);pending.finally(()=>retiring.delete(pending));}
|
||||
function gatedExporter(exporter,current){return {export(records,callback){if(!current.active){callback({code:0});return;}const now=Date.now(),fresh=records.filter(record=>{const timestamp=record.endTime||record.hrTime||record.hrTimeObserved;return timestamp&&now-(timestamp[0]*1000+timestamp[1]/1e6)<=current.config.maxRecordAgeMs;});if(!fresh.length){callback({code:0});return;}try{exporter.export(fresh,result=>{if(result.code!==0)current.failedExports++;callback(result);});}catch{current.failedExports++;callback({code:1});}},shutdown:()=>exporter.shutdown(),forceFlush:()=>exporter.forceFlush?.()||Promise.resolve()};}
|
||||
function configure(input={},context={}){if(closed)return false;const config={...defaults,...input};const key=JSON.stringify([config,attributes(context)]);if(key===configurationKey)return !!generation?.active;configurationKey=key;const previous=generation;generation=null;retire(previous);if(!valid(config)||retiring.size>=4)return false;
|
||||
let current;try{current={active:true,config,context:attributes(context),agents:new Set(),failedExports:0};
|
||||
const agentFactory=async protocol=>{const Base=protocol==='https:'?https.Agent:http.Agent;class PrivateAgent extends Base{addRequest(request,options){if(!current.active){request.destroy(Error('Diagnostics disabled'));return;}for(const key of request.getHeaderNames())if(!['host','content-type','content-length','user-agent'].includes(key.toLowerCase()))request.removeHeader(key);super.addRequest(request,options);}}
|
||||
const agent=new PrivateAgent({keepAlive:false,maxSockets:1,rejectUnauthorized:true});current.agents.add(agent);return agent;};
|
||||
const base=config.endpoint.replace(/\/$/,'');const exporterConfig=path=>({url:base+path,headers:{},timeoutMillis:config.exportTimeoutMs,concurrencyLimit:1,compression:'none',httpAgentOptions:agentFactory});
|
||||
const resource=resourceFromAttributes({'service.name':'safetynet-browser','service.version':'0.1.0','safetynet.diagnostics.mode':config.mode});
|
||||
const limits={maxQueueSize:config.maxQueueSize,maxExportBatchSize:Math.min(config.maxQueueSize,128),scheduledDelayMillis:config.flushIntervalMs,exportTimeoutMillis:config.exportTimeoutMs};
|
||||
current.tracerProvider=new TracerProvider({resource,sampler:new ParentBasedSampler({root:new TraceIdRatioBasedSampler(config.mode==='debug'?config.debugSampleRate:config.sampleRate)}),spanLimits:{attributeCountLimit:20,attributeValueLengthLimit:64,eventCountLimit:0,linkCountLimit:0},spanProcessors:[new BatchSpanProcessor({...limits,exporter:gatedExporter(new OTLPTraceExporter(exporterConfig('/v1/traces')),current)})]});
|
||||
current.loggerProvider=new LoggerProvider({resource,logRecordLimits:{attributeCountLimit:20,attributeValueLengthLimit:64},processors:[new BatchLogRecordProcessor({...limits,exporter:gatedExporter(new OTLPLogExporter(exporterConfig('/v1/logs')),current)})]});current.tracer=current.tracerProvider.getTracer('safetynet.browser','0.1.0');current.logger=current.loggerProvider.getLogger('safetynet.browser','0.1.0');generation=current;return true;
|
||||
}catch{generation=null;retire(current);return false;}
|
||||
}
|
||||
function event(name,input={}){const current=generation;if(!current?.active||!names.has(name))return;try{const parent=storage.getStore(),rate=current.config.mode==='debug'?current.config.debugSampleRate:current.config.sampleRate;if(parent?.generation===current){if(!parent.span.isRecording())return;}else if(randomInt(1000000)>=Math.floor(rate*1000000))return;
|
||||
current.logger.emit({severityNumber:9,severityText:'INFO',body:name,attributes:{...current.context,...attributes(input)},...(parent?.generation===current?{context:trace.setSpan(ROOT_CONTEXT,parent.span)}:{})});
|
||||
}catch{}
|
||||
}
|
||||
async function span(name,input,fn){if(typeof fn!=='function')throw TypeError('Diagnostics operation requires a function');const current=generation;if(!current?.active||!names.has(name)||activeSpans>=128)return fn();let nativeSpan;try{const parent=storage.getStore(),context=parent?.generation===current?trace.setSpan(ROOT_CONTEXT,parent.span):ROOT_CONTEXT;nativeSpan=current.tracer.startSpan(name,{attributes:{...current.context,...attributes(input)}},context);}catch{return fn();}
|
||||
activeSpans++;const started=performance.now();let status='ok';try{const result=await storage.run({generation:current,span:nativeSpan},fn);try{nativeSpan.setStatus({code:SpanStatusCode.OK});}catch{}return result;}catch(error){status='error';try{nativeSpan.setStatus({code:SpanStatusCode.ERROR});nativeSpan.setAttribute('error.type',error instanceof TypeError?'TypeError':error instanceof RangeError?'RangeError':'Error');}catch{}throw error;}finally{activeSpans--;try{const sampled=nativeSpan.isRecording();nativeSpan.setAttribute('safetynet.status',status);nativeSpan.setAttribute('safetynet.duration.ms',Math.max(0,performance.now()-started));nativeSpan.end();if(current.active&&sampled)current.logger.emit({severityNumber:9,severityText:'INFO',body:name,attributes:{...current.context,...attributes(input),'safetynet.status':status,'safetynet.duration.ms':Math.max(0,performance.now()-started)},context:trace.setSpan(ROOT_CONTEXT,nativeSpan)});}catch{}}
|
||||
}
|
||||
async function flush(){const current=generation;if(!current?.active)return;await Promise.allSettled([current.tracerProvider.forceFlush({timeoutMillis:current.config.exportTimeoutMs}),current.loggerProvider.forceFlush({timeoutMillis:current.config.exportTimeoutMs})]);}
|
||||
async function shutdown(){if(closed)return;await flush();closed=true;const current=generation;generation=null;retire(current);await Promise.allSettled([...retiring]);}
|
||||
return {configure,span,event,flush,shutdown,correlation:()=>{const parent=storage.getStore();if(!parent||parent.generation!==generation||!generation?.active||!parent.span.isRecording())return {};const ids=parent.span.spanContext();return {traceId:ids.traceId,spanId:ids.spanId};},status:()=>({enabled:!!generation?.active,mode:generation?.config.mode||'operational',failedExports:generation?.failedExports||0,activeSpans})};
|
||||
}
|
||||
module.exports={createDiagnostics,attributes,endpointAllowed};
|
||||
Reference in new issue
Block a user