UNPKG

@luminati-io/luminati-proxy

Version:

A configurable local proxy for luminati.io

563 lines (517 loc) 18.9 kB
// LICENSE_CODE ZON 'use strict'; /*zlint node*/ require('./config.js'); const cluster = require('cluster'); const cluster_ipc = require('./cluster_ipc.js'); const conf = require('./conf.js'); const date = require('./date.js'); const etask = require('./etask.js'); const file = require('./file.js'); const queue = require('./queue.js'); const zerr = require('./zerr.js'); const zutil = require('./util.js'); const env = process.env, ef = etask.ef, ms = date.ms; const E = exports; const interval = 10*ms.SEC, counter_factor = ms.SEC/interval; const max_age = 30*ms.SEC, level_eco_dispose = ms.HOUR; let hosts = ['zs-graphite.luminati.io']; if (!+env.LXC) hosts.push('zs-graphite-log.luminati.io'); E.enable_submit = when=>{ E.enable_submit = ()=>{ if (process.env.IS_MOCHA) return; zerr.warn('zcounter.enable_submit() was already called: ignoring.'); }; if (+process.env.ZCOUNTER_DROP) when = false; switch (when) { case false: init(); return zerr.warn(`zcounter will not be submitted`); case 'always': return run(); case 'production': if (env.NODE_ENV=='production') return run(); break; } zerr.warn(`zcounter is disabled`); }; let to_valid_ids = {}; E.to_valid_id = id=>to_valid_ids[id]|| (to_valid_ids[id]=id.toLowerCase().replace(/[^a-z0-9_]/g, '_')); // XXX vladislavl: convert to Map all structure let type = {sum: new Map(), avg: {}, sum_mono: {}, avg_level: {}, sum_level: {}, max_level: {}, min_level: {}, avg_level_eco: {}, sum_level_eco: {}, sum_mono_eco: {}}; let reported_names = {}; function report_invalid_val(name, value){ let parts = name.split('/'); name = parts[parts.length-1]; if (reported_names[name]) return; reported_names[name] = true; zerr.perr('zcounter_invalid_val', {name, value}); } // monotonic counters: http req/sec, bytes/sec E.inc = (name, inc=1, agg_srv='sum')=>{ if (!Number.isFinite(inc)) return void report_invalid_val(name, inc); let _type = type.sum, entry = _type.get(name); if (!entry) _type.set(name, entry = {v: 0, agg_srv, agg_tm: 'avg0'}); entry.v += inc; }; // monotonic counters read from elsewhere: pkt rx/tx on interface from kernel E.minc = (name, value, agg_srv='sum')=>{ if (!Number.isFinite(value)) return void report_invalid_val(name, value); let _type = type.sum_mono, entry = _type[name]; if (!entry) entry = _type[name] = {v: 0, b: value, agg_srv, agg_tm: 'avg0'}; // handle wraparound if (value<entry.b) entry.b = value; entry.v = value-entry.b; }; // minc but starts sending after non-0 value and stops after 0-only for 1h E.minc_eco = (name, value, agg_srv='sum')=>{ if (!Number.isFinite(value)) return void report_invalid_val(name, value); let _type = type.sum_mono_eco, entry = _type[name]; if (!entry) entry = _type[name] = {v: 0, b: value, agg_srv, agg_tm: 'avg0'}; // handle wraparound if (value<entry.b) entry.b = value; entry.v = value-entry.b; }; // current in-progress counter: num of open tcp connections: +1 open, -1 close E.inc_level = (name, inc=1, agg_mas='avg', agg_srv='avg')=>{ if (!Number.isFinite(inc)) return void report_invalid_val(name, inc); let _type = type[agg_mas+'_level'], entry = _type[name]; if (!entry) entry = _type[name] = {v: 0, agg_srv}; entry.v += inc; }; // inc_level but starts sending after non-0 value and stops after 0-only for 1h E.inc_level_eco = (name, inc=1, agg_mas='avg', agg_srv='avg')=>{ if (!Number.isFinite(inc)) return void report_invalid_val(name, inc); let _type = type[agg_mas+'_level_eco'], entry = _type[name]; if (!entry) entry = _type[name] = {v: 0, agg_srv}; entry.v += inc; }; // current in-progress counter read from elsewhere, or absolute value such // as %cpu or %disk usage E.set_level = (name, value, agg_mas='avg', agg_srv='avg')=>{ if (!Number.isFinite(value)) return void report_invalid_val(name, value); let _type = type[agg_mas+'_level'], entry = _type[name]; if (!entry) entry = _type[name] = {v: 0, agg_srv}; entry.v = value; }; // set_level but starts sending after non-0 value and stops after 0-only for 1h E.set_level_eco = (name, value, agg_mas='avg', agg_srv='avg')=>{ if (!Number.isFinite(value)) return void report_invalid_val(name, value); let _type = type[agg_mas+'_level_eco'], entry = _type[name]; if (!entry) entry = _type[name] = {v: 0, agg_srv}; entry.v = value; }; E.avg = (name, value, agg_srv)=>E.avgw(name, value, 1, agg_srv); E.avgw = (name, value, weight, agg_srv='avg')=>{ if (!Number.isFinite(value)) return void report_invalid_val(name, value); if (!Number.isFinite(weight) || weight<0) return void report_invalid_val(name, weight); let _type = type.avg, entry = _type[name]; if (!entry) entry = _type[name] = {v: 0, w: 0, agg_srv}; entry.v += value; entry.w += weight; }; E.max = (name, value, agg_srv='max', agg_tm='max')=>{ if (!Number.isFinite(value)) return void report_invalid_val(name, value); let _type = type.max_level, entry = _type[name]; if (!entry) entry = _type[name] = {v: value, agg_srv, agg_tm}; if (entry.v<value) entry.v = value; }; E.min = (name, value, agg_srv='min')=>{ if (!Number.isFinite(value)) return void report_invalid_val(name, value); let _type = type.min_level, entry = _type[name]; if (!entry) entry = _type[name] = {v: value, agg_srv, agg_tm: 'min'}; if (entry.v>value) entry.v = value; }; function _get(_type, n){ let c = _type.avg_level[n]||_type.sum_level[n]||_type.avg[n] ||(_type.sum instanceof Map ? _type.sum.get(n) : _type.sum[n]) ||_type.sum_mono[n]||_type.max_level[n]||_type.min_level[n] ||_type.avg_level_eco[n]||_type.sum_level_eco[n] ||_type.sum_mono_eco[n]; if (c && c.w!==undefined) return c.v/c.w; return c ? c.v : 0; } E.get = n=>_get(type, n); // The ext_* versions of the above functions provide for reporting for a // different server than the local host. E.ext_inc = (server, name, inc, agg_srv)=>E.inc(server+'/'+name, inc, agg_srv); E.ext_minc = (server, name, value, agg_srv)=> E.minc(server+'/'+name, value, agg_srv); E.ext_inc_level = (server, name, inc, agg_mas, agg_srv)=> E.inc_level(server+'/'+name, inc, agg_mas, agg_srv); E.ext_set_level = (server, name, value, agg_mas, agg_srv)=> E.set_level(server+'/'+name, value, agg_mas, agg_srv); E.ext_avg = (server, name, value, agg_srv)=> E.avg(server+'/'+name, value, agg_srv); E.ext_avgw = (server, name, value, weight, agg_srv)=> E.avgw(server+'/'+name, value, weight, agg_srv); E.ext_max = (server, name, value, agg_srv)=> E.max(server+'/'+name, value, agg_srv); E.ext_min = (server, name, value, agg_srv)=> E.min(server+'/'+name, value, agg_srv); E.ext_get = (server, name)=>E.get(server+'/'+name); // The group_* versions of the functions provide group reporting for non-tun // zagents metrics while regular reporting for servers and tun zagents E.group_inc = E.inc; E.group_avg = E.avg; E.group_max = (name, value, agg_srv)=>E.max(name, value, agg_srv, 'avg'); E.group_set_level = E.set_level; if (env.ZCOUNTER_GROUP!==undefined) { let groups = ['', '_g_'+env.ZCOUNTER_GROUP]; if (env.AGENT_DC) groups.push('_g_'+env.AGENT_COUNTRY+'_'+env.AGENT_DC); E.group_inc = (name, inc, agg_srv)=>groups.forEach(g=> E.inc('glob/'+name+g, inc, agg_srv)); E.group_avg = (name, value, agg_srv)=>groups.forEach(g=> E.avg('glob/'+name+g, value, agg_srv)); E.group_max = (name, value, agg_srv)=>groups.forEach(g=> E.avg('glob/'+name+g, value, agg_srv)); E.group_set_level = (name, value, agg_mas, agg_srv)=>groups.forEach(g=> E.set_level('glob/'+name+g, value, agg_mas, agg_srv)); } E.glob_inc = (name, inc, agg_srv)=>E.inc('glob/'+name, inc, agg_srv); E.glob_inc_level = (name, inc, agg_mas, agg_srv)=> E.inc_level('glob/'+name, inc, agg_mas, agg_srv); E.glob_avg = (name, value, agg_srv)=>E.avg('glob/'+name, value, agg_srv); E.glob_max = (name, value, agg_srv)=>E.max('glob/'+name, value, agg_srv); E.glob_min = (name, value, agg_srv)=>E.min('glob/'+name, value, agg_srv); E.glob_set_level = (name, value, agg_mas, agg_srv)=> E.set_level('glob/'+name, value, agg_mas, agg_srv); E.del = name=>{ if (reported_names[name]) { delete reported_names[name]; return; } for (let agg_type in type) { let _type = type[agg_type]; if (_type instanceof Map) _type.delete(name); else if (_type[name]) delete _type[name]; } }; function pluck(obj, key){ return obj.map(v=>v[key]); } function agg_max(val, cnt){ val.v = val.v===undefined ? cnt.v : Math.max(val.v, cnt.v); } function agg_min(val, cnt){ val.v = val.v===undefined ? cnt.v : Math.min(val.v, cnt.v); } function agg_sum(val, cnt){ val.v = (val.v||0)+cnt.v; } function agg_avg(val, cnt){ val.v = (val.v||0)+cnt.v; val.w = (val.w||0)+1; } function agg_avgw(val, cnt){ val.v = (val.v||0)+cnt.v; val.w = (val.w||0)+cnt.w; } const aggs = {sum: agg_sum, sum_mono: agg_sum, avg_level: agg_avg, max_level: agg_max, min_level: agg_min, sum_level: agg_sum, avg: agg_avgw, avg_level_eco: agg_avg, sum_level_eco: agg_sum, sum_mono_eco: agg_sum}; function mas_agg_counters(worker_counters){ let res = zutil.map_obj(aggs, (agg, key)=>{ let worker_counter = pluck(worker_counters, key); let vals = {}; for (let i = 0; i<worker_counter.length; i++) { let cs = worker_counter[i]; // Object.keys/for is 25% faster than for..in let ids = Object.keys(cs); for (let j=0; j<ids.length; j++) { let id = ids[j], _cs = cs[id]; if (!vals[id]) vals[id] = {agg_srv: _cs.agg_srv, agg_tm: _cs.agg_tm}; agg(vals[id], _cs); } } return vals; }); return res; } E.on_send = []; const eco_ts = {}; let loc_get_counters = update_prev=>etask(function*zcounter_loc_get_counters(){ let ret = Object.assign({}, type); if (update_prev) { for (let fn of E.on_send) try { yield fn(); } catch(e){ ef(e); } // base gauge for 'sum_mono' kind should be reset so that next // measurement will be relative to the current value let mono = {sum_mono: {}, sum_mono_eco: {}}; ['sum_mono', 'sum_mono_eco'].forEach(m=>{ let mono_new = mono[m]; let mono_cur = type[m]; for (let c in mono_cur) { let cur = mono_cur[c]; mono_new[c] = {v: 0, b: cur.b+cur.v, agg_srv: cur.agg_srv, agg_tm: cur.agg_tm}; } }); type = {sum: new Map(), avg: {}, sum_mono: mono.sum_mono, avg_level: type.avg_level, sum_level: type.sum_level, max_level: {}, min_level: {}, avg_level_eco: type.avg_level_eco, sum_level_eco: type.sum_level_eco, sum_mono_eco: mono.sum_mono_eco}; } let now = Date.now(), sum = ret.sum; ret.sum = {}; for (let [k, v] of sum) ret.sum[k] = v; [ret.avg_level_eco, ret.sum_level_eco].forEach(eco=>{ for (let t in eco) { let c = eco[t]; if (c.v) { eco_ts[t] = now; continue; } let ts = eco_ts[t]; if (!ts || now-ts>level_eco_dispose) { delete eco_ts[t]; delete eco[t]; } } }); let sum_mono_eco = {}; let eco = ret.sum_mono_eco; for (let t in eco) { let c = eco[t]; if (c.v) { eco_ts[t] = now; sum_mono_eco[t] = c; continue; } let ts = eco_ts[t]; if (ts && now-ts<level_eco_dispose) sum_mono_eco[t] = c; } ret.sum_mono_eco = sum_mono_eco; return ret; }); let mas_get_counters = update_prev=>etask(function*zcounter_mas_get_counters(){ update_prev = update_prev||false; let a = cluster_ipc.call_all_workers('get_zcounters', update_prev) .concat(yield loc_get_counters(update_prev)); let counters = yield etask.all({allow_fail: true}, a); return mas_agg_counters(counters.filter(v=>v && !etask.is_err(v))); }); E.get_names = _type=>type[_type] instanceof Map ? [...type[_type].keys()] : Object.keys(type[_type]); function ws_format(metrics){ let res = [], last = ''; for (let i = 0; i<metrics.length; i++) { let metric = metrics[i], path = metric.path; let repeat = 0; while (repeat<last.length && repeat<path.length && last[repeat]==path[repeat]) { repeat++; } last = path; let row = [repeat, path.slice(repeat), metric.v]; if (metric.agg_srv && metric.agg_srv!='avg') row[4] = metric.agg_srv; if (metric.agg_tm && metric.agg_tm!='avg') row[5] = metric.agg_tm; if (row.length>3 || metric.w && metric.w!=1) row[3] = metric.w||1; res.push(row); } return res; } function agg_mas_sum_fn(id, c){ return {v: c.v*counter_factor, w: undefined}; } function agg_mas_avg_fn(id, c){ return c; } function agg_mas_level_fn(id, c){ return {v: c.w!==undefined ? c.v/c.w : c.v, w: undefined}; } const agg_mas_fn = {sum: agg_mas_sum_fn, sum_mono: agg_mas_sum_fn, avg: agg_mas_avg_fn, avg_level: agg_mas_level_fn, sum_level: agg_mas_level_fn, max_level: agg_mas_level_fn, min_level: agg_mas_level_fn, avg_level_eco: agg_mas_level_fn, sum_level_eco: agg_mas_level_fn, sum_mono_eco: agg_mas_sum_fn}; let prepare = ()=>etask(function*zcounter_prepare(){ let get_counters_fn = Object.keys(cluster.workers).length ? mas_get_counters : loc_get_counters; let counters = yield get_counters_fn(true); let prefix = `stats.${conf.hostname}.${conf.app}.`; let metrics = []; for (let t in agg_mas_fn) { let _type = counters[t]; let agg_fn = agg_mas_fn[t]; for (let key in _type) { let c = _type[key], agg = agg_fn(key, c); let path; if (key[0]=='.') path = key.slice(1).replace(/\//g, '.'); else { let parts = key.split('/'); if (parts.length==1) path = prefix+key; else path = `stats.${parts[0]}.${conf.app}.${parts[1]}`; } metrics.push({path, v: agg.v, w: agg.w, agg_srv: c.agg_srv, agg_tm: c.agg_tm}); } } return metrics; }); let get_agg_counters = ()=>etask(function*zcounter_get_agg_counters(){ if (cluster.isMaster) return yield mas_get_counters(); return yield cluster_ipc.call_master('get_zcounters'); }); let handler_get = (req, res)=>etask(function*zcounter_handler_get(){ let counters = yield get_agg_counters(); if (req.url.match(/^\/procinfo\/zcounter(\/|)$/)) return void res.json({counters}); let name = req.url.replace(/^\/procinfo\/zcounter\//, ''); res.type('text/plain'); res.send(''+(_get(counters, name)||0)); }); function init(){ const procinfo = require('./procinfo.js'); procinfo.register('/procinfo/zcounter', handler_get, false); procinfo.register('/procinfo/zcounter/*', handler_get, false); if (cluster.isMaster) cluster_ipc.master_on('get_zcounters', mas_get_counters); else cluster_ipc.worker_on('get_zcounters', loc_get_counters); } let ws_queue = new queue('zcounter'); const ws_conn = new Map(); let send_loop = ()=>etask(function*(){ if (send_loop.et) zerr.perr('zcounter_double_conn'); this.finally(()=>send_loop.et = null); send_loop.et = this; for (;;) { const data = ws_format(yield ws_queue.wait()); if (!ws_conn.size) return void (send_loop.et = null); ws_conn.forEach(ws=>ws.json(data)); } }); function ws_client(url){ if (!url.startsWith('ws')) url = `ws://${url}`; if (ws_conn.has(url)) return ws_conn.get(url); zerr.notice(`Opening zcounter conn to ${url}`); let zws = require('./ws.js'); const label = url.split(':')[1].replace(/^\/\//, ''); const ws = new zws.Client(url, {label: `zcounter-${label}`, retry_interval: ms.SEC}); ws.on('connected', ()=>{ ws_conn.set(url, ws); if (!send_loop.et) send_loop(); }); ws.on('disconnected', ()=>ws_conn.delete(url)); return ws; } function run(){ init(); if (cluster.isWorker) return; let port = env.ZCOUNTER_PORT||3374; if (env.NODE_ENV!='production') ws_client(`ws://localhost:${port}`); else if (env.ZCOUNTER_URL) env.ZCOUNTER_URL.split(';').forEach(ws_client); else hosts.forEach(h=>ws_client(`ws://${h}:${port}`)); etask.interval({ms: interval, mode: 'smart'}, function*zcounter_run(){ let metrics = yield prepare(); if (metrics.length) ws_queue.put(metrics, max_age); }); } E.submit_raw = (metrics, ttl)=>ws_queue.put(metrics, ttl); E.flush = ()=>etask(function*zcounter_flush(){ if (cluster.isWorker) zerr.zexit('zcounter.flush must be called from the master'); let metrics = yield prepare(); if (metrics.length) ws_queue.put(metrics, max_age); if (ws_conn.size) return; const dir = env.ZCOUNTER_DIR||'/run/shm/zcounter'; for (;;) { let entry = ws_queue.get_ex(); if (!entry) break; let r = Math.random()*1e8|0; file.mkdirp(dir); file.write_atomic(`${dir}/${entry.expires}.${conf.app}.${r}.json`, JSON.stringify(entry.item)); } }); let agent_conf; E.is_debug = title=>{ if (!agent_conf) { let system_db = require('../system/db/db.js'); agent_conf = system_db.use('agent_conf'); } let debug = agent_conf.debug_zcounter; let v = debug && debug[title]; if (typeof v=='object') return v.includes(+env.AGENT_NUM); return !!v; }; function test_reset(){ type = {sum: new Map(), avg: {}, sum_mono: {}, avg_level: {}, sum_level: {}, max_level: {}, min_level: {}, avg_level_eco: {}, sum_level_eco: {}, sum_mono_eco: {}}; } E.t = {loc_get_counters, get_agg_counters, _get, prepare, ws_client, test_reset};