Changeset - 04ed5d134973
[Not reviewed]
default
0 6 0
drewp@bigasterisk.com - 3 years ago 2022-04-09 09:47:45
drewp@bigasterisk.com
WIP draw prom metrics on homepage
6 files changed with 134 insertions and 52 deletions:
0 comments (0 inline, 0 general)
bin/homepageConfig
Show inline comments
 
@@ -29,20 +29,33 @@ def location(path, server):
 
      proxy_buffering off;
 
      rewrite /[^/]+/(.*) /$1 break;
 
    }}""")
 

	
 

	
 
for role, server in sorted(graph.predicate_objects(netHome)):
 
    if not server.startswith('http') or role == L9['webServer']:
 
        continue
 
    path = graph.value(role, L9['urlPath'])
 
    if not path:
 
        continue
 
    server = server.rstrip('/')
 
    if 'collector' in path: continue
 
    location(path, server)
 

	
 
print('''
 

	
 
  location /collector/metrics {
 
    rewrite "/collector(/.*)" "$1" break;
 
    proxy_pass http://localhost:8202;
 
  }
 
  location /collector/ {
 
    proxy_pass http://localhost:8302;
 
  }
 

	
 
''')
 

	
 
showPath = showconfig.showUri().split('/', 3)[-1]
 
root = showconfig.root()[:-len(showPath)].decode('ascii')
 
print(f"""
 
    location /{showPath} {{
 
      root {root};
 
    }}""")
light9/web/homepage/ServiceButtonRow.ts
Show inline comments
 
@@ -41,23 +41,23 @@ export class ServiceButtonRow extends Li
 
      }
 
      :host > div:nth-child(2) {
 
        width: 9em;
 
      }
 
    `,
 
  ];
 

	
 
  render() {
 
    return html`
 
      <div>
 
        <div class="left"><a class="big" href="${this.name}/">${this.name}</a></div>
 
        <div class="window"><button @click="${this.click}">window</button></div>
 
        <div><a href="${this.name}/stats/">stats</a></div>
 
        <div><a href="${this.name}/metrics">metrics</a></div>
 
      </div>
 

	
 
      <div id="stats"><stats-line name="${this.name}"></div>
 
      `;
 
  }
 

	
 
  click() {
 
    window.open(this.name + "/", "_blank", "scrollbars=1,resizable=1,titlebar=0,location=0");
 
  }
 
}
light9/web/homepage/StatsLine.ts
Show inline comments
 
import { css, html, LitElement, TemplateResult } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
import { rounding } from "significant-rounding";
 
import { StatsProcess } from "./StatsProcess";
 
export { StatsProcess } from "./StatsProcess";
 
import parsePrometheusTextFormat from "parse-prometheus-text-format";
 

	
 
interface Value {
 
  labels: { string: string };
 
  value: string;
 
}
 
interface Metric {
 
  name: string;
 
  help: string;
 
  type: "GAUGE" | "SUMMARY" | "COUNTER";
 
  metrics: Value[];
 
}
 
type Metrics = Metric[];
 

	
 
@customElement("stats-line")
 
export class StatsLine extends LitElement {
 
  @property() name = "?";
 
  @property() stats: any;
 
  @property() stats: Metrics = [];
 

	
 
  updated(changedProperties: any) {
 
    changedProperties.forEach((oldValue: any, propName: string) => {
 
      if (propName == "name") {
 
        const reload = () => {
 
          fetch(this.name + "/stats/?format=json").then((resp) => {
 
          fetch(this.name + "/metrics").then((resp) => {
 
            if (resp.ok) {
 
              resp
 
                .json()
 
                .text()
 
                .then((msg) => {
 
                  this.stats = msg;
 
                  this.stats = parsePrometheusTextFormat(msg) as Metrics;
 
                  setTimeout(reload, 1000);
 
                })
 
                .catch((err) => {
 
                  setTimeout(reload, 1000);
 
                });
 
            } else {
 
              if (resp.status == 502) {
 
                setTimeout(reload, 5000);
 
              }
 
              // 404: likely not mapped to a responding server
 
            }
 
          });
 
@@ -75,42 +89,46 @@ export class StatsLine extends LitElemen
 
        background: red;
 
        border-right: 1px solid black;
 
      }
 
      .bigInt {
 
        min-width: 6em;
 
      }
 
    `,
 
  ];
 

	
 
  render() {
 
    const now = Date.now() / 1000;
 

	
 
    const table = (d: any, path: string[]): TemplateResult => {
 
      let cols = Object.keys(d);
 
    const table = (d: Metrics, path: string[]): TemplateResult => {
 
      const byName = new Map<string, Metric>();
 
      d.forEach((row) => {
 
        byName.set(row.name, row);
 
      });
 
      let cols = d.map((row) => row.name);
 
      cols.sort();
 

	
 
      if (path.length == 0) {
 
        ["webServer", "process"].forEach((earlyKey) => {
 
          let i = cols.indexOf(earlyKey);
 
          if (i != -1) {
 
            cols = [earlyKey].concat(cols.slice(0, i), cols.slice(i + 1));
 
          }
 
        });
 
      }
 

	
 
      const th = (col: string): TemplateResult => {
 
        return html`<th>${col}</th>`;
 
      };
 
      const td = (col: string): TemplateResult => {
 
        const cell = d[col];
 
        const cell = byName.get(col)!;
 
        return html`${drawLevel(cell, path.concat(col))}`;
 
      };
 
      return html` <table>
 
        <tr>
 
          ${cols.map(th)}
 
        </tr>
 
        <tr>
 
          ${cols.map(td)}
 
        </tr>
 
      </table>`;
 
    };
 

	
 
@@ -128,54 +146,83 @@ export class StatsLine extends LitElemen
 
          color = "#6a6aff";
 
        } else {
 
          color = "#d09e4c";
 
        }
 
        return html`<div class="bar" style="height: ${y * scl}px; background: ${color};"></div>`;
 
      };
 
      return html`<td>
 
        <div class="recents">${d.recents.map(bar)}</div>
 
        <div>avg=${rounding(d.average, 3)}</div>
 
      </td>`;
 
    };
 

	
 
    const pmf = (d: any, path: string[]) => {
 
    const pmf = (d: Metrics, path: string[]) => {
 
      return tdWrap(
 
        table(
 
          {
 
            count: d.count,
 
            "values [ms]": html`
 
              <div>mean=${rounding(d.mean * 1000, 3)}</div>
 
              <div>sd=${rounding(d.stddev * 1000, 3)}</div>
 
              <div>99=${rounding(d["99percentile"] * 1000, 3)}</div>
 
            `,
 
          },
 
          path
 
        )
 
      );
 
    };
 

	
 
    const drawLevel = (d: any, path: string[]) => {
 
    const drawLevel = (d: Metric, path: string[]) => {
 
      if (path.length == 1 && path[0] === "process") {
 
        const elem = this.shadowRoot!.querySelector("#proc");
 
        if (elem) {
 
          (elem as StatsProcess).data = d;
 
        }
 
        return html`<stats-process id="proc"></stats-process>`;
 
      }
 
      if (typeof d === "object") {
 
        if (d.strings) {//} instanceof TemplateResult) {
 
        if (d.strings) {
 
          //} instanceof TemplateResult) {
 
          return html`<td class="val">${d}</td>`;
 
        } else if (d.count !== undefined && d.min !== undefined) {
 
          return pmf(d, path);
 
        } else if (d.average !== undefined && d.recents !== undefined) {
 
          return recents(d, path);
 
        } else {
 
          return tdWrap(table(d, path));
 
        }
 
      } else {
 
        return html`<td class="val bigInt">${d}</td>`;
 
      }
 
    };
 

	
 
    return table(this.stats || {}, []);
 
const nonBoring = (m: Metric)=>{
 
  return !m.name.endsWith('_created') && m.name!= 'python_info'
 
}
 

	
 
    // return table(this.stats, []);
 
    return html`
 
      <table>
 
        ${this.stats.filter(nonBoring).map(
 
          (row) => html`
 
            <tr>
 
              <th>${row.name}</th>
 
              <td>
 
                <table>
 
                  ${row.metrics.map(
 
                    (v) => html`
 
                      <tr>
 
                        <td>${JSON.stringify(v.labels)}</td>
 
                        <td>${v.value}</td>
 
                      </tr>
 
                    `
 
                  )}
 
                </table>
 
              </td>
 
            </tr>
 
          `
 
        )}
 
      </table>
 
      <stats-process id="proc"></stats-process>
 
    `;
 
  }
 
}
light9/web/homepage/StatsProcess.ts
Show inline comments
 
import { LitElement, html, css } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
import debug from "debug";
 

	
 
const log = debug("process");
 

	
 
const remap = (x: number, lo: number, hi: number, outLo: number, outHi: number) => {
 
  return outLo + (outHi - outLo) * Math.max(0, Math.min(1, (x - lo) / (hi - lo)));
 
};
 

	
 
@customElement("stats-process")
 
export class StatsProcess extends LitElement {
 
  @property() data: any;
 
  @property() dataTime: number = 0; // millis at last data change
 
  @property() cpu: number = 12; // process_cpu_seconds_total
 
  @property() mem: number = 50000000; // process_resident_memory_bytes
 

	
 
  firstUpdated() {
 
  w = 64;
 
  h = 64;
 
  revs = 0;
 
  prev = 0;
 
  ctx?: CanvasRenderingContext2D;
 
  firstUpdated(c: Map<string, any>) {
 
    super.firstUpdated(c);
 
    // inspired by https://codepen.io/qiruiyin/pen/qOopQx
 
    var context = this.shadowRoot!.firstElementChild as HTMLCanvasElement;
 
    var ctx = context.getContext("2d")!,
 
      w = 64,
 
      h = 64,
 
      revs = 0;
 

	
 
    context.width = w;
 
    context.height = h;
 

	
 
    let prev = Date.now() / 1000;
 
    this.initCanvas(this.shadowRoot!.firstElementChild as HTMLCanvasElement);
 
    this.prev = Date.now() / 1000;
 

	
 
    var animate = () => {
 
      requestAnimationFrame(animate);
 

	
 
      const now = Date.now() / 1000;
 
      ctx.beginPath();
 
      // wrong type of fade- never goes to 0
 
      ctx.fillStyle = "#00000003";
 
      ctx.fillRect(0, 0, w, h);
 
      if (!this.data || this.data.time < now - 2) {
 
        return;
 
      }
 
      const dt = now - prev;
 
      prev = now;
 

	
 
      const size = remap(this.data.memMb, /*in*/ 20, 600, /*out*/ 3, 30);
 
      revs += dt * remap(this.data.cpuPercent, /*in*/ 0, 100, /*out*/ 4, 120);
 
      const rad = remap(size, /*in*/ 3, 30, /*out*/ 14, 5);
 

	
 
      var x = w / 2 + rad * Math.cos(revs / 6.28),
 
        y = h / 2 + rad * Math.sin(revs / 6.28);
 

	
 
      ctx.save();
 
      ctx.beginPath();
 
      ctx.fillStyle = "hsl(194, 100%, 42%)";
 
      ctx.arc(x, y, size, 0, 2 * Math.PI);
 
      ctx.fill();
 
      ctx.restore();
 
      this.redraw();
 
    };
 
    animate();
 
  }
 
  initCanvas(canvas: HTMLCanvasElement) {
 
    var ctx = canvas.getContext("2d")!;
 

	
 
  updated(changedProperties: any) {
 
    if (changedProperties.has("data")) {
 
      this.shadowRoot!.firstElementChild!.setAttribute("title", `cpu ${this.data.cpuPercent}% mem ${this.data.memMb}MB`);
 
    canvas.width = this.w;
 
    canvas.height = this.h;
 
  }
 
  redraw() {
 
    if (!this.ctx) return;
 
    const now = Date.now() / 1000;
 
    const ctx = this.ctx;
 
    ctx.beginPath();
 
    // wrong type of fade- never goes to 0
 
    ctx.fillStyle = "#00000003";
 
    ctx.fillRect(0, 0, this.w, this.h);
 
    // if (!this.data || this.data.time < now - 2) {
 
    //   return;
 
    // }
 
    const dt = now - this.prev;
 
    this.prev = now;
 

	
 
    const size = remap(this.mem / 1024 / 1024, /*in*/ 20, 600, /*out*/ 3, 30);
 
    this.revs += dt * remap(this.cpu, /*in*/ 0, 100, /*out*/ 4, 120);
 
    const rad = remap(size, /*in*/ 3, 30, /*out*/ 14, 5);
 

	
 
    var x = this.w / 2 + rad * Math.cos(this.revs / 6.28),
 
      y = this.h / 2 + rad * Math.sin(this.revs / 6.28);
 

	
 
    ctx.save();
 
    ctx.beginPath();
 
    ctx.fillStyle = "hsl(194, 100%, 42%)";
 
    ctx.arc(x, y, size, 0, 2 * Math.PI);
 
    ctx.fill();
 
    ctx.restore();
 
  }
 
  updated(changedProperties: Map<string, any>) {
 
    if (changedProperties.has("dataTime")) {
 
      this.shadowRoot!.firstElementChild!.setAttribute("title", `cpu ${this.cpu}% mem ${this.mem}MB`);
 
    }
 
  }
 

	
 
  static styles = [
 
    css`
 
      :host {
 
        display: inline-block;
 
        width: 64px;
 
        height: 64px;
 
      }
 
    `,
 
  ];
package.json
Show inline comments
 
@@ -15,24 +15,25 @@
 
    "bower": "^1.8.4",
 
    "browserify": "^16.2.3",
 
    "chai": "^3.5.0",
 
    "cjs-to-es6": "^1.1.1",
 
    "coffeelint": "^2.1.0",
 
    "coffeescript": "^2.3.0",
 
    "d3": "^5.1.0",
 
    "debug": "^4.3.4",
 
    "esmify": "^2.1.1",
 
    "lit": "^2.2.2",
 
    "mocha": "^2.5.3",
 
    "n3": "^1.0.0-alpha",
 
    "parse-prometheus-text-format": "^1.1.1",
 
    "pixi.js": "^4.7.3",
 
    "significant-rounding": "^2.0.0",
 
    "tinycolor2": "^1.4.1",
 
    "vite": "^2.9.1"
 
  },
 
  "devDependencies": {
 
    "mocha": "^2.5.3"
 
  },
 
  "scripts": {
 
    "test": "mocha"
 
  }
 
}
pnpm-lock.yaml
Show inline comments
 
@@ -7,45 +7,47 @@ specifiers:
 
  bower: ^1.8.4
 
  browserify: ^16.2.3
 
  chai: ^3.5.0
 
  cjs-to-es6: ^1.1.1
 
  coffeelint: ^2.1.0
 
  coffeescript: ^2.3.0
 
  d3: ^5.1.0
 
  debug: ^4.3.4
 
  esmify: ^2.1.1
 
  lit: ^2.2.2
 
  mocha: ^2.5.3
 
  n3: ^1.0.0-alpha
 
  parse-prometheus-text-format: ^1.1.1
 
  pixi.js: ^4.7.3
 
  significant-rounding: ^2.0.0
 
  tinycolor2: ^1.4.1
 
  vite: ^2.9.1
 

	
 
dependencies:
 
  '@types/debug': 4.1.7
 
  '@webcomponents/shadycss': 1.11.0
 
  '@webcomponents/webcomponentsjs': 1.3.3
 
  bower: 1.8.14
 
  browserify: 16.5.2
 
  chai: 3.5.0
 
  cjs-to-es6: 1.1.1
 
  coffeelint: 2.1.0
 
  coffeescript: 2.6.1
 
  d3: 5.16.0
 
  debug: 4.3.4
 
  esmify: 2.1.1
 
  lit: 2.2.2
 
  mocha: 2.5.3
 
  n3: 1.16.0
 
  parse-prometheus-text-format: 1.1.1
 
  pixi.js: 4.8.9
 
  significant-rounding: 2.0.0
 
  tinycolor2: 1.4.2
 
  vite: 2.9.1
 

	
 
packages:
 

	
 
  /5to6-codemod/1.8.0:
 
    resolution: {integrity: sha512-RUHjjwl9+p1d46USvmoKsmMaHODFUAESE1de/q0qQM+hwzgk/HssTwb1Nc5dbUpKEkJ7duLg6ggMIwScd+TRig==}
 
    dependencies:
 
      jscodeshift: 0.6.4
 
      lodash: 4.17.21
 
@@ -4799,24 +4801,30 @@ packages:
 
    dev: false
 

	
 
  /parse-glob/3.0.4:
 
    resolution: {integrity: sha1-ssN2z7EfNVE7rdFz7wu246OIORw=}
 
    engines: {node: '>=0.10.0'}
 
    dependencies:
 
      glob-base: 0.3.0
 
      is-dotfile: 1.0.3
 
      is-extglob: 1.0.0
 
      is-glob: 2.0.1
 
    dev: false
 

	
 
  /parse-prometheus-text-format/1.1.1:
 
    resolution: {integrity: sha512-dBlhYVACjRdSqLMFe4/Q1l/Gd3UmXm8ruvsTi7J6ul3ih45AkzkVpI5XHV4aZ37juGZW5+3dGU5lwk+QLM9XJA==}
 
    dependencies:
 
      shallow-equal: 1.2.1
 
    dev: false
 

	
 
  /parse-uri/1.0.7:
 
    resolution: {integrity: sha512-eWuZCMKNlVkXrEoANdXxbmqhu2SQO9jUMCSpdbJDObin0JxISn6e400EWsSRbr/czdKvWKkhZnMKEGUwf/Plmg==}
 
    engines: {node: '>= 0.10'}
 
    dev: false
 

	
 
  /pascalcase/0.1.1:
 
    resolution: {integrity: sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=}
 
    engines: {node: '>=0.10.0'}
 
    dev: false
 

	
 
  /path-browserify/0.0.1:
 
    resolution: {integrity: sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==}
 
@@ -5334,24 +5342,28 @@ packages:
 
    dependencies:
 
      inherits: 2.0.4
 
      safe-buffer: 5.2.1
 
    dev: false
 

	
 
  /shallow-clone/3.0.1:
 
    resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==}
 
    engines: {node: '>=8'}
 
    dependencies:
 
      kind-of: 6.0.3
 
    dev: false
 

	
 
  /shallow-equal/1.2.1:
 
    resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==}
 
    dev: false
 

	
 
  /shasum-object/1.0.0:
 
    resolution: {integrity: sha512-Iqo5rp/3xVi6M4YheapzZhhGPVs0yZwHj7wvwQ1B9z8H6zk+FEnI7y3Teq7qwnekfEhu8WmG2z0z4iWZaxLWVg==}
 
    dependencies:
 
      fast-safe-stringify: 2.1.1
 
    dev: false
 

	
 
  /shasum/1.0.2:
 
    resolution: {integrity: sha1-5wEjENj0F/TetXEhUOVni4euVl8=}
 
    dependencies:
 
      json-stable-stringify: 0.0.1
 
      sha.js: 2.4.11
 
    dev: false
0 comments (0 inline, 0 general)