UNPKG

edge-utils

Version:

Platform-agnostic utilities for edge computing and serverless environments

3 lines (2 loc) 7.62 kB
const t=require("crypto");class e{constructor(t={}){this.level=t.level||"info",this.format=t.format||"json",this.sampling=t.sampling||{},this.redaction=t.redaction||[],this.aggregation=t.aggregation||{},this.forceConsoleLog=!1!==t.forceConsoleLog,this.levels={debug:0,info:1,warn:2,error:3,fatal:4}}debug(t,e={}){this._log("debug",t,e)}info(t,e={}){this._log("info",t,e)}warn(t,e={}){this._log("warn",t,e)}error(t,e={}){this._log("error",t,e)}fatal(t,e={}){this._log("fatal",t,e)}child(t={}){const s=new e({level:this.level,format:this.format,sampling:this.sampling,redaction:this.redaction,aggregation:this.aggregation});return s._baseContext={...this._baseContext,...t},s}_log(t,e,s={}){if(this.levels[t]<this.levels[this.level])return;if(this._shouldSample(t,s))return;const a={timestamp:(new Date).toISOString(),level:t,message:e,...this._baseContext,...this._redact(s)};s.requestId&&(a.requestId=s.requestId),s.traceId&&(a.traceId=s.traceId);const r=this._format(a);this.forceConsoleLog?console.log(r):"undefined"!=typeof process&&process.stderr?"error"===t||"fatal"===t?process.stderr.write(r+"\n"):process.stdout.write(r+"\n"):console.log(r),this._aggregate(a)}_shouldSample(e,s){const a=this.sampling[e];if(!a||a>=1)return!1;const r=t.createHash("md5").update(`${e}:${JSON.stringify(s)}`).digest("hex");return parseInt(r.substring(0,8),16)/4294967295>a}_redact(t){const e={...t};for(const t of this.redaction)e[t]&&(t.toLowerCase().includes("password")||t.toLowerCase().includes("secret")||t.toLowerCase().includes("token")?e[t]="[REDACTED]":t.toLowerCase().includes("email")?e[t]=e[t].replace(/(.{2}).*(@.*)/,"$1***$2"):t.toLowerCase().includes("ip")&&(e[t]=e[t].replace(/(\d+\.\d+\.\d+)\.\d+/,"$1.***")));return e}_format(t){return"json"===this.format?JSON.stringify(t):`[${t.timestamp}] ${t.level.toUpperCase()}: ${t.message}`}_aggregate(t){const e=`${t.level}:${t.message}`;this.aggregation[e]||(this.aggregation[e]={count:0,firstSeen:t.timestamp}),this.aggregation[e].count++,this.aggregation[e].lastSeen=t.timestamp}getAggregationStats(){return{...this.aggregation}}}module.exports={MetricsCollector:class{constructor(t={}){this.metrics=new Map,this.histograms=new Map,this.counters=new Map,this.gauges=new Map,this.storage=t.storage,this.flushInterval=t.flushInterval||6e4,this.retentionPeriod=t.retentionPeriod||864e5,!1!==t.autoFlush&&setInterval(()=>this.flush(),this.flushInterval)}increment(t,e=1,s={}){const a=this._getKey(t,s),r=this.counters.get(a)||0;this.counters.set(a,r+e)}gauge(t,e,s={}){const a=this._getKey(t,s);this.gauges.set(a,{value:e,timestamp:Date.now()})}histogram(t,e,s={}){const a=this._getKey(t,s);this.histograms.has(a)||this.histograms.set(a,[]),this.histograms.get(a).push({value:e,timestamp:Date.now()})}timing(t,e,s={}){const a=Date.now()-e;this.histogram(t,a,s)}getCounter(t,e={}){const s=this._getKey(t,e);return this.counters.get(s)||0}getGauge(t,e={}){const s=this._getKey(t,e),a=this.gauges.get(s);return a?a.value:null}getHistogramPercentiles(t,e={},s=[50,95,99,99.9]){const a=this._getKey(t,e),r=this.histograms.get(a)||[];if(0===r.length)return{};const n=r.map(t=>t.value).sort((t,e)=>t-e),i={};for(const t of s){const e=Math.ceil(t/100*n.length)-1;i[`p${t}`]=n[Math.max(0,e)]}return i}getAllMetrics(){const t={counters:{},gauges:{},histograms:{}};for(const[e,s]of this.counters)t.counters[e]=s;for(const[e,s]of this.gauges)t.gauges[e]=s.value;for(const[e,s]of this.histograms)t.histograms[e]=this.getHistogramPercentiles(e.split(":")[0],this._parseTags(e));return t}async flush(){const t=this.getAllMetrics(),e=Date.now();try{const s=`metrics:${e}`;await(this.storage?.put?.(s,JSON.stringify(t)))||this.storage?.set?.(s,JSON.stringify(t))}catch(t){}this._cleanup()}_getKey(t,e){const s=Object.entries(e).sort(([t],[e])=>t.localeCompare(e)).map(([t,e])=>`${t}=${e}`).join(",");return s?`${t}:{${s}}`:t}_parseTags(t){const e=t.match(/{(.+)}/);if(!e)return{};const s={};return e[1].split(",").forEach(t=>{const[e,a]=t.split("=");s[e]=a}),s}_cleanup(){const t=Date.now()-this.retentionPeriod;for(const[e,s]of this.histograms){const a=s.filter(e=>e.timestamp>t);0===a.length?this.histograms.delete(e):this.histograms.set(e,a)}}},StructuredLogger:e,TracingManager:class{constructor(t={}){this.serviceName=t.serviceName||"edge-service",this.sampler=t.sampler||(()=>Math.random()<.1),this.exporter=t.exporter,this.spans=new Map,this.activeSpans=new Map}extract(t){const e=t?.traceparent,s=t?.tracestate;if(!e)return null;const a=e.split("-");return 4!==a.length?null:{version:a[0],traceId:a[1],parentId:a[2],flags:a[3],state:s}}inject(t,e){t&&(e.traceparent=`${t.version}-${t.traceId}-${t.spanId}-${t.flags}`,t.state&&(e.tracestate=t.state))}startSpan(t,e=null,s={}){const a=e?.traceId||this._generateTraceId(),r=this._generateSpanId(),n=e?.spanId,i={traceId:a,spanId:r,parentId:n,name:t,startTime:Date.now(),attributes:s,events:[]},o=e?e.sampled:this.sampler();return i.sampled=o,this.spans.set(r,i),this.activeSpans.set(r,i),{traceId:a,spanId:r,parentId:n,sampled:o,flags:o?"01":"00",version:"00"}}endSpan(t,e={}){const s=this.spans.get(t);s&&(s.endTime=Date.now(),s.duration=s.endTime-s.startTime,Object.assign(s.attributes,e),this.activeSpans.delete(t),s.sampled&&this.exporter&&this.exporter.export(s))}addEvent(t,e,s={}){const a=this.spans.get(t);a&&a.events.push({name:e,timestamp:Date.now(),attributes:s})}setAttributes(t,e){const s=this.spans.get(t);s&&Object.assign(s.attributes,e)}getActiveSpan(){const t=Array.from(this.activeSpans.values());return t[t.length-1]||null}_generateTraceId(){return t.randomBytes(16).toString("hex")}_generateSpanId(){return t.randomBytes(8).toString("hex")}},HealthCheckManager:class{constructor(t={}){this.checks=new Map,this.overallTimeout=t.timeout||3e4,this.cache=new Map,this.cacheTimeout=t.cacheTimeout||5e3}addCheck(t,e,s={}){this.checks.set(t,{fn:e,timeout:s.timeout||5e3,critical:!1!==s.critical,tags:s.tags||[]})}async runChecks(){const t={status:"healthy",timestamp:(new Date).toISOString(),checks:{},uptime:process.uptime?.()||0},e=[];for(const[t,s]of this.checks)e.push({name:t,promise:this._runCheck(t,s)});const s=await Promise.allSettled(e.map(t=>t.promise));for(let a=0;a<s.length;a++){const{name:r}=e[a],n=s[a];"fulfilled"===n.status?(t.checks[r]=n.value,!n.value.healthy&&this.checks.get(r).critical&&(t.status="unhealthy")):(t.checks[r]={healthy:!1,error:n.reason.message,duration:0},t.status="unhealthy")}return t}getLiveness(){return{status:"healthy",timestamp:(new Date).toISOString(),uptime:process.uptime?.()||0}}async getReadiness(){const t=this.cache.get("readiness");if(t&&Date.now()-t.timestamp<this.cacheTimeout)return t.result;const e=await this.runChecks();return this.cache.set("readiness",{result:e,timestamp:Date.now()}),e}async _runCheck(t,e){const s=Date.now();try{const t=await Promise.race([e.fn(),new Promise((t,s)=>setTimeout(()=>s(new Error("Timeout")),e.timeout))]),a=Date.now()-s;return"boolean"==typeof t?{healthy:t,duration:a}:"object"==typeof t?{healthy:!1!==t.healthy,duration:a,...t}:{healthy:!0,duration:a}}catch(t){return{healthy:!1,error:t.message,duration:Date.now()-s}}}middleware(){return async(t,e)=>{const s=new URL(t.url).pathname;if("/health/live"===s||"/health/liveness"===s){const t=this.getLiveness();return new Response(JSON.stringify(t),{status:200,headers:{"Content-Type":"application/json"}})}if("/health/ready"===s||"/health/readiness"===s){const t=await this.getReadiness(),e="healthy"===t.status?200:503;return new Response(JSON.stringify(t),{status:e,headers:{"Content-Type":"application/json"}})}return null}}}}; //# sourceMappingURL=monitoring.esm.js.map