diff options
author | Ivan Topolnjak <ivantopo@gmail.com> | 2019-02-04 19:16:07 +0100 |
---|---|---|
committer | Ivan Topolnjak <ivantopo@gmail.com> | 2019-02-04 19:16:07 +0100 |
commit | 11fc4117be9ef93f3471ed22de596a6bd8088eb9 (patch) | |
tree | 1673db751d4a24c048e362dca43facf5e0521709 /kamon-status/src | |
parent | 0a2b7f4bf0dde31c82482fbaf5153c22c84ada69 (diff) | |
download | Kamon-11fc4117be9ef93f3471ed22de596a6bd8088eb9.tar.gz Kamon-11fc4117be9ef93f3471ed22de596a6bd8088eb9.tar.bz2 Kamon-11fc4117be9ef93f3471ed22de596a6bd8088eb9.zip |
update the status page to show modules information and metrics
Diffstat (limited to 'kamon-status/src')
-rw-r--r-- | kamon-status/src/App.vue | 26 | ||||
-rw-r--r-- | kamon-status/src/api/StatusApi.ts | 67 | ||||
-rw-r--r-- | kamon-status/src/components/Card.vue | 19 | ||||
-rw-r--r-- | kamon-status/src/components/HelloWorld.vue | 65 | ||||
-rw-r--r-- | kamon-status/src/components/MetricList.vue | 112 | ||||
-rw-r--r-- | kamon-status/src/components/ModuleStatus.vue | 76 | ||||
-rw-r--r-- | kamon-status/src/components/StatusCard.vue | 79 | ||||
-rw-r--r-- | kamon-status/src/main.ts | 16 | ||||
-rw-r--r-- | kamon-status/src/router.ts | 18 | ||||
-rw-r--r-- | kamon-status/src/styles/main.scss | 8 | ||||
-rw-r--r-- | kamon-status/src/views/About.vue | 5 | ||||
-rw-r--r-- | kamon-status/src/views/Overview.vue | 134 |
12 files changed, 350 insertions, 275 deletions
diff --git a/kamon-status/src/App.vue b/kamon-status/src/App.vue index 88938081..ebb432c9 100644 --- a/kamon-status/src/App.vue +++ b/kamon-status/src/App.vue @@ -1,24 +1,11 @@ <template> <div id="app"> - <div class="header w-100 mb-5 sticky-top"> + <div class="header w-100 mb-1 sticky-top"> <div class="container h-100"> <div class="row h-100 justify-content-between"> <div class="col-auto h-100"> <img class="py-3 h-100 img-fluid" src="./assets/logo.svg" alt=""> </div> - - <div class="col-auto navigation"> - <router-link to="/"> - <div class="navigation-link"> - Overview - </div> - </router-link> - <router-link to="/"> - <div class="navigation-link"> - Metrics - </div> - </router-link> - </div> </div> </div> </div> @@ -53,15 +40,4 @@ $header-height: 70px; } } } - -// #nav { -// padding: 30px; -// a { -// font-weight: bold; -// color: #2c3e50; -// &.router-link-exact-active { -// color: #42b983; -// } -// } -// } </style> diff --git a/kamon-status/src/api/StatusApi.ts b/kamon-status/src/api/StatusApi.ts index 7f18b2ed..d00761e5 100644 --- a/kamon-status/src/api/StatusApi.ts +++ b/kamon-status/src/api/StatusApi.ts @@ -7,47 +7,48 @@ export interface Environment { tags: { [key: string]: string } } -export interface Config { +export interface Settings { version: string environment: Environment config: any } export enum ModuleKind { - Combined = "combined", - Metric = "metric", - Span = "span", - Plain = "plain", - Unknown = "unknown" + Combined = 'combined', + Metric = 'metric', + Span = 'span', + Plain = 'plain', + Unknown = 'unknown' } export interface Module { name: string description: string kind: ModuleKind - enabled: boolean - started: boolean + isProgrammaticallyRegistered: boolean + isStarted: boolean } -export interface MetricInfo { +export interface Metric { name: string type: string tags: { [key: string ]: string } + search: string } -export interface ModuleRegistryStatus { - modules: Array<Module> +export interface ModuleRegistry { + modules: Module[] } -export interface MetricRegistryStatus { - metrics: Array<MetricInfo> +export interface MetricRegistry { + metrics: Metric[] } export class StatusApi { - public static configStatus(): Promise<Config> { - return axios.get("/status/config").then(response => { + public static settings(): Promise<Settings> { + return axios.get('/status/settings').then(response => { const config = JSON.parse(response.data.config) return { version: response.data.version, @@ -57,15 +58,37 @@ export class StatusApi { }) } - public static moduleRegistryStatus(): Promise<ModuleRegistryStatus> { - return axios.get("/status/modules").then(response => { - return response.data as ModuleRegistryStatus + public static moduleRegistryStatus(): Promise<ModuleRegistry> { + return axios.get('/status/modules').then(response => { + return response.data as ModuleRegistry }) } - public static metricRegistryStatus(): Promise<MetricRegistryStatus> { - return axios.get("/status/metrics").then(response => { - return response.data as MetricRegistryStatus + public static metricRegistryStatus(): Promise<MetricRegistry> { + return axios.get('/status/metrics').then(response => { + const metricRegistry = response.data as MetricRegistry + const pair = (key: string, value: string) => key + ':' + value + ' ' + + metricRegistry.metrics.forEach(metric => { + // Fixes the display name for range samplers + if (metric.type === 'RangeSampler') { + metric.type = 'Range Sampler' + } + + + // Calculate the "search" string and inject it in all metrics. + let tagsSearch = '' + Object.keys(metric.tags).forEach(tag => { + tagsSearch += pair(tag, metric.tags[tag]) + }) + + metric.search = + pair('name', metric.name.toLowerCase()) + + pair('type', metric.type.toLowerCase()) + + tagsSearch + }) + + return metricRegistry }) } -}
\ No newline at end of file +} diff --git a/kamon-status/src/components/Card.vue b/kamon-status/src/components/Card.vue new file mode 100644 index 00000000..745a1caf --- /dev/null +++ b/kamon-status/src/components/Card.vue @@ -0,0 +1,19 @@ +<template> + <div class="outer py-1"> + <slot/> + </div> +</template> + + +<!-- Add "scoped" attribute to limit CSS to this component only --> +<style scoped lang="scss"> +.outer { + background-color: white; + box-shadow: 0 2px 9px 1px rgba(0, 0, 0, 0.1); +} + +hr { + margin: 1px; + border-color: #f3f3f3; +} +</style> diff --git a/kamon-status/src/components/HelloWorld.vue b/kamon-status/src/components/HelloWorld.vue deleted file mode 100644 index af340a46..00000000 --- a/kamon-status/src/components/HelloWorld.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> - <div class="hello"> - <h1>{{ msg }}</h1> - <p> - For a guide and recipes on how to configure / customize this project,<br> - check out the - <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>. - </p> - <h3>Installed CLI Plugins</h3> - <ul> - <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li> - </ul> - <h3>Essential Links</h3> - <ul> - <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li> - <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li> - <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li> - <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li> - <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li> - </ul> - <h3>Ecosystem</h3> - <ul> - <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li> - <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li> - <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li> - <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li> - <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li> - </ul> - </div> -</template> - -<script lang="ts"> -import { Component, Prop, Vue } from 'vue-property-decorator' -import axios from 'axios' - -@Component -export default class HelloWorld extends Vue { - @Prop() private msg!: string; - - private mounted() { - console.log("I'm mounting the thing") - axios.get("/status/config").then(result => { - console.log("GOT A RESULT", result.data) - }) - } -} -</script> - -<!-- Add "scoped" attribute to limit CSS to this component only --> -<style scoped lang="scss"> -h3 { - margin: 40px 0 0; -} -ul { - list-style-type: none; - padding: 0; -} -li { - display: inline-block; - margin: 0 10px; -} -a { - color: #42b983; -} -</style> diff --git a/kamon-status/src/components/MetricList.vue b/kamon-status/src/components/MetricList.vue new file mode 100644 index 00000000..758252e7 --- /dev/null +++ b/kamon-status/src/components/MetricList.vue @@ -0,0 +1,112 @@ +<template> + <div class="row no-gutters"> + <div class="col-12"> + <div class="search-box mb-3"> + <input class="w-100 px-3 py-2" v-model="filterPattern" type="text" placeholder="filter"> + <span class="search-stats">{{ searchStats }}</span> + </div> + </div> + + <div class="col-12"> + <card v-if="matchedMetrics.length > 0"> + <div class="row no-gutters" v-for="(metric, index) in matchedMetrics" :key="metric.search"> + <div class="col-12 px-3 pt-1 pb-3"> + <div class="text-uppercase text-label">{{ metric.type }}</div> + <h4>{{ metric.name }}</h4> + <div class="tag-container"> + <span class="tag" v-for="tag in Object.keys(metric.tags)" :key="tag"> + {{ tag }}=<span class="tag-value">{{ metric.tags[tag] }}</span> + </span> + </div> + + </div> + <hr v-if="index < (matchedMetrics.length - 1)" class="w-100"> + </div> + </card> + </div> + </div> +</template> + +<script lang="ts"> +import { Component, Prop, Vue } from 'vue-property-decorator' +import {Metric} from '../api/StatusApi' +import Card from './Card.vue' + + +@Component({ + components: { + card: Card + } +}) +export default class MetricList extends Vue { + @Prop() private metrics!: Metric[] + private filterPattern: string = '' + + get totalMetrics(): number { + return this.metrics.length + } + + get filterRegex(): RegExp { + return new RegExp(this.filterPattern) + } + + get searchStats(): string { + if (this.filterPattern.length > 0) { + return this.matchedMetrics.length + ' matched' + } else { + return this.totalMetrics + ' metrics' + } + } + + get matchedMetrics(): Metric[] { + if (this.filterPattern.length > 0) { + return this.metrics.filter(m => m.search.match(this.filterRegex) != null) + } else { + return this.metrics + } + } +} +</script> + +<style scoped lang="scss"> +.search-box { + input { + color: #676767; + height: 2.5rem; + border: none; + border-radius: 0.3rem; + background-color: #e8e8e8; + + &:focus { + outline: none; + } + } + + ::placeholder { + color: #929292; + } + + .search-stats { + position: absolute; + line-height: 2.5rem; + right: 0; + padding-right: 1rem; + } +} + +.tag-container { + margin: 0rem -0.3rem; +} + +.tag { + background-color: #f4f4f4; + margin: 0.3rem; + padding: 0.1rem 0.5rem; + border-radius: 0.2rem; +} + +.tag-value { + color: #676767; +} + +</style> diff --git a/kamon-status/src/components/ModuleStatus.vue b/kamon-status/src/components/ModuleStatus.vue new file mode 100644 index 00000000..a92cf46f --- /dev/null +++ b/kamon-status/src/components/ModuleStatus.vue @@ -0,0 +1,76 @@ +<template> + <card> + <div class="row"> + <div class="col-12"> + <div class="py-2 px-3 text-uppercase text-label"> + {{ moduleStatus.kind }} + </div> + <hr> + </div> + <div class="col"> + <div class="px-3 py-2"> + <h4>{{ moduleStatus.name }}</h4> + <div class="text-label"> + {{ moduleStatus.description }} + </div> + </div> + </div> + <div class="col-auto"> + <div class="status-indicator text-center" :class="runStatus.class">{{ runStatus.message }}</div> + <div class="status-indicator text-center">{{ discoveryStatus.message }}</div> + </div> + </div> + </card> +</template> + +<script lang="ts"> +import { Component, Prop, Vue } from 'vue-property-decorator' +import {Module} from '../api/StatusApi' +import Card from './Card.vue' + + +@Component({ + components: { + card: Card + } +}) +export default class ModuleStatus extends Vue { + @Prop() private moduleStatus!: Module + + get discoveryStatus(): { message: string, class: string } { + return this.moduleStatus.isProgrammaticallyRegistered ? + { message: 'manual', class: '' } : + { message: 'automatic', class: '' } + } + + get runStatus(): { message: string, class: string } { + return this.moduleStatus.isStarted ? + { message: 'started', class: 'healthy' } : + { message: 'disabled', class: 'critical' } + } +} +</script> + +<style scoped lang="scss"> +.status-indicator { + font-size: 0.9rem; + border-radius: 0.3rem; + color: white; + margin: 0.5rem 1rem; + padding: 0.1rem 1rem; + background-color: #cccccc; +} + +.critical { + background-color: #ff6e6b; +} + +.healthy { + background-color: #6ada87; +} + +hr { + margin: 1px; + border-color: #f3f3f3; +} +</style> diff --git a/kamon-status/src/components/StatusCard.vue b/kamon-status/src/components/StatusCard.vue deleted file mode 100644 index 05047a19..00000000 --- a/kamon-status/src/components/StatusCard.vue +++ /dev/null @@ -1,79 +0,0 @@ -<template> - <div class="outer"> - <div class="py-2 px-3 heading text-uppercase"> - {{ statusInfo.heading }} - </div> - <hr> - <div class="px-3 py-2"> - <div class="message" :class="messageStatusClass"> - {{ statusInfo.message }} - </div> - <div class="caption"> - {{ statusInfo.caption }} - </div> - </div> - </div> -</template> - -<script lang="ts"> -import { Component, Prop, Vue } from 'vue-property-decorator' -import axios from 'axios' - -export enum Status { - Healthy, - Critical, - Unknown -} - -export interface StatusInfo { - heading: string - message: string - caption?: string - status: Status -} - -@Component -export default class StatusCard extends Vue { - @Prop() private statusInfo!: StatusInfo - - get messageStatusClass(): string[] { - if(this.statusInfo != null && this.statusInfo.status != Status.Unknown) { - return this.statusInfo.status == Status.Healthy ? ['healthy'] : ['critical'] - } else { - return [] - } - } -} -</script> - -<!-- Add "scoped" attribute to limit CSS to this component only --> -<style scoped lang="scss"> -.outer { - background-color: white; - box-shadow: 0 2px 9px 1px rgba(0, 0, 0, 0.1); - - .heading { - font-size: 0.9rem; - color: #a5a5a5; - } - - .message { - color: #868686; - font-weight: 600; - font-size: 1.5rem; - } - - .caption { - color: #a5a5a5; - } - - .critical { - color: #ff6e6b; - } -} - -hr { - margin: 1px; - border-color: #f3f3f3; -} -</style> diff --git a/kamon-status/src/main.ts b/kamon-status/src/main.ts index 0be7bece..02a4b217 100644 --- a/kamon-status/src/main.ts +++ b/kamon-status/src/main.ts @@ -1,12 +1,12 @@ -import Vue from 'vue'; -import App from './App.vue'; -import router from './router'; -import 'bootstrap/dist/css/bootstrap.min.css'; -import './styles/main.scss'; +import Vue from 'vue' +import App from './App.vue' +import router from './router' +import 'bootstrap/dist/css/bootstrap.min.css' +import './styles/main.scss' -Vue.config.productionTip = false; +Vue.config.productionTip = false new Vue({ router, - render: (h) => h(App), -}).$mount('#app'); + render: h => h(App), +}).$mount('#app') diff --git a/kamon-status/src/router.ts b/kamon-status/src/router.ts index 03d07eae..1f11ff0a 100644 --- a/kamon-status/src/router.ts +++ b/kamon-status/src/router.ts @@ -1,8 +1,8 @@ -import Vue from 'vue'; -import Router from 'vue-router'; -import Overview from './views/Overview.vue'; +import Vue from 'vue' +import Router from 'vue-router' +import Overview from './views/Overview.vue' -Vue.use(Router); +Vue.use(Router) export default new Router({ routes: [ @@ -11,13 +11,5 @@ export default new Router({ name: 'overview', component: Overview, }, - { - path: '/about', - name: 'about', - // route level code-splitting - // this generates a separate chunk (about.[hash].js) for this route - // which is lazy-loaded when the route is visited. - component: () => import(/* webpackChunkName: "about" */ './views/About.vue'), - }, ], -}); +}) diff --git a/kamon-status/src/styles/main.scss b/kamon-status/src/styles/main.scss index 8483e81b..cde62b37 100644 --- a/kamon-status/src/styles/main.scss +++ b/kamon-status/src/styles/main.scss @@ -1,7 +1,7 @@ body { background-color: #f7f7f7; - font-size: 16px; - color: #616161; + font-size: 14px; + color: #929292; font-family: 'Open Sans', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -9,4 +9,8 @@ body { h1, h2, h3 { font-weight: 300; +} + +.text-label { + color: #b3b3b3; }
\ No newline at end of file diff --git a/kamon-status/src/views/About.vue b/kamon-status/src/views/About.vue deleted file mode 100644 index 3fa28070..00000000 --- a/kamon-status/src/views/About.vue +++ /dev/null @@ -1,5 +0,0 @@ -<template> - <div class="about"> - <h1>This is an about page</h1> - </div> -</template> diff --git a/kamon-status/src/views/Overview.vue b/kamon-status/src/views/Overview.vue index e0802eb7..2d6e8dd7 100644 --- a/kamon-status/src/views/Overview.vue +++ b/kamon-status/src/views/Overview.vue @@ -1,19 +1,48 @@ <template> <div class="container"> <div class="row"> - <div class="col-12 pb-3"> - <h2>Overview</h2> + <div class="col-12 pt-4 pb-2"> + <h3>Status</h3> </div> - <div class="col-6 col-md-4 p-2"> - <status-card :statusInfo="instrumentationStatus"></status-card> + <div class="col-12"> + <card> + <div class="row py-2 no-gutters"> + <div class="col-12 col-md-3 py-2 px-3"> + <div class="text-uppercase text-label pb-2">Instrumentation</div> + <h5>Active</h5> + </div> + <div class="col-12 col-md-3 py-2 px-3"> + <div class="text-uppercase text-label pb-2">Reporters</div> + <h5>{{ activeReporters.length }} Started</h5> + </div> + <div class="col-12 col-md-3 py-2 px-3"> + <div class="text-uppercase text-label pb-2">Metrics</div> + <h5>{{metricsStatusMessage}}</h5> + </div> + </div> + </card> </div> - <div class="col-6 col-md-4 p-2"> - <status-card :statusInfo="reporterStatus"></status-card> + + <div class="col-12 pt-4 pb-2"> + <h3>Reporters</h3> + </div> + <div class="col-12 py-1" v-for="reporter in reporterModules" :key="reporter.name"> + <module-status :moduleStatus="reporter" /> + </div> + + <div class="col-12 pt-4 pb-2" v-if="plainModules.length > 0"> + <h2>Modules</h2> </div> - <div class="col-6 col-md-4 p-2"> - <status-card :statusInfo="metricsStatus"></status-card> + <div class="col-12 py-1" v-for="module in plainModules" :key="module.name"> + <module-status :moduleStatus="module" /> </div> + <div class="col-12 pt-4 pb-2" v-if="metrics.length > 0"> + <h2>Metrics</h2> + </div> + <div class="col-12 mb-5"> + <metric-list :metrics="metrics"/> + </div> </div> </div> </template> @@ -21,66 +50,51 @@ <script lang="ts"> import { Component, Vue } from 'vue-property-decorator' import {Option, none, some} from 'ts-option' -import StatusCard, {StatusInfo, Status} from '../components/StatusCard.vue' -import {StatusApi, Config, ModuleRegistryStatus, ModuleKind, MetricRegistryStatus, Module} from '../api/StatusApi' - -interface ComponentStatus { - -} +import ModuleStatus from '../components/ModuleStatus.vue' +import Card from '../components/Card.vue' +import MetricList from '../components/MetricList.vue' +import {StatusApi, Settings, ModuleRegistry, ModuleKind, MetricRegistry, Module, Metric} from '../api/StatusApi' @Component({ components: { - 'status-card': StatusCard + 'card': Card, + 'module-status': ModuleStatus, + 'metric-list': MetricList }, }) export default class Overview extends Vue { - private config: Option<Config> = none - private moduleRegistry: Option<ModuleRegistryStatus> = none - private metricsRegistry: Option<MetricRegistryStatus> = none - - - get reporterStatus(): StatusInfo { - const status: StatusInfo = { - heading: 'Reporters', - message: 'Unknown', - status: Status.Unknown - } - - const isReporter = function(module: Module): boolean { - return [ModuleKind.Combined, ModuleKind.Span, ModuleKind.Metric].indexOf(module.kind) > 0 - } - - this.moduleRegistry.forEach(moduleRegistry => { - const reportingModules = moduleRegistry.modules.filter(isReporter) - if(reportingModules.length > 0) { - status.status = Status.Healthy - status.message = reportingModules.length + " Active" - } - }) + private settings: Option<Settings> = none + private moduleRegistry: Option<ModuleRegistry> = none + private metricsRegistry: Option<MetricRegistry> = none + + get reporterModules(): Module[] { + return this.moduleRegistry + .map(moduleRegistry => moduleRegistry.modules.filter(this.isReporter)) + .getOrElse([]) + } - return status + get activeReporters(): Module[] { + return this.reporterModules.filter(this.isStarted) } - get metricsStatus(): StatusInfo { - const status: StatusInfo = { - heading: 'Metrics', - message: 'Unknown', - status: Status.Unknown - } + get plainModules(): Module[] { + return this.moduleRegistry + .map(moduleRegistry => moduleRegistry.modules.filter(m => !this.isReporter(m))) + .getOrElse([]) + } - this.metricsRegistry.forEach(metricRegistry => { - status.message = metricRegistry.metrics.length + " Metrics" - }) + get trackedMetrics(): Option<number> { + return this.metricsRegistry.map(metricRegistry => metricRegistry.metrics.length) + } - return status + get metricsStatusMessage(): string { + return this.trackedMetrics.map(mc => mc + ' Tracked').getOrElse('Unknown') } - get instrumentationStatus(): StatusInfo { - return { - heading: 'Instrumentation', - message: 'Unknown', - status: Status.Unknown - } + get metrics(): Metric[] { + return this.metricsRegistry + .map(mr => mr.metrics) + .getOrElse([]) } @@ -89,9 +103,17 @@ export default class Overview extends Vue { } private refreshData(): void { - StatusApi.configStatus().then(config => { this.config = some(config) }) + StatusApi.settings().then(settings => { this.settings = some(settings) }) StatusApi.metricRegistryStatus().then(metricsRegistry => { this.metricsRegistry = some(metricsRegistry) }) StatusApi.moduleRegistryStatus().then(moduleRegistry => {this.moduleRegistry = some(moduleRegistry) }) } + + private isReporter(module: Module): boolean { + return [ModuleKind.Combined, ModuleKind.Span, ModuleKind.Metric].indexOf(module.kind) > 0 + } + + private isStarted(module: Module): boolean { + return module.isStarted + } } </script> |