diff options
Diffstat (limited to 'kamon-status-page/src/main/vue/src')
20 files changed, 1149 insertions, 0 deletions
diff --git a/kamon-status-page/src/main/vue/src/App.vue b/kamon-status-page/src/main/vue/src/App.vue new file mode 100644 index 00000000..fd612828 --- /dev/null +++ b/kamon-status-page/src/main/vue/src/App.vue @@ -0,0 +1,47 @@ +<template> + <div id="app"> + <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="logo h-100 img-fluid" src="./assets/logo.svg" alt=""> + </div> + </div> + </div> + </div> + + <router-view/> + </div> + +</template> + +<style lang="scss"> + +$header-height: 85px; + +.header { + height: $header-height; + background-color: white; + box-shadow: 0 5px 9px 1px rgba(0,0,0,0.1); + + .navigation { + line-height: $header-height; + + .navigation-link, a { + display: inline-block; + padding: 0 0.5rem; + text-transform: uppercase; + text-decoration: none; + color: #b3b3b3; + + &:hover { + color: #888888; + } + } + } + + .logo { + padding: 1rem 0rem; + } +} +</style> diff --git a/kamon-status-page/src/main/vue/src/api/StatusApi.ts b/kamon-status-page/src/main/vue/src/api/StatusApi.ts new file mode 100644 index 00000000..ee596b8d --- /dev/null +++ b/kamon-status-page/src/main/vue/src/api/StatusApi.ts @@ -0,0 +1,137 @@ +import axios, { AxiosResponse } from 'axios' + +export interface Environment { + service: string + host: string + instance: string + tags: { [key: string]: string } +} + +export interface Settings { + version: string + environment: Environment + config: any +} + +export enum ModuleKind { + Combined = 'combined', + Metric = 'metric', + Span = 'span', + Plain = 'plain', + Unknown = 'unknown' +} + +export interface Module { + name: string + description: string + clazz: string + kind: ModuleKind + programmaticallyRegistered: boolean + enabled: boolean + started: boolean +} + +export interface Metric { + name: string + type: string + unitDimension: string + unitMagnitude: string + tags: { [key: string ]: string } + search: string +} + +export interface ModuleRegistry { + modules: Module[] +} + +export interface MetricRegistry { + metrics: Metric[] +} + +export interface InstrumentationModule { + name: string + description: string + enabled: boolean + active: boolean +} + +export interface Instrumentation { + active: boolean + modules: InstrumentationModule[] + errors: { [key: string]: string[]} +} + + +export class StatusApi { + + public static settings(): Promise<Settings> { + return axios.get('/status/settings').then(response => { + const config = JSON.parse(response.data.config) + return { + version: response.data.version, + environment: response.data.environment, + config + } + }) + } + + public static moduleRegistryStatus(): Promise<ModuleRegistry> { + return axios.get('/status/modules').then(response => { + return response.data as ModuleRegistry + }) + } + + 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 + }) + } + + public static instrumentationStatus(): Promise<Instrumentation> { + return axios.get('/status/instrumentation').then(response => { + const instrumentation: Instrumentation = { + active: response.data.active as boolean, + modules: [], + errors: {} + } + + const rawModules = response.data.modules + Object.keys(rawModules).forEach(key => { + const rawModule = JSON.parse(rawModules[key]) + instrumentation.modules.push({ + name: key, + ...rawModule + }) + }) + + const rawErrors = response.data.errors + Object.keys(rawErrors).forEach(key => { + instrumentation.errors[key] = JSON.parse(rawErrors[key]) + }) + + return instrumentation + }) + } +} diff --git a/kamon-status-page/src/main/vue/src/assets/logo.svg b/kamon-status-page/src/main/vue/src/assets/logo.svg new file mode 100644 index 00000000..d351c48a --- /dev/null +++ b/kamon-status-page/src/main/vue/src/assets/logo.svg @@ -0,0 +1,26 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="Layer_1" width="1529.7" height="283.96"> + <g id="XMLID_903_-5" fill="#616161" transform="matrix(.99987 0 0 .99987 .19 0)"> + <path id="XMLID_914_-7" d="M388.3 82.3v62.3l34.9-40h34l-38.9 41.7 39.3 66.7h-34l-20-37c-3.6-6.4-7.2-9.6-10.8-9.6-3 .6-4.5 2.6-4.5 5.5v41H360V68.4h14.7c7.5 0 13.6 6.4 13.6 13.9z" class="st0"/> + <path id="XMLID_911_-4" d="M492.9 129.9h-28.3c3.4-20.8 18.1-31 44-31 31 0 46.8 10.2 47.4 31v38.3c0 31-18.7 44.8-49.1 47-27 2.1-45.7-10.4-45.7-35.3.6-27 20.2-34.2 48.7-37 12.1-1.5 18.3-5.1 18.3-11.3-.6-6.4-6.6-9.6-18.3-9.6-10 0-15.5 2.5-17 7.9zm35.7 36.9v-10.4c-6.6 2.8-14.2 5.1-22.5 6.8-11.3 2.1-17 7.4-17 15.7.6 8.9 5.3 13.2 14.2 13.2 15.7 0 25.3-9.3 25.3-25.3z" class="st0"/> + <path id="XMLID_909_-1" d="M598.3 141.1V213h-28.7v-66.1c0-32.1 16.2-48 48.2-48 14.5 0 25.5 3.2 33.6 9.6 8.1-6.2 19.1-9.1 33.6-9.1 32.1 0 48 15.9 47.8 48v66.1h-14.7c-9.3-.6-14-5.1-14-14v-57.8c-.6-11.7-7-17.4-19.6-17.4-12.5 0-18.9 5.7-19.1 17.4V213H637v-71.8c-.6-11.7-7-17.4-19.6-17.4-12.6 0-18.9 5.6-19.1 17.3z" class="st0"/> + <path id="XMLID_906_-8" d="M850.6 157.3c0 38.7-17.2 58.2-51.2 58.2s-51-19.6-51-58.2c0-39.1 17-58.4 51-58.4s51.2 19.3 51.2 58.4zm-73.6 0c.2 21.9 7.6 32.9 22.1 32.9 14.5 0 21.7-11.5 21.9-33.8 0-21.9-7.2-32.7-21.7-32.7-14.8 0-22.3 11.3-22.3 33.6z" class="st0"/> + <path id="XMLID_904_-5" d="M945.5 213c-9.3-.6-14-5.1-14-14v-57.8c-.6-11.7-7-17.4-19.6-17.4-12.6 0-18.9 5.7-19.1 17.4V213h-28.7v-66.1c0-32.1 16.2-48 48.2-48 32.1 0 47.8 15.9 47.6 48V213z" class="st0"/> + </g> + <g id="XMLID_899_-9" transform="matrix(.99987 0 0 .99987 .19 0)"> + <path id="XMLID_902_-7" fill="#dadada" d="M153.7 244l36.5 38c1.2 1.2 3.1 2 5 2h89.5c5.2 0 8.3-4.7 5.2-8.1l-83-102.5c-1.8-2-5.2-1.9-6.9.1L153.4 238c-1.6 1.8-1.4 4.2.3 6z"/> + <linearGradient id="linearGradient936" x1="63.89" x2="63.89" y1="198.13" y2="-214.95" gradientUnits="userSpaceOnUse" xlink:href="#XMLID_15_-2"> + <stop id="stop1003-5" offset="0" stop-color="#145643"/> + <stop id="stop1005-3" offset="1" stop-color="#14c441"/> + </linearGradient> + <path id="XMLID_901_-8" fill="#199053" d="M77 12.8C69.8 4.7 60.2 0 50.1 0 32.8 0 17.5 13.6 12.7 33.4 5.9 62 1.8 91.2.5 120.6l9.8 13.4c17.5 24.1 47.9 25.2 66.7 4.5 2.8-2.3 5.4-4.9 7.8-7.8l42.6-51.5c-.1 0-47.9-64.2-50.4-66.4z"/> + <path id="XMLID_900_-8" fill="#34cc5b" d="M284.6 0h-73.3c-12.9 0-24.8 5.9-31.7 15.7l-52.4 63.5-42.6 51.5c-2.4 2.9-5 5.4-7.8 7.8-18.8 20.7-49.2 19.6-66.7-4.5L.3 120.6c-1.4 30.8.2 61.7 4.9 92.2v.2c.4 2.3.7 4.7 1.1 7 .1.4.1.8.2 1.2a130.15 130.15 0 0 0 1.4 7.6l1.2 5.9.3 1.4c.5 2.2.9 4.4 1.4 6.6 0 .2.1.4.1.5 4.9 22.3 20.9 38.4 39.9 40.5.2 0 .3.1.5.1 1.3.1 2.5.2 3.8.2h.1c32.3 0 50.7-18.2 59.9-30.8L291.7 11.8c3.7-5.1-.3-11.8-7.1-11.8z"/> + <g aria-label="STATUS" style="line-height:125%;-inkscape-font-specification:'Montserrat, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start" id="text950" fill="#b3b3b3" stroke-width="3.12" font-family="Montserrat" font-size="118.63" font-weight="400" letter-spacing="0" word-spacing="0"> + <path d="M1039.3 138.08q-6.37 0-10.5 2.62-4.12 2.62-4.12 8 0 5.25 4.12 8.12 4.13 2.75 17.5 6 13.49 3.24 20.24 9.12 6.87 5.87 6.87 17.36 0 11.37-8.62 18.5-8.62 7.12-22.62 7.12-20.49 0-36.36-14.12l9.25-11.12q13.24 11.5 27.49 11.5 7.12 0 11.24-3 4.25-3.13 4.25-8.13 0-5.12-4-7.87-3.87-2.87-13.5-5.12-9.61-2.37-14.61-4.25-5-2-8.87-5.12-7.75-5.87-7.75-18 0-12.11 8.75-18.6 8.87-6.63 21.86-6.63 8.37 0 16.62 2.75 8.25 2.75 14.24 7.74l-7.87 11.12q-3.87-3.5-10.5-5.74-6.61-2.25-13.11-2.25z" style="-inkscape-font-specification:'Montserrat, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start" id="path952"/> + <path d="M1119.42 140.08v73.84h-14.74v-73.84h-26.5v-13.5h67.73v13.5z" style="-inkscape-font-specification:'Montserrat, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start" id="path954"/> + <path d="M1160.15 194.05l-8.74 19.87h-15.75l38.48-87.34h15.75l38.48 87.34h-15.74l-8.75-19.87zm37.73-13.62l-15.86-35.98-15.87 35.98z" style="-inkscape-font-specification:'Montserrat, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start" id="path956"/> + <path d="M1258.58 140.08v73.84h-14.74v-73.84h-26.5v-13.5h67.73v13.5z" style="-inkscape-font-specification:'Montserrat, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start" id="path958"/> + <path d="M1315.3 193.93q5.99 7 16.24 7 10.24 0 16.24-7 6-7 6-19v-48.35h14.74v48.98q0 18.87-10.37 29.11-10.37 10.12-26.61 10.12-16.25 0-26.62-10.12-10.37-10.24-10.37-29.11v-48.98h14.75v48.36q0 11.99 6 18.99z" style="-inkscape-font-specification:'Montserrat, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start" id="path960"/> + <path d="M1407.73 138.08q-6.37 0-10.5 2.62-4.12 2.62-4.12 8 0 5.25 4.12 8.12 4.13 2.75 17.5 6 13.49 3.24 20.24 9.12 6.87 5.87 6.87 17.36 0 11.37-8.62 18.5-8.62 7.12-22.62 7.12-20.49 0-36.36-14.12l9.25-11.12q13.24 11.5 27.49 11.5 7.12 0 11.24-3 4.25-3.13 4.25-8.13 0-5.12-4-7.87-3.87-2.87-13.5-5.12-9.61-2.37-14.61-4.25-5-2-8.87-5.12-7.75-5.87-7.75-18 0-12.11 8.75-18.6 8.87-6.63 21.86-6.63 8.37 0 16.62 2.75 8.25 2.75 14.24 7.74l-7.87 11.12q-3.87-3.5-10.5-5.74-6.61-2.25-13.11-2.25z" style="-inkscape-font-specification:'Montserrat, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start" id="path962"/> + </g> + </g> +</svg>
\ No newline at end of file diff --git a/kamon-status-page/src/main/vue/src/components/Card.vue b/kamon-status-page/src/main/vue/src/components/Card.vue new file mode 100644 index 00000000..d3300336 --- /dev/null +++ b/kamon-status-page/src/main/vue/src/components/Card.vue @@ -0,0 +1,20 @@ +<template> + <div class="card-wrapper"> + <slot/> + </div> +</template> + +<script lang="ts"> +import { Component, Vue } from 'vue-property-decorator' + +@Component +export default class Card extends Vue {} +</script> + + +<style lang="scss"> +.card-wrapper { + background-color: white; + box-shadow: 0 2px 9px 1px rgba(0, 0, 0, 0.1); +} +</style> diff --git a/kamon-status-page/src/main/vue/src/components/EnvironmentCard.vue b/kamon-status-page/src/main/vue/src/components/EnvironmentCard.vue new file mode 100644 index 00000000..98dc3f9f --- /dev/null +++ b/kamon-status-page/src/main/vue/src/components/EnvironmentCard.vue @@ -0,0 +1,66 @@ +<template> + <status-section title="Environment"> + <card> + <div class="row py-2 no-gutters"> + <div class="col-auto py-2 px-3"> + <div class="text-uppercase text-label pb-1">Service</div> + <h6>{{ service }}</h6> + </div> + <div class="col-auto py-2 px-3"> + <div class="text-uppercase text-label pb-1">Host</div> + <h6>{{ host }}</h6> + </div> + <div class="col-auto py-2 px-3"> + <div class="text-uppercase text-label pb-1">instance</div> + <h6>{{instance}}</h6> + </div> + <div class="col-12 col-md-3 py-2 px-3"> + <div class="text-uppercase text-label pb-1">tags</div> + <div class="tag-container" v-if="Object.keys(environmentTags).length > 0"> + <span class="tag" v-for="tag in Object.keys(environmentTags)" :key="tag"> + {{ tag }}=<span class="tag-value">{{ environmentTags[tag] }}</span> + </span> + </div> + <div v-else> + <h6>None</h6> + </div> + </div> + </div> + </card> + </status-section> +</template> + +<script lang="ts"> +import { Component, Prop, Vue } from 'vue-property-decorator' +import {Environment} from '../api/StatusApi' +import Card from './Card.vue' +import StatusSection from '../components/StatusSection.vue' +import {Option, none} from 'ts-option' + + +@Component({ + components: { + 'card': Card, + 'status-section': StatusSection + } +}) +export default class EnvironmentCard extends Vue { + @Prop() private environment: Option<Environment> = none + + get instance(): string { + return this.environment.map(e => e.instance).getOrElse('Unknown') + } + + get host(): string { + return this.environment.map(e => e.host).getOrElse('Unknown') + } + + get service(): string { + return this.environment.map(e => e.service).getOrElse('Unknown') + } + + get environmentTags(): { [key: string]: string } { + return this.environment.map(e => e.tags).getOrElse({}) + } +} +</script>
\ No newline at end of file diff --git a/kamon-status-page/src/main/vue/src/components/InstrumentationModuleList.vue b/kamon-status-page/src/main/vue/src/components/InstrumentationModuleList.vue new file mode 100644 index 00000000..224f6716 --- /dev/null +++ b/kamon-status-page/src/main/vue/src/components/InstrumentationModuleList.vue @@ -0,0 +1,44 @@ +<template> + <div class="row"> + <div class="col-12 pt-4 pb-2" v-if="modules.length > 0"> + <h2>Instrumentation Modules</h2> + </div> + <div class="col-12 py-1" v-for="module in sortedModules" :key="module.name"> + <instrumentation-module-status-card :module="module"/> + </div> + </div> +</template> + +<script lang="ts"> +import { Component, Prop, Vue } from 'vue-property-decorator' +import {InstrumentationModule} from '../api/StatusApi' +import InstrumentationModuleStatusCard from './InstrumentationModuleStatusCard.vue' + + +@Component({ + components: { + 'instrumentation-module-status-card': InstrumentationModuleStatusCard + } +}) +export default class ModuleList extends Vue { + @Prop() private modules!: InstrumentationModule[] + + get sortedModules(): InstrumentationModule[] { + return this.modules.sort((left, right) => { + if (left.active === right.active) { + return left.name.localeCompare(right.name) + } else { + return left.active ? -1 : 1 + } + }) + } +} +</script> + +<style lang="scss"> +.apm-suggestion { + .kind-label { + background-color: #d0f3f0; + } +} +</style> diff --git a/kamon-status-page/src/main/vue/src/components/InstrumentationModuleStatusCard.vue b/kamon-status-page/src/main/vue/src/components/InstrumentationModuleStatusCard.vue new file mode 100644 index 00000000..ed74143a --- /dev/null +++ b/kamon-status-page/src/main/vue/src/components/InstrumentationModuleStatusCard.vue @@ -0,0 +1,40 @@ +<template> + <status-card :indicator-text="runStatus.message" :indicator-icon="runStatus.icon" :indicator-background-color="runStatus.color"> + <div slot="default" class="py-3 pl-4"> + <h5 class="mb-0">{{ module.name }}</h5> + <div class="text-label"> + {{ module.description }} + </div> + </div> + </status-card> +</template> + +<script lang="ts"> +import { Component, Prop, Vue } from 'vue-property-decorator' +import {InstrumentationModule} from '../api/StatusApi' +import StatusCard from './StatusCard.vue' + + +@Component({ + components: { + 'status-card': StatusCard + } +}) +export default class InstrumentationModuleStatusCard extends Vue { + @Prop() private module!: InstrumentationModule + + get runStatus(): { message: string, color: string, icon: string } { + if (!this.module.enabled) { + return { message: 'disabled', color: '#ff9898', icon: 'fa-stop-circle' } + } else { + return this.module.active ? + { message: 'active', color: '#7ade94', icon: 'fa-check' } : + { message: 'available', color: '#bbbbbb', icon: 'fa-stop-circle' } + } + } +} +</script> + +<style lang="scss"> + +</style> diff --git a/kamon-status-page/src/main/vue/src/components/MetricList.vue b/kamon-status-page/src/main/vue/src/components/MetricList.vue new file mode 100644 index 00000000..f0f4637b --- /dev/null +++ b/kamon-status-page/src/main/vue/src/components/MetricList.vue @@ -0,0 +1,129 @@ +<template> + <div class="row no-gutters"> + <div class="col-12"> + <div class="search-box mb-3"> + <span class="search-icon"><i class="fas fa-search fa-fw fa-flip-horizontal"></i></span> + <input class="w-100" v-model="filterPattern" type="text"> + <span class="search-stats">{{ searchStats }}</span> + </div> + </div> + + <div class="col-12" v-if="matchedMetrics.length > 0"> + <div class="row no-gutters" v-for="(group, index) in groups" :key="group.name"> + <div class="col-12"> + <metric-list-item :group="group"/> + </div> + <hr v-if="index < (groups.length - 1)" class="w-100"> + </div> + </div> + </div> +</template> + +<script lang="ts"> +import { Component, Prop, Vue } from 'vue-property-decorator' +import {Metric} from '../api/StatusApi' +import Card from './Card.vue' +import MetricListItem, {MetricGroup} from './MetricListItem.vue' +import StatusCard from './StatusCard.vue' +import _ from 'underscore' + +@Component({ + components: { + 'card': Card, + 'status-card': StatusCard, + 'metric-list-item': MetricListItem + } +}) +export default class MetricList extends Vue { + @Prop( { default: [] }) private metrics!: Metric[] + private filterPattern: string = '' + + get totalMetrics(): number { + return this.metrics.length + } + + get groups(): MetricGroup[] { + const gropedByName = _.groupBy(this.matchedMetrics, m => m.name) + const metricGroups: MetricGroup[] = [] + + Object.keys(gropedByName).forEach(metricName => { + const metrics = gropedByName[metricName] + + // All metrics with the same name must have the same unit (constrained in Kamon) so + // we can safely assume the first unit is the same for all. + metricGroups.push({ + name: metricName, + type: metrics[0].type, + unitDimension: metrics[0].unitDimension, + unitMagnitude: metrics[0].unitMagnitude, + metrics + }) + }) + + return _.sortBy(metricGroups, mg => mg.metrics.length).reverse() + } + + get filterRegex(): RegExp { + return new RegExp(this.filterPattern) + } + + get searchStats(): string { + if (this.filterPattern.length > 0) { + return 'showing ' + this.matchedMetrics.length + ' out of ' + this.totalMetrics + ' series' + } else { + return this.totalMetrics + ' series' + } + } + + 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 lang="scss"> +.search-box { + input { + color: #676767; + caret-color: #676767; + height: 3rem; + border: none; + border-radius: 0.4rem; + background-color: #efefef; + padding-left: 3.5rem; + font-size: 1.1rem; + box-shadow: 0 2px 4px 1px rgba(0, 0, 0, 0.1); + + + &:focus { + outline: none; + } + } + + ::placeholder { + color: #929292; + } + + .search-icon { + color: #c0c0c0; + line-height: 3rem; + font-size: 1.4rem; + position: absolute; + left: 1rem; + } + + .search-stats { + color: #a2a2a2; + font-size: 1.1rem; + position: absolute; + line-height: 3rem; + right: 0; + padding-right: 1rem; + } +} + +</style> diff --git a/kamon-status-page/src/main/vue/src/components/MetricListItem.vue b/kamon-status-page/src/main/vue/src/components/MetricListItem.vue new file mode 100644 index 00000000..1414bbaf --- /dev/null +++ b/kamon-status-page/src/main/vue/src/components/MetricListItem.vue @@ -0,0 +1,108 @@ +<template> + <status-card indicator-background-color="#7ade94" class="metric-list-item c-pointer my-1" :class="{ 'my-4': expanded }"> + + <div slot="status-indicator" @click="onCardClick"> + <div class="metric-count"> + {{ group.metrics.length }} + </div> + <div>SERIES</div> + </div> + + <div slot="default" @click="onCardClick"> + <div class="row no-gutters"> + <div class="col"> + <div class="py-3 pl-4"> + <h5>{{ group.name }}</h5> + <div class="text-label"> + <span>{{group.type}}</span> + <span v-if="group.unitDimension !== 'none'"> - {{ group.unitDimension }}</span> + <span v-if="group.unitMagnitude !== 'none'"> - {{ group.unitMagnitude }}</span> + </div> + </div> + </div> + <div class="col-auto expansion-icon px-5"> + <i class="fas fa-fw" :class="expansionIcon"></i> + </div> + <div class="col-12 series-container" v-if="expanded"> + <div v-for="(metric, index) in group.metrics" :key="index"> + <div class="p-3"> + <h6>Incarnation #{{ index + 1 }}</h6> + <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> + <span v-if="Object.keys(metric.tags).length === 0" class="pl-2">Base Metric - No Tags</span> + </div> + </div> + <hr v-if="index < (group.metrics.length - 1)" class="w-100 incarnation-hr"> + </div> + </div> + </div> + </div> + + + </status-card> +</template> + +<script lang="ts"> +import { Component, Prop, Vue } from 'vue-property-decorator' +import {Metric} from '../api/StatusApi' +import Card from './Card.vue' +import StatusCard from './StatusCard.vue' +import _ from 'underscore' + +export interface MetricGroup { + name: string + type: string + unitDimension: string + unitMagnitude: string + metrics: Metric[] +} + +@Component({ + components: { + 'status-card': StatusCard + } +}) +export default class MetricListItem extends Vue { + @Prop( { default: [] }) private group!: MetricGroup + private expanded: boolean = false + + get expansionIcon(): string { + return this.expanded ? 'fa-angle-up' : 'fa-angle-down' + } + + private onCardClick() { + this.expanded = !this.expanded + } +} +</script> + +<style lang="scss"> + +.metric-count { + font-size: 2rem; + font-weight: 700; + line-height: 4rem; +} + +.expansion-icon { + line-height: 6rem; + color: #d0d0d0; + font-size: 2rem; +} + +.series-container { + background-color: #f7f7f7; +} + +.incarnation-hr { + border-color: #e2e2e2; +} + +.metric-list-item { + .tag { + background-color: #e6e6e6; + } +} +</style> diff --git a/kamon-status-page/src/main/vue/src/components/ModuleList.vue b/kamon-status-page/src/main/vue/src/components/ModuleList.vue new file mode 100644 index 00000000..ac1e7963 --- /dev/null +++ b/kamon-status-page/src/main/vue/src/components/ModuleList.vue @@ -0,0 +1,86 @@ +<template> + <div class="w-100"> + <status-section title="Reporters"> + <div class="row"> + <div class="col-12 py-1" v-for="reporter in reporterModules" :key="reporter.name"> + <module-status-card :module="reporter" /> + </div> + <div v-if="!hasApmModule" class="col-12 py-1 apm-suggestion"> + <a href="https://kamon.io/apm/?utm_source=kamon&utm_medium=status-page&utm_campaign=kamon-status" target="_blank"> + <module-status-card :is-suggestion="true" :module="apmModuleSuggestion" /> + </a> + </div> + </div> + </status-section> + + <status-section title="Modules" v-if="plainModules.length > 0"> + <div class="row"> + <div class="col-12 py-1" v-for="module in plainModules" :key="module.name"> + <module-status-card :module="module"/> + </div> + </div> + </status-section> + </div> +</template> + +<script lang="ts"> +import { Component, Prop, Vue } from 'vue-property-decorator' +import {Module, ModuleKind} from '../api/StatusApi' +import ModuleStatusCard from './ModuleStatusCard.vue' +import StatusSection from './StatusSection.vue' + + +@Component({ + components: { + 'status-section': StatusSection, + 'module-status-card': ModuleStatusCard + } +}) +export default class ModuleList extends Vue { + @Prop() private modules!: Module[] + private apmModuleSuggestion: Module = { + name: 'Kamon APM', + description: 'See your metrics and trace data for free with a Starter account.', + kind: ModuleKind.Combined, + programmaticallyRegistered: false, + enabled: false, + started: false, + clazz: '' + } + + get sortedModules(): Module[] { + return this.modules.sort((left, right) => { + if (left.started === right.started) { + return left.name.localeCompare(right.name) + } else { + return left.started ? -1 : 1 + } + }) + } + + + get reporterModules(): Module[] { + return this.sortedModules.filter(this.isReporter) + } + + get plainModules(): Module[] { + return this.sortedModules.filter(m => !this.isReporter(m)) + } + + get hasApmModule(): boolean { + const knownApmClasses = [ + 'kamon.kamino.KaminoReporter' + ] + + return this.modules.find(m => knownApmClasses.indexOf(m.clazz) > 0) !== undefined + } + + private isReporter(module: Module): boolean { + return [ModuleKind.Combined, ModuleKind.Span, ModuleKind.Metric].indexOf(module.kind) > 0 + } + + private isStarted(module: Module): boolean { + return module.started + } +} +</script>
\ No newline at end of file diff --git a/kamon-status-page/src/main/vue/src/components/ModuleStatusCard.vue b/kamon-status-page/src/main/vue/src/components/ModuleStatusCard.vue new file mode 100644 index 00000000..18e2b038 --- /dev/null +++ b/kamon-status-page/src/main/vue/src/components/ModuleStatusCard.vue @@ -0,0 +1,52 @@ +<template> + <status-card :indicator-text="runStatus.message" :indicator-icon="runStatus.icon" :indicator-background-color="runStatus.color"> + <div slot="default" class="py-3 pl-4"> + <h5 class="mb-0 mr-3 d-inline-block">{{ module.name }}</h5> + <div class="tag-container d-inline-block" v-if="!isSuggestion"> + <span class="tag">{{ module.kind }}</span> + <span class="tag">{{ discoveryStatus }}</span> + </div> + + <div class="text-label"> + {{ module.description }} + </div> + </div> + </status-card> +</template> + +<script lang="ts"> +import { Component, Prop, Vue } from 'vue-property-decorator' +import {Module} from '../api/StatusApi' +import StatusCard from './StatusCard.vue' + + +@Component({ + components: { + 'status-card': StatusCard + } +}) +export default class ModuleStatusCard extends Vue { + @Prop({ default: false }) private isSuggestion!: boolean + @Prop() private module!: Module + + get discoveryStatus(): string { + return this.module.programmaticallyRegistered ? 'manual' : 'automatic' + } + + get runStatus(): { message: string, color: string, icon: string } { + if (this.isSuggestion) { + return { message: 'suggested', color: '#5fd7cc', icon: 'fa-plug' } + } else if (!this.module.enabled) { + return { message: 'disabled', color: '#ff9898', icon: 'fa-stop-circle' } + } else { + return this.module.started ? + { message: 'active', color: '#7ade94', icon: 'fa-check' } : + { message: 'available', color: '#bbbbbb', icon: 'fa-check' } + } + } +} +</script> + +<style lang="scss"> + +</style> diff --git a/kamon-status-page/src/main/vue/src/components/OverviewCard.vue b/kamon-status-page/src/main/vue/src/components/OverviewCard.vue new file mode 100644 index 00000000..6746d761 --- /dev/null +++ b/kamon-status-page/src/main/vue/src/components/OverviewCard.vue @@ -0,0 +1,71 @@ +<template> + <status-section title="Overview"> + <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-1">Instrumentation</div> + <h5>{{instrumentationStatusMessage}}</h5> + </div> + <div class="col-12 col-md-3 py-2 px-3"> + <div class="text-uppercase text-label pb-1">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-1">Metrics</div> + <h5>{{metricsStatusMessage}}</h5> + </div> + </div> + </card> + </status-section> +</template> + + +<script lang="ts"> +import { Component, Vue, Prop } from 'vue-property-decorator' +import {Option, none, some} from 'ts-option' +import Card from '../components/Card.vue' +import StatusSection from '../components/StatusSection.vue' +import {StatusApi, ModuleRegistry, ModuleKind, MetricRegistry, Module, Metric, Instrumentation} from '../api/StatusApi' + +@Component({ + components: { + 'card': Card, + 'status-section': StatusSection + }, +}) +export default class OverviewCard extends Vue { + @Prop() private moduleRegistry: Option<ModuleRegistry> = none + @Prop() private metricRegistry: Option<MetricRegistry> = none + @Prop() private instrumentation: Option<Instrumentation> = none + + get reporterModules(): Module[] { + return this.moduleRegistry + .map(moduleRegistry => moduleRegistry.modules.filter(this.isReporter)) + .getOrElse([]) + } + + get activeReporters(): Module[] { + return this.reporterModules.filter(this.isStarted) + } + + get trackedMetrics(): Option<number> { + return this.metricRegistry.map(metricRegistry => metricRegistry.metrics.length) + } + + get instrumentationStatusMessage(): string { + return this.instrumentation.map(i => (i.active ? 'Active' : 'Disabled') as string).getOrElse('Unknown') + } + + get metricsStatusMessage(): string { + return this.trackedMetrics.map(mc => mc + ' Series').getOrElse('Unknown') + } + + private isReporter(module: Module): boolean { + return [ModuleKind.Combined, ModuleKind.Span, ModuleKind.Metric].indexOf(module.kind) > 0 + } + + private isStarted(module: Module): boolean { + return module.started + } +} +</script> diff --git a/kamon-status-page/src/main/vue/src/components/StatusCard.vue b/kamon-status-page/src/main/vue/src/components/StatusCard.vue new file mode 100644 index 00000000..c402842f --- /dev/null +++ b/kamon-status-page/src/main/vue/src/components/StatusCard.vue @@ -0,0 +1,85 @@ +<template> + <card> + <div class="row status-card no-gutters"> + <div class="col-auto"> + <div class="status-indicator-wrap text-center text-uppercase" :style="indicatorStyle"> + <slot name="status-indicator"> + <div class="status-indicator h-100 pt-3"> + <i class="fas fa-fw" :class="indicatorIcon"></i> + <div> + {{ indicatorText }} + </div> + </div> + </slot> + </div> + </div> + <div class="col"> + <slot name="default"> + + </slot> + </div> + </div> + </card> +</template> + +<script lang="ts"> +import { Component, Prop, Vue } from 'vue-property-decorator' +import Card from './Card.vue' + +@Component({ + components: { + card: Card + } +}) +export default class StatusCard extends Vue { + @Prop({ default: 'white' }) private indicatorColor!: string + @Prop({ default: '#989898' }) private indicatorBackgroundColor!: string + @Prop({ default: 'fa-question' }) private indicatorIcon!: string + @Prop({ default: 'Unknown' }) private indicatorText!: string + + get indicatorStyle() { + return { + color: this.indicatorColor, + backgroundColor: this.indicatorBackgroundColor + } + } +} + +</script> + +<style lang="scss"> + +$indicator-size: 6rem; +.status-card { + min-height: $indicator-size; + + .status-indicator-wrap { + height: 100%; + min-width: $indicator-size; + max-width: $indicator-size; + min-height: $indicator-size; + font-size: 0.9rem; + font-weight: 600; + } + + .status-indicator { + line-height: 2rem; + + i { + font-size: 2.5rem; + } + } + + .critical { + background-color: #dadada; + } + + .healthy { + background-color: #7ade94; + } + + .suggested { + background-color: #5fd7cc; + } +} +</style> diff --git a/kamon-status-page/src/main/vue/src/components/StatusSection.vue b/kamon-status-page/src/main/vue/src/components/StatusSection.vue new file mode 100644 index 00000000..b94b5bdf --- /dev/null +++ b/kamon-status-page/src/main/vue/src/components/StatusSection.vue @@ -0,0 +1,19 @@ +<template> + <div class="row"> + <div class="col-12 pt-4 pb-2"> + <h3>{{ title }}</h3> + </div> + <div class="col-12 py-1"> + <slot name="default"/> + </div> + </div> +</template> + +<script lang="ts"> +import { Component, Prop, Vue } from 'vue-property-decorator' + +@Component +export default class StatusSection extends Vue { + @Prop() private title!: string +} +</script>
\ No newline at end of file diff --git a/kamon-status-page/src/main/vue/src/main.ts b/kamon-status-page/src/main/vue/src/main.ts new file mode 100644 index 00000000..a0a1ac2c --- /dev/null +++ b/kamon-status-page/src/main/vue/src/main.ts @@ -0,0 +1,13 @@ +import Vue from 'vue' +import App from './App.vue' +import router from './router' +import 'bootstrap/dist/css/bootstrap.min.css' +import '@fortawesome/fontawesome-free/css/all.min.css' +import './styles/main.scss' + +Vue.config.productionTip = false + +new Vue({ + router, + render: h => h(App), +}).$mount('#app') diff --git a/kamon-status-page/src/main/vue/src/router.ts b/kamon-status-page/src/main/vue/src/router.ts new file mode 100644 index 00000000..1f11ff0a --- /dev/null +++ b/kamon-status-page/src/main/vue/src/router.ts @@ -0,0 +1,15 @@ +import Vue from 'vue' +import Router from 'vue-router' +import Overview from './views/Overview.vue' + +Vue.use(Router) + +export default new Router({ + routes: [ + { + path: '/', + name: 'overview', + component: Overview, + }, + ], +}) diff --git a/kamon-status-page/src/main/vue/src/shims-tsx.d.ts b/kamon-status-page/src/main/vue/src/shims-tsx.d.ts new file mode 100644 index 00000000..3b88b582 --- /dev/null +++ b/kamon-status-page/src/main/vue/src/shims-tsx.d.ts @@ -0,0 +1,13 @@ +import Vue, { VNode } from 'vue'; + +declare global { + namespace JSX { + // tslint:disable no-empty-interface + interface Element extends VNode {} + // tslint:disable no-empty-interface + interface ElementClass extends Vue {} + interface IntrinsicElements { + [elem: string]: any; + } + } +} diff --git a/kamon-status-page/src/main/vue/src/shims-vue.d.ts b/kamon-status-page/src/main/vue/src/shims-vue.d.ts new file mode 100644 index 00000000..8f6f4102 --- /dev/null +++ b/kamon-status-page/src/main/vue/src/shims-vue.d.ts @@ -0,0 +1,4 @@ +declare module '*.vue' { + import Vue from 'vue'; + export default Vue; +} diff --git a/kamon-status-page/src/main/vue/src/styles/main.scss b/kamon-status-page/src/main/vue/src/styles/main.scss new file mode 100644 index 00000000..6c0c0551 --- /dev/null +++ b/kamon-status-page/src/main/vue/src/styles/main.scss @@ -0,0 +1,49 @@ +body { + background-color: #f7f7f7; + font-size: 14px; + color: #929292; + font-family: 'Open Sans', Helvetica, Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +h1, h2, h3 { + font-weight: 300; +} + +hr { + margin: 0px; + border-color: #f3f3f3; +} + +.text-label { + font-size: 16px; + color: #b3b3b3; +} + +.tag-container { + margin: 0rem -0.3rem; +} + +.tag { + display: inline-block; + overflow-wrap: anywhere; + background-color: #f5f5f5; + margin: 0.3rem; + padding: 0.1rem 0.5rem; + border-radius: 0.2rem; +} + +.tag-value { + overflow-wrap: anywhere; + color: #676767; +} + +a, a:hover, a:active { + color: inherit; + text-decoration: none; +} + +.c-pointer { + cursor: pointer; +}
\ No newline at end of file diff --git a/kamon-status-page/src/main/vue/src/views/Overview.vue b/kamon-status-page/src/main/vue/src/views/Overview.vue new file mode 100644 index 00000000..424987c1 --- /dev/null +++ b/kamon-status-page/src/main/vue/src/views/Overview.vue @@ -0,0 +1,125 @@ +<template> + <div class="container"> + <div class="row"> + <div class="col-12"> + <overview-card :module-registry="moduleRegistry" :metric-registry="metricRegistry" :instrumentation="instrumentation"/> + </div> + + <div class="col-12"> + <environment-card :environment="environment"/> + </div> + + <div class="col-12"> + <module-list :modules="modules"/> + </div> + + <div class="col-12 pt-4 pb-2" v-if="metrics.length > 0"> + <h2>Metrics</h2> + </div> + <div class="col-12" v-if="metrics.length > 0"> + <metric-list :metrics="metrics"/> + </div> + <div class="col-12 mb-5"> + <instrumentation-module-list :modules="instrumentationModules"/> + </div> + + </div> + </div> +</template> + +<script lang="ts"> +import { Component, Vue } from 'vue-property-decorator' +import {Option, none, some} from 'ts-option' +import ModuleList from '../components/ModuleList.vue' +import InstrumentationModuleList from '../components/InstrumentationModuleList.vue' +import MetricList from '../components/MetricList.vue' +import EnvironmentCard from '../components/EnvironmentCard.vue' +import OverviewCard from '../components/OverviewCard.vue' +import {StatusApi, Settings, ModuleRegistry, ModuleKind, MetricRegistry, Module, Metric, + Instrumentation, Environment, InstrumentationModule} from '../api/StatusApi' + +@Component({ + components: { + 'overview-card': OverviewCard, + 'module-list': ModuleList, + 'instrumentation-module-list': InstrumentationModuleList, + 'metric-list': MetricList, + 'environment-card': EnvironmentCard + }, +}) +export default class Overview extends Vue { + private settings: Option<Settings> = none + private moduleRegistry: Option<ModuleRegistry> = none + private metricRegistry: Option<MetricRegistry> = none + private instrumentation: Option<Instrumentation> = none + + get reporterModules(): Module[] { + return this.moduleRegistry + .map(moduleRegistry => moduleRegistry.modules.filter(this.isReporter)) + .getOrElse([]) + } + + get activeReporters(): Module[] { + return this.reporterModules.filter(this.isStarted) + } + + get plainModules(): Module[] { + return this.moduleRegistry + .map(moduleRegistry => moduleRegistry.modules.filter(m => !this.isReporter(m))) + .getOrElse([]) + } + + get trackedMetrics(): Option<number> { + return this.metricRegistry.map(metricRegistry => metricRegistry.metrics.length) + } + + get instrumentationStatusMessage(): string { + return this.instrumentation.map(i => (i.active ? 'Active' : 'Disabled') as string).getOrElse('Unknown') + } + + get metricsStatusMessage(): string { + return this.trackedMetrics.map(mc => mc + ' Tracked').getOrElse('Unknown') + } + + get metrics(): Metric[] { + return this.metricRegistry + .map(mr => mr.metrics) + .getOrElse([]) + } + + get modules(): Module[] { + return this.moduleRegistry + .map(mr => mr.modules) + .getOrElse([]) + } + + get instrumentationModules(): InstrumentationModule[] { + return this.instrumentation + .map(i => i.modules) + .getOrElse([]) + } + + get environment(): Option<Environment> { + return this.settings.map(s => s.environment) + } + + public mounted() { + this.refreshData() + } + + private refreshData(): void { + StatusApi.settings().then(settings => { this.settings = some(settings) }) + StatusApi.metricRegistryStatus().then(metricRegistry => { this.metricRegistry = some(metricRegistry) }) + StatusApi.moduleRegistryStatus().then(moduleRegistry => {this.moduleRegistry = some(moduleRegistry) }) + StatusApi.instrumentationStatus().then(instrumentation => {this.instrumentation = some(instrumentation) }) + } + + private isReporter(module: Module): boolean { + return [ModuleKind.Combined, ModuleKind.Span, ModuleKind.Metric].indexOf(module.kind) > 0 + } + + private isStarted(module: Module): boolean { + return module.started + } +} +</script> |