import inspectionStages from '../../packages/core/inspection-stages.cjs'; import { NodeSDK } from '@opentelemetry/sdk-node'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { trace, metrics,context,ROOT_CONTEXT,SpanStatusCode } from '@opentelemetry/api'; import { logs } from '@opentelemetry/api-logs'; import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs'; import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'; import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'; import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'; export function startTelemetry() { if(!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) return null; const endpoint=new URL(process.env.OTEL_EXPORTER_OTLP_ENDPOINT); if(endpoint.username||endpoint.password||endpoint.search||endpoint.hash||!(endpoint.protocol==='https:'||endpoint.protocol==='http:'&&['127.0.0.1','[::1]'].includes(endpoint.hostname)))throw Error('OTLP requires HTTPS or numeric loopback without URL credentials'); const sdk = new NodeSDK({serviceName:'safetynet-control-plane',traceExporter:new OTLPTraceExporter(), metricReaders:[new PeriodicExportingMetricReader({exporter:new OTLPMetricExporter(),exportIntervalMillis:10000})], logRecordProcessors:[new BatchLogRecordProcessor({exporter:new OTLPLogExporter()})]}); sdk.start(); return sdk; } export const tracer=trace.getTracer('safetynet','0.1.0'); const stages=new Set(['evidence.challenge','evidence.decrypt','evidence.store','evidence.encrypt','audit.verify','startpage.news','ai.gateway','ai.inspect.input','ai.inspect.output','ai.provider.request','ai.provider.read','ai.provider.decode','library.propose','library.validate','library.apply','library.derive','server.request']); export async function traceOperation(name,attributes,fn){ if(typeof fn!=='function')throw TypeError('Trace operation requires a function'); if(!stages.has(name))return fn(); const safe={};for(const [key,value]of Object.entries(attributes||{}))if(['bytes','count','policyVersion'].includes(key)&&Number.isSafeInteger(value)&&value>=0)safe[`safetynet.${key}`]=value; return tracer.startActiveSpan(name,{attributes:safe},async span=>{const start=performance.now();try{return await fn();}catch(error){span.setStatus({code:SpanStatusCode.ERROR});span.setAttribute('error.type',error instanceof TypeError?'TypeError':error instanceof RangeError?'RangeError':'Error');throw error;}finally{span.setAttribute('safetynet.duration.ms',performance.now()-start);span.end();}}); } export function installRequestTelemetry(app){ app.use((req,res,next)=>{ if(!req.path.startsWith('/api/'))return next(); const match=/^00-([a-f0-9]{32})-([a-f0-9]{16})-(0[01])$/.exec(req.headers.traceparent||''); const parent=match&&match[1]!=='0'.repeat(32)&&match[2]!=='0'.repeat(16)?trace.setSpanContext(ROOT_CONTEXT,{traceId:match[1],spanId:match[2],traceFlags:parseInt(match[3],16),isRemote:true}):ROOT_CONTEXT; const span=tracer.startSpan('server.request',{attributes:{'http.request.method':['GET','POST','PUT','PATCH','DELETE','OPTIONS'].includes(req.method)?req.method:'OTHER'}},parent),start=performance.now();let ended=false; function end(){if(ended)return;ended=true;const route=req.route?.path;if(typeof route==='string'&&route.length<=120)span.setAttribute('http.route',route);span.setAttribute('http.response.status_code',res.statusCode);span.setAttribute('safetynet.duration.ms',performance.now()-start);if(res.statusCode>=500)span.setStatus({code:SpanStatusCode.ERROR});span.end();} res.once('finish',end);res.once('close',end);context.with(trace.setSpan(parent,span),next); }); } export function recordSecurityEvent(kind,metadata,eventId){ // Only bounded, allowlisted fields. Never emit body text, rules, vectors or prompts. const decision=metadata.decision??(kind.includes('block')?'block':'other'); metrics.getMeter('safetynet').createCounter('safetynet.security.events').add(1,{'event.action':kind,'dlp.decision':decision}); logs.getLogger('safetynet.security').emit({severityNumber:kind.includes('block')?13:9,severityText:kind.includes('block')?'WARN':'INFO',body:kind, attributes:{'event.id':eventId,'event.action':kind,'dlp.decision':decision,...(metadata.ruleId?{'rule.id':metadata.ruleId}:{}),...(metadata.policyVersion?{'safetynet.policy.version':metadata.policyVersion}:{})}}); } export function importInspectionStages(envelope,policy){ try{ if(policy?.diagnostics?.enabled!==true||policy.diagnostics.mode!=='debug')return; const parent=context.active(),active=trace.getSpan(parent);if(!active?.isRecording())return; const batch=inspectionStages.validateStages(envelope);if(!batch)return; const parents=new Map();for(const row of batch.records){const scope=row.parentId===null?parent:parents.get(row.parentId);if(!scope)return;const span=tracer.startSpan('inspector.'+row.stage,{startTime:batch.baseUnixMs+row.startOffsetMs,attributes:{'safetynet.component':'inspector','safetynet.status':row.status,'safetynet.duration.ms':row.durationMs}},scope);if(row.status==='error')span.setStatus({code:SpanStatusCode.ERROR});parents.set(row.id,trace.setSpan(scope,span));span.end(batch.baseUnixMs+row.startOffsetMs+row.durationMs);} }catch{/* Diagnostics cannot change a DLP decision. */} }