This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git


The following commit(s) were added to refs/heads/main by this push:
     new e7fd03b  metrics: layer-specific defaults for MQ / DB / Browser / FaaS 
/ GenAI / K8s
e7fd03b is described below

commit e7fd03bbf5d1e8f281b4f859628212089c025683
Author: Wu Sheng <[email protected]>
AuthorDate: Tue May 12 15:21:23 2026 +0800

    metrics: layer-specific defaults for MQ / DB / Browser / FaaS / GenAI / K8s
    
    Catalog gains 18 short-key metrics (mq.msg-rate, db.qps, browser.pv,
    faas.cold-start, k8s.cpu, genai.tokens, ...) plus a layer-category
    resolver. defaultLandingFor() now seeds an OAP layer with columns,
    orderBy, and sparkline drawn from its category — so a Kafka card opens
    with msg/s + lag instead of cpm + p99.
    
    Setup card splits the column-picker chips into a 'recommended' band
    (layer-relevant) and a 'more' fold-out (full catalog), keeping the
    22-metric list navigable.
---
 apps/ui/src/composables/metricCatalog.ts   | 361 +++++++++++++++++++++++++++++
 apps/ui/src/stores/setup.ts                |  22 +-
 apps/ui/src/views/setup/LayerSetupCard.vue |  55 ++++-
 3 files changed, 422 insertions(+), 16 deletions(-)

diff --git a/apps/ui/src/composables/metricCatalog.ts 
b/apps/ui/src/composables/metricCatalog.ts
index 52ad065..fe69010 100644
--- a/apps/ui/src/composables/metricCatalog.ts
+++ b/apps/ui/src/composables/metricCatalog.ts
@@ -39,6 +39,21 @@ export interface MetricMeta {
   category?: 'throughput' | 'latency' | 'reliability' | 'resource';
 }
 
+/**
+ * Logical layer category. Used to pick a sensible default column set for
+ * the Overview landing card. Concrete OAP layer enums map to one of these
+ * via {@link layerCategory}.
+ */
+export type LayerCategory =
+  | 'general'
+  | 'mesh'
+  | 'k8s'
+  | 'browser'
+  | 'database'
+  | 'mq'
+  | 'faas'
+  | 'genai';
+
 export const METRICS: Record<string, MetricMeta> = {
   cpm: {
     key: 'cpm',
@@ -110,6 +125,171 @@ export const METRICS: Record<string, MetricMeta> = {
     tip: 'Percentage of failed requests. Lower is better.',
     category: 'reliability',
   },
+
+  // --- MQ-flavored layers (kafka / pulsar / rocketmq / rabbitmq / activemq / 
virtual_mq)
+  // Names mirror the booster-ui MQ widget keys (`service_mq_consume_count`,
+  // `service_mq_consumer_lag`) — we surface the short alias here, the MQE
+  // expression is resolved per-deployment by the BFF in Stage 2.6.
+  'mq.msg-rate': {
+    key: 'mq.msg-rate',
+    label: 'msg/s',
+    longLabel: 'Messages per second',
+    tip: 'Producer or consumer message throughput across the cluster.',
+    category: 'throughput',
+  },
+  'mq.consumer-lag': {
+    key: 'mq.consumer-lag',
+    label: 'lag',
+    longLabel: 'Consumer lag',
+    tip: 'Number of messages a consumer is behind the latest offset. Lower is 
better.',
+    category: 'reliability',
+  },
+  'mq.consume-latency': {
+    key: 'mq.consume-latency',
+    label: 'consume',
+    longLabel: 'Consume latency',
+    unit: 'ms',
+    tip: 'Time from publish to consumer acknowledgment.',
+    category: 'latency',
+  },
+
+  // --- DB layers (mysql / postgresql / mongodb / elasticsearch / redis / 
clickhouse / virtual_database)
+  'db.qps': {
+    key: 'db.qps',
+    label: 'qps',
+    longLabel: 'Queries per second',
+    tip: 'Average query throughput over the time window.',
+    category: 'throughput',
+  },
+  'db.slow-queries': {
+    key: 'db.slow-queries',
+    label: 'slow',
+    longLabel: 'Slow query count',
+    tip: 'Number of queries exceeding the slow-query threshold.',
+    category: 'reliability',
+  },
+  'db.conn': {
+    key: 'db.conn',
+    label: 'conns',
+    longLabel: 'Active connections',
+    tip: 'Open client connections to the database.',
+    category: 'resource',
+  },
+
+  // --- Cache (redis / virtual_cache)
+  'cache.hit-rate': {
+    key: 'cache.hit-rate',
+    label: 'hit',
+    longLabel: 'Cache hit rate',
+    unit: '%',
+    tip: 'Percentage of lookups served from cache.',
+    category: 'reliability',
+  },
+
+  // --- Browser layer
+  'browser.pv': {
+    key: 'browser.pv',
+    label: 'pv',
+    longLabel: 'Page views',
+    tip: 'Page-view count over the time window.',
+    category: 'throughput',
+  },
+  'browser.js-err': {
+    key: 'browser.js-err',
+    label: 'js-err',
+    longLabel: 'JS errors',
+    tip: 'Browser JavaScript exceptions reported by the agent.',
+    category: 'reliability',
+  },
+  'browser.page-load': {
+    key: 'browser.page-load',
+    label: 'load',
+    longLabel: 'Page load time',
+    unit: 'ms',
+    tip: 'Full document load time as measured by the navigation timing API.',
+    category: 'latency',
+  },
+  'browser.ajax-resp': {
+    key: 'browser.ajax-resp',
+    label: 'ajax',
+    longLabel: 'AJAX response time',
+    unit: 'ms',
+    tip: 'Average AJAX round-trip latency.',
+    category: 'latency',
+  },
+
+  // --- FaaS (no first-class layer enum yet — speculative for OpenFunction / 
AWS Lambda)
+  'faas.invocations': {
+    key: 'faas.invocations',
+    label: 'invk',
+    longLabel: 'Invocations',
+    tip: 'Number of function invocations over the time window.',
+    category: 'throughput',
+  },
+  'faas.cold-start': {
+    key: 'faas.cold-start',
+    label: 'cold',
+    longLabel: 'Cold-start count',
+    tip: 'Invocations that incurred a runtime cold start.',
+    category: 'reliability',
+  },
+  'faas.duration': {
+    key: 'faas.duration',
+    label: 'dur',
+    longLabel: 'Invocation duration',
+    unit: 'ms',
+    tip: 'Wall-clock execution time of the function.',
+    category: 'latency',
+  },
+
+  // --- K8s layer
+  'k8s.cpu': {
+    key: 'k8s.cpu',
+    label: 'cpu',
+    longLabel: 'CPU usage',
+    unit: '%',
+    tip: 'Container or pod CPU as a percentage of the limit.',
+    category: 'resource',
+  },
+  'k8s.mem': {
+    key: 'k8s.mem',
+    label: 'mem',
+    longLabel: 'Memory usage',
+    unit: '%',
+    tip: 'Container or pod memory as a percentage of the limit.',
+    category: 'resource',
+  },
+  'k8s.restart': {
+    key: 'k8s.restart',
+    label: 'restarts',
+    longLabel: 'Pod restarts',
+    tip: 'Restart count observed on the workload over the time window.',
+    category: 'reliability',
+  },
+
+  // --- GenAI (virtual_genai)
+  'genai.tokens': {
+    key: 'genai.tokens',
+    label: 'tok/s',
+    longLabel: 'Tokens per second',
+    tip: 'Combined input + output token throughput.',
+    category: 'throughput',
+  },
+  'genai.req': {
+    key: 'genai.req',
+    label: 'req',
+    longLabel: 'Requests',
+    tip: 'Inference request count over the time window.',
+    category: 'throughput',
+  },
+  'genai.latency': {
+    key: 'genai.latency',
+    label: 'latency',
+    longLabel: 'Inference latency',
+    unit: 'ms',
+    tip: 'End-to-end request latency including queueing.',
+    category: 'latency',
+  },
 };
 
 /** Lookup with a graceful fallback so unknown metrics render readable. */
@@ -125,3 +305,184 @@ export function metricMeta(key: string): MetricMeta {
 }
 
 export const METRIC_KEYS: ReadonlyArray<string> = Object.keys(METRICS);
+
+/**
+ * Bucket an OAP layer enum into a logical category so we can pick a sane
+ * default column set on the landing card. Unknown layers fall back to the
+ * `general` (RPC-shaped) set.
+ */
+export function layerCategory(layerKey: string): LayerCategory {
+  const k = layerKey.toLowerCase();
+  if (k === 'general') return 'general';
+  if (k === 'mesh' || k === 'mesh_cp' || k === 'mesh_dp') return 'mesh';
+  if (k === 'k8s' || k === 'k8s_service') return 'k8s';
+  if (k === 'browser') return 'browser';
+  if (k === 'virtual_genai') return 'genai';
+  if (k === 'faas' || k === 'so11y_openfunction' || k.endsWith('_faas')) 
return 'faas';
+  if (
+    k === 'mysql' || k === 'postgresql' || k === 'mongodb' || k === 
'elasticsearch' ||
+    k === 'redis' || k === 'clickhouse' || k === 'virtual_database' || k === 
'virtual_cache'
+  ) return 'database';
+  if (
+    k === 'kafka' || k === 'pulsar' || k === 'rocketmq' || k === 'rabbitmq' ||
+    k === 'activemq' || k === 'virtual_mq'
+  ) return 'mq';
+  return 'general';
+}
+
+interface DefaultLandingSet {
+  /** 3–4 columns; first one is usually a throughput-ish metric. */
+  columns: Array<{ metric: string; label?: string; unit?: string }>;
+  /** Metric key used to rank the top-N. */
+  orderBy: string;
+  /** Sparkline metric (defaults to `orderBy` when omitted). */
+  spark?: string;
+}
+
+const LAYER_TYPE_DEFAULTS: Record<LayerCategory, DefaultLandingSet> = {
+  general: {
+    columns: [
+      { metric: 'cpm' },
+      { metric: 'p99' },
+      { metric: 'sla' },
+      { metric: 'err' },
+    ],
+    orderBy: 'cpm',
+  },
+  mesh: {
+    columns: [
+      { metric: 'cpm' },
+      { metric: 'p99' },
+      { metric: 'sla' },
+      { metric: 'err' },
+    ],
+    orderBy: 'cpm',
+  },
+  k8s: {
+    columns: [
+      { metric: 'k8s.cpu' },
+      { metric: 'k8s.mem' },
+      { metric: 'k8s.restart' },
+    ],
+    orderBy: 'k8s.cpu',
+    spark: 'k8s.cpu',
+  },
+  browser: {
+    columns: [
+      { metric: 'browser.pv' },
+      { metric: 'browser.page-load' },
+      { metric: 'browser.ajax-resp' },
+      { metric: 'browser.js-err' },
+    ],
+    orderBy: 'browser.pv',
+    spark: 'browser.pv',
+  },
+  database: {
+    columns: [
+      { metric: 'db.qps' },
+      { metric: 'resp' },
+      { metric: 'db.slow-queries' },
+      { metric: 'db.conn' },
+    ],
+    orderBy: 'db.qps',
+    spark: 'db.qps',
+  },
+  mq: {
+    columns: [
+      { metric: 'mq.msg-rate' },
+      { metric: 'mq.consume-latency' },
+      { metric: 'mq.consumer-lag' },
+    ],
+    orderBy: 'mq.msg-rate',
+    spark: 'mq.msg-rate',
+  },
+  faas: {
+    columns: [
+      { metric: 'faas.invocations' },
+      { metric: 'faas.duration' },
+      { metric: 'faas.cold-start' },
+      { metric: 'err' },
+    ],
+    orderBy: 'faas.invocations',
+    spark: 'faas.invocations',
+  },
+  genai: {
+    columns: [
+      { metric: 'genai.req' },
+      { metric: 'genai.tokens' },
+      { metric: 'genai.latency' },
+    ],
+    orderBy: 'genai.req',
+    spark: 'genai.tokens',
+  },
+};
+
+/** Per-layer-type column defaults. Each column reads label + unit from the
+ *  metric catalog so changes in METRICS flow through without touching this. */
+export function defaultColumnsForLayer(
+  layerKey: string,
+): Array<{ metric: string; label: string; unit?: string }> {
+  const set = LAYER_TYPE_DEFAULTS[layerCategory(layerKey)];
+  return set.columns.map((c) => {
+    const meta = metricMeta(c.metric);
+    return {
+      metric: c.metric,
+      label: c.label ?? meta.label,
+      unit: c.unit ?? meta.unit,
+    };
+  });
+}
+
+/** Metric key used to rank the top-N services on a layer's landing card. */
+export function defaultOrderByForLayer(layerKey: string): string {
+  return LAYER_TYPE_DEFAULTS[layerCategory(layerKey)].orderBy;
+}
+
+/** Sparkline metric for the landing card (falls back to the order-by key). */
+export function defaultSparkForLayer(layerKey: string): string {
+  const set = LAYER_TYPE_DEFAULTS[layerCategory(layerKey)];
+  return set.spark ?? set.orderBy;
+}
+
+/**
+ * Generic RPC-shaped metrics every layer can render — surfaced as a
+ * fallback group in the setup UI's chip picker after the layer-specific
+ * defaults.
+ */
+const GENERIC_METRIC_KEYS: ReadonlyArray<string> = [
+  'cpm', 'resp', 'p50', 'p75', 'p95', 'p99', 'sla', 'apdex', 'err',
+];
+
+const LAYER_TYPE_METRIC_KEYS: Record<LayerCategory, ReadonlyArray<string>> = {
+  general: GENERIC_METRIC_KEYS,
+  mesh: GENERIC_METRIC_KEYS,
+  k8s: ['k8s.cpu', 'k8s.mem', 'k8s.restart'],
+  browser: ['browser.pv', 'browser.page-load', 'browser.ajax-resp', 
'browser.js-err'],
+  database: ['db.qps', 'db.slow-queries', 'db.conn', 'cache.hit-rate'],
+  mq: ['mq.msg-rate', 'mq.consume-latency', 'mq.consumer-lag'],
+  faas: ['faas.invocations', 'faas.duration', 'faas.cold-start'],
+  genai: ['genai.req', 'genai.tokens', 'genai.latency'],
+};
+
+/**
+ * Sort metric keys into two buckets for the setup UI: relevant to this
+ * layer's category, vs. the rest of the catalog. Operators can still pick
+ * anything, but the recommended ones surface first.
+ */
+export function metricsForLayer(layerKey: string): {
+  recommended: MetricMeta[];
+  other: MetricMeta[];
+} {
+  const cat = layerCategory(layerKey);
+  const recoKeys = new Set<string>([
+    ...LAYER_TYPE_METRIC_KEYS[cat],
+    // Every layer can usefully render generic reliability/error metrics.
+    ...(cat === 'general' || cat === 'mesh' ? [] : ['err']),
+  ]);
+  const recommended: MetricMeta[] = [];
+  const other: MetricMeta[] = [];
+  for (const m of Object.values(METRICS)) {
+    (recoKeys.has(m.key) ? recommended : other).push(m);
+  }
+  return { recommended, other };
+}
diff --git a/apps/ui/src/stores/setup.ts b/apps/ui/src/stores/setup.ts
index b39c1ba..0a885c5 100644
--- a/apps/ui/src/stores/setup.ts
+++ b/apps/ui/src/stores/setup.ts
@@ -24,6 +24,11 @@ import type {
   LayerSlots,
 } from '@skywalking-horizon-ui/api-client';
 import { bffClient } from '@/api/client';
+import {
+  defaultColumnsForLayer,
+  defaultOrderByForLayer,
+  defaultSparkForLayer,
+} from '@/composables/metricCatalog';
 
 export type { LayerConfig, LandingConfig };
 
@@ -37,24 +42,13 @@ function defaultPriority(layerKey: string): number {
   return 99;
 }
 
-/** Default-columns table per layer category. Concrete MQE metric names are
- *  illustrative until Stage 2.6 wires them up — adjust per layer admin. */
-function defaultColumns(_layerKey: string): LandingConfig['columns'] {
-  return [
-    { metric: 'cpm', label: 'cpm' },
-    { metric: 'p99', label: 'p99', unit: 'ms' },
-    { metric: 'sla', label: 'SLA', unit: '%' },
-    { metric: 'err', label: 'err', unit: '%' },
-  ];
-}
-
 export function defaultLandingFor(layerKey: string): LandingConfig {
   return {
     priority: defaultPriority(layerKey),
     topN: 5,
-    orderBy: 'cpm',
-    columns: defaultColumns(layerKey),
-    spark: { metric: 'cpm', height: 28 },
+    orderBy: defaultOrderByForLayer(layerKey),
+    columns: defaultColumnsForLayer(layerKey),
+    spark: { metric: defaultSparkForLayer(layerKey), height: 28 },
     style: 'table',
   };
 }
diff --git a/apps/ui/src/views/setup/LayerSetupCard.vue 
b/apps/ui/src/views/setup/LayerSetupCard.vue
index 3f30c48..cb2b04e 100644
--- a/apps/ui/src/views/setup/LayerSetupCard.vue
+++ b/apps/ui/src/views/setup/LayerSetupCard.vue
@@ -18,7 +18,7 @@
 import { computed, ref } from 'vue';
 import type { LayerDef } from '@skywalking-horizon-ui/api-client';
 import Icon from '@/components/icons/Icon.vue';
-import { METRICS } from '@/composables/metricCatalog';
+import { METRICS, metricsForLayer } from '@/composables/metricCatalog';
 import { useSetupStore, defaultLandingFor } from '@/stores/setup';
 
 const props = defineProps<{ layer: LayerDef; expanded?: boolean }>();
@@ -79,6 +79,22 @@ const availableColumns = Object.values(METRICS).map((m) => ({
   unit: m.unit,
   tip: m.tip,
 }));
+// Chip groups: layer-relevant metrics first, the rest collapsed below.
+const groupedColumns = computed(() => {
+  const { recommended, other } = metricsForLayer(props.layer.key);
+  const toOpt = (m: typeof recommended[number]) => ({
+    metric: m.key,
+    label: m.label,
+    longLabel: m.longLabel,
+    unit: m.unit,
+    tip: m.tip,
+  });
+  return {
+    recommended: recommended.map(toOpt),
+    other: other.map(toOpt),
+  };
+});
+const showAllChips = ref(false);
 function isColumnSelected(metric: string): boolean {
   return cfg.value.landing.columns.some((c) => c.metric === metric);
 }
@@ -204,7 +220,7 @@ const isDefaultLanding = computed(() => {
           <span class="cols-label">Columns (max 5)</span>
           <div class="cols-chips">
             <button
-              v-for="c in availableColumns"
+              v-for="c in groupedColumns.recommended"
               :key="c.metric"
               class="chip"
               :class="{ on: isColumnSelected(c.metric) }"
@@ -214,6 +230,29 @@ const isDefaultLanding = computed(() => {
             >
               {{ c.label }}<span v-if="c.unit" class="unit">{{ c.unit }}</span>
             </button>
+            <button
+              v-if="!showAllChips && groupedColumns.other.length > 0"
+              class="chip more"
+              type="button"
+              :title="`Show ${groupedColumns.other.length} more 
metric${groupedColumns.other.length === 1 ? '' : 's'}`"
+              @click="showAllChips = true"
+            >
+              + {{ groupedColumns.other.length }} more
+            </button>
+            <template v-if="showAllChips">
+              <span class="group-sep">other</span>
+              <button
+                v-for="c in groupedColumns.other"
+                :key="c.metric"
+                class="chip"
+                :class="{ on: isColumnSelected(c.metric) }"
+                type="button"
+                :title="`${c.longLabel}\n\n${c.tip}`"
+                @click="toggleColumn(c.metric, c.label, c.unit)"
+              >
+                {{ c.label }}<span v-if="c.unit" class="unit">{{ c.unit 
}}</span>
+              </button>
+            </template>
           </div>
         </div>
       </section>
@@ -381,6 +420,18 @@ const isDefaultLanding = computed(() => {
   color: var(--sw-fg-3);
   font-size: 10px;
 }
+.chip.more {
+  border-style: dashed;
+  color: var(--sw-fg-2);
+}
+.group-sep {
+  font-size: 9px;
+  text-transform: uppercase;
+  letter-spacing: 0.1em;
+  color: var(--sw-fg-3);
+  align-self: center;
+  margin: 0 6px;
+}
 .actions {
   display: flex;
   align-items: center;

Reply via email to