You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/09/20 16:08:54 UTC
(skywalking-horizon-ui) branch feat/traceql-otlp-and-layer-config created (now 96e0cbb0)
This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a change to branch feat/traceql-otlp-and-layer-config
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
at 96e0cbb0 fix(traces): conclude success trace-wide, and keep every labelled series
This branch includes the following new commits:
new 265274f2 feat(traces): query OAP's natively stored OTLP spans, and let a layer configure what it shows
new 96e0cbb0 fix(traces): conclude success trace-wide, and keep every labelled series
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(skywalking-horizon-ui) 02/02: fix(traces): conclude success trace-wide, and keep every labelled series
Posted by wu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch feat/traceql-otlp-and-layer-config
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit 96e0cbb0fbeeecf62b60348c1dfdfe8f8edb3666
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Mon Sep 21 00:08:44 2026 +0800
fix(traces): conclude success trace-wide, and keep every labelled series
All four found in review.
A search span set holds the spans that MATCHED, capped at `spss`, so an `ok`
among them said nothing about the spans the filter excluded — and reading one
as success stopped both the deferred pass and the UI from ever looking again.
Success now comes only from `serviceStats`, which counts the whole trace;
a failing span still fails it at once, and an OAP that sends no counts leaves
the row unknown, to be read.
Percentile and status-code metrics answer with several labelled results. The
GraphQL selection did not ask for the labels and the reader kept `results[0]`,
so the ten percentiles and the response-status metric this adds would each
have drawn one unnamed series as though it were the metric. Labels are
requested, every result is carried, and the edge chart draws them all.
`parsePreviewProcessTopology` still demanded the old side lists, so Preview
returned null for anything the editor now writes and quietly used the
published template instead — an edit that looked like it did nothing. Its
metric cap was 32, below the 38 the bundled layers ship.
The editor normalised an old-spelling config on mount only. Switching layer or
resetting to remote reuses the component, leaving the rows bound to the
resolver's copies, so edits landed on a clone the draft never saw.
---
apps/bff/src/client/traceql.ts | 29 ++++++++++++------
apps/bff/src/http/query/ebpf.ts | 34 ++++++++++++++++-----
apps/bff/src/http/query/traceql.test.ts | 35 ++++++++++++++++++----
apps/bff/src/logic/layers/preview.ts | 21 +++++++++----
.../layer-templates/ProcessConfigEditor.test.ts | 20 +++++++++++++
.../admin/layer-templates/ProcessConfigEditor.vue | 20 +++++++++----
apps/ui/src/i18n/locales/de.json | 1 +
apps/ui/src/i18n/locales/en.json | 1 +
apps/ui/src/i18n/locales/es.json | 1 +
apps/ui/src/i18n/locales/fr.json | 1 +
apps/ui/src/i18n/locales/ja.json | 1 +
apps/ui/src/i18n/locales/ko.json | 1 +
apps/ui/src/i18n/locales/pt.json | 1 +
apps/ui/src/i18n/locales/zh-CN.json | 1 +
.../layer/profiling/LayerNetworkProfilingView.vue | 7 +++--
packages/api-client/src/topology.ts | 5 ++++
16 files changed, 144 insertions(+), 35 deletions(-)
diff --git a/apps/bff/src/client/traceql.ts b/apps/bff/src/client/traceql.ts
index be60ad28..feae0359 100644
--- a/apps/bff/src/client/traceql.ts
+++ b/apps/bff/src/client/traceql.ts
@@ -236,6 +236,9 @@ interface WireSearchTrace {
startTimeUnixNano?: string;
durationMs?: number;
spanSets?: Array<{ spans?: WireSearchSpan[]; matched?: number }>;
+ /** Span and error counts per service over the WHOLE trace, which the span
+ * sets are not: they hold the spans that matched, capped at `spss`. */
+ serviceStats?: Record<string, { spanCount?: number; errorCount?: number }>;
}
interface WireSearchResponse {
@@ -379,14 +382,21 @@ export function focusServiceOf(q: string | undefined): string | null {
return m ? m[1] : null;
}
+/** Errors over the whole trace, or null when OAP did not count them. Older
+ * OAPs send no `serviceStats`, and then the row stays unknown and is read. */
+function traceWideErrors(t: WireSearchTrace): number | null {
+ const stats = t.serviceStats;
+ if (!stats || Object.keys(stats).length === 0) return null;
+ let errors = 0;
+ for (const s of Object.values(stats)) errors += s.errorCount ?? 0;
+ return errors;
+}
+
function toRow(ds: TraceQLDatasource, t: WireSearchTrace, focus: string | null): TraceQLTraceRow {
const spans = (t.spanSets ?? []).flatMap((s) => s.spans ?? []);
const traceStartNs = num(t.startTimeUnixNano);
const services: string[] = [];
let anyError = false;
- /** Whether the response ANSWERED for the status, so an unknown row is told
- * apart from one OAP called healthy — the list only re-reads the unknowns. */
- let statusKnown = false;
let entry: TraceQLServiceEntry | undefined;
let entryStartNs: number | null = null;
for (const s of spans) {
@@ -402,7 +412,6 @@ function toRow(ds: TraceQLDatasource, t: WireSearchTrace, focus: string | null):
if (listed === 'error') anyError = true;
else if (listed !== 'ok' && !anyError
&& attributesReportError(attrList(s.attributes), { emptyIsAbsent: true })) anyError = true;
- if (listed === 'error' || listed === 'ok') statusKnown = true;
if (focus && svc === focus) {
const startNs = num(s.startTimeUnixNano);
// The service's FIRST span — where it enters the trace. A service can be
@@ -430,11 +439,13 @@ function toRow(ds: TraceQLDatasource, t: WireSearchTrace, focus: string | null):
services,
serviceEntry: entry,
// `undefined` is UNKNOWN, and the list re-reads those traces one by one.
- // OAP answering with a `status` settles the row here, which is the whole
- // point of it projecting one. Without that, a marker declares FAILURE only:
- // no marker is not success, since the result tags are configurable upstream
- // and a span can fail carrying none of them.
- isError: anyError ? true : statusKnown ? false : undefined,
+ //
+ // SUCCESS is only ever concluded from `serviceStats`, which counts the
+ // whole trace. A span set holds the spans that MATCHED, capped at `spss`,
+ // so an `ok` among them says nothing about the spans the query excluded —
+ // reading one as success hides a failure downstream of the filter and
+ // stops anything looking again.
+ isError: anyError ? true : traceWideErrors(t) === 0 ? false : undefined,
};
}
diff --git a/apps/bff/src/http/query/ebpf.ts b/apps/bff/src/http/query/ebpf.ts
index 325d4710..34355d6e 100644
--- a/apps/bff/src/http/query/ebpf.ts
+++ b/apps/bff/src/http/query/ebpf.ts
@@ -302,7 +302,13 @@ function sanitiseNetworkSamplings(
interface MqeEnv {
error?: string | null;
- results?: Array<{ values?: Array<{ value: string | number | null }> }>;
+ results?: Array<{
+ /** Labels distinguish the results of a multi-result metric — a percentile
+ * publishes one per rank, a status-code metric one per code. Without them
+ * the extra results are indistinguishable and get dropped. */
+ metric?: { labels?: Array<{ key?: string; value?: string }> };
+ values?: Array<{ value: string | number | null }>;
+ }>;
}
/**
@@ -334,20 +340,32 @@ function processRelationFragment(
` destServiceInstanceName: ${JSON.stringify(dst.serviceInstanceName)},` +
` destProcessName: ${JSON.stringify(dst.processName)} },\n` +
` duration: { start: ${JSON.stringify(w.start)}, end: ${JSON.stringify(w.end)}, step: MINUTE${coldFrag} }\n` +
- ` ) { error results { values { value } } }`
+ ` ) { error results { metric { labels { key value } } values { value } } }`
);
}
-function relationSeries(env: MqeEnv | undefined): Array<number | null> {
- if (!env || env.error) return [];
- const values = env.results?.[0]?.values ?? [];
- return values.map((v) => {
+function numbers(values: Array<{ value: string | number | null }> | undefined): Array<number | null> {
+ return (values ?? []).map((v) => {
if (v.value === null || v.value === undefined) return null;
const n = Number(v.value);
return Number.isFinite(n) ? n : null;
});
}
+/** EVERY result, each under its labels. A percentile metric answers with one
+ * result per rank and a status-code metric one per code; keeping only the
+ * first drew one of them, unnamed, as if it were the whole metric. */
+function relationSeriesAll(env: MqeEnv | undefined): Array<{ label?: string; values: Array<number | null> }> {
+ if (!env || env.error) return [];
+ return (env.results ?? []).map((r) => {
+ const label = (r.metric?.labels ?? [])
+ .map((l) => l.value)
+ .filter((v): v is string => !!v)
+ .join(' · ');
+ return { ...(label ? { label } : {}), values: numbers(r.values) };
+ });
+}
+
// `detectType` matters because the two sides disagree on VIRTUAL processes:
// GraphQL listProcesses calls the DAO with includeVirtual = TRUE, while the gate
// that decides whether a network task may be created —
@@ -784,11 +802,13 @@ export function registerEBPFRoutes(app: FastifyInstance, deps: EBPFRouteDeps): v
try {
const raw = await graphqlPost<Record<string, MqeEnv>>(opts, query);
for (const [alias, { side, metric }] of aliasMap) {
+ const series = relationSeriesAll(raw[alias]);
const out: ProcessRelationMetric = {
id: metric.id,
label: metric.label,
unit: metric.unit,
- values: relationSeries(raw[alias]),
+ values: series[0]?.values ?? [],
+ ...(series.length > 1 ? { series } : {}),
};
if (side === 'client') payload.client.push(out);
else if (side === 'server') payload.server.push(out);
diff --git a/apps/bff/src/http/query/traceql.test.ts b/apps/bff/src/http/query/traceql.test.ts
index 52fe4acf..f8e55fff 100644
--- a/apps/bff/src/http/query/traceql.test.ts
+++ b/apps/bff/src/http/query/traceql.test.ts
@@ -251,10 +251,33 @@ describe('search: the window is seconds, and the rows are reduced', () => {
expect(zipkinRow!.isError).toBe(true);
});
- it('settles the row from the status OAP projects onto a search span', async () => {
+ it('will not call a trace healthy from the spans a query matched', async () => {
+ // A span set holds the spans that MATCHED, capped at `spss`. An `ok`
+ // among them says nothing about the ones the filter excluded, so success
+ // comes from serviceStats — which counts the whole trace — or not at all.
+ const withStats = (stats?: Record<string, { errorCount: number }>): FetchLike => async () => {
+ const body = searchBody();
+ body.traces[0]!.spanSets[0]!.spans[0]!.attributes.push(attr('status', 'ok'));
+ if (stats) (body.traces[0] as Record<string, unknown>).serviceStats = stats;
+ return json(body);
+ };
+ const row = async (stats?: Record<string, { errorCount: number }>) => {
+ const opts = buildTraceQLOpts(cfgWith({ otlpUrl: OTLP_URL }), 'otlp', withStats(stats))!;
+ const [r] = await traceqlSearch(opts, { startMs: 1, endMs: 2, limit: 5 });
+ return r!.isError;
+ };
+ // An ok span and no trace-wide count: unknown, and the list reads it.
+ expect(await row()).toBeUndefined();
+ // Counted, and clean across every service: settled here.
+ expect(await row({ a: { errorCount: 0 }, b: { errorCount: 0 } })).toBe(false);
+ // Counted, and a service the span set never listed failed.
+ expect(await row({ a: { errorCount: 0 }, b: { errorCount: 2 } })).toBeUndefined();
+ });
+
+ it('reads a FAILURE from the status OAP projects onto a search span', async () => {
// OAP writes `status` on every search-result span so a list can show
- // failures without reading each trace. Without it the row stays unknown
- // and the page reads the trace to find out.
+ // failures without reading each trace. One failing span fails the trace
+ // whatever the query matched, which is why this direction settles.
const withStatus = (status: string): FetchLike => async () => {
const body = searchBody();
body.traces[0]!.spanSets[0]!.spans[0]!.attributes.push(attr('status', status));
@@ -266,9 +289,9 @@ describe('search: the window is seconds, and the rows are reduced', () => {
return r!.isError;
};
expect(await row('error')).toBe(true);
- // `ok` is an ANSWER, so the row is settled and nothing re-reads it.
- expect(await row('ok')).toBe(false);
- // `unset` is the absence of one, and leaves the row to the status pass.
+ // `ok` settles nothing on its own — it speaks for one matched span, not
+ // for the trace. Success needs the trace-wide count; see the test above.
+ expect(await row('ok')).toBeUndefined();
expect(await row('unset')).toBeUndefined();
});
diff --git a/apps/bff/src/logic/layers/preview.ts b/apps/bff/src/logic/layers/preview.ts
index cb2d5fb2..0f59e712 100644
--- a/apps/bff/src/logic/layers/preview.ts
+++ b/apps/bff/src/logic/layers/preview.ts
@@ -44,7 +44,10 @@ import type {
} from './loader.js';
const MAX_RAW_BYTES = 32_000;
-const MAX_METRICS = 32;
+/** A preview config comes from the browser, so every list is bounded. 64
+ * clears the largest bundled set — network profiling ships 38 edge metrics —
+ * with room to add a few while editing. */
+const MAX_METRICS = 64;
const MAX_MQE_LEN = 512;
function isMetricList(v: unknown): v is TopologyMetricDef[] {
@@ -188,13 +191,19 @@ function previewStores(raw: unknown): TracesConfig['stores'] | null {
return Object.keys(out).length > 0 ? out : null;
}
-/** `processTopology` block — the network-profiling edge-detail metric
- * lists (ProcessRelation scope, client + server). */
+/** `processTopology` block — the network-profiling edge-detail metrics
+ * (ProcessRelation scope). `edgeMetrics` is what the editor writes; the two
+ * side lists are still accepted from a draft that predates it. Refusing the
+ * new spelling made Preview fall back to the published template without
+ * saying so, which reads as the edit having no effect. */
export function parsePreviewProcessTopology(raw: string | undefined): ProcessTopologyConfig | null {
const o = parseJson(raw);
if (!o) return null;
- if (o.edgeClientMetrics !== undefined && !isMetricList(o.edgeClientMetrics)) return null;
- if (o.edgeServerMetrics !== undefined && !isMetricList(o.edgeServerMetrics)) return null;
- if (o.edgeClientMetrics === undefined && o.edgeServerMetrics === undefined) return null;
+ for (const key of ['edgeMetrics', 'edgeClientMetrics', 'edgeServerMetrics']) {
+ if (o[key] !== undefined && !isMetricList(o[key])) return null;
+ }
+ if (o.edgeMetrics === undefined
+ && o.edgeClientMetrics === undefined
+ && o.edgeServerMetrics === undefined) return null;
return o as unknown as ProcessTopologyConfig;
}
diff --git a/apps/ui/src/features/admin/layer-templates/ProcessConfigEditor.test.ts b/apps/ui/src/features/admin/layer-templates/ProcessConfigEditor.test.ts
index a80001c6..774f206e 100644
--- a/apps/ui/src/features/admin/layer-templates/ProcessConfigEditor.test.ts
+++ b/apps/ui/src/features/admin/layer-templates/ProcessConfigEditor.test.ts
@@ -18,6 +18,8 @@
import { describe, expect, it } from 'vitest';
import { mount } from '@vue/test-utils';
import { i18n } from '@/i18n';
+import type { ProcessTopologyConfig } from '@skywalking-horizon-ui/api-client';
+import { nextTick } from 'vue';
import ProcessConfigEditor from './ProcessConfigEditor.vue';
const config = () => ({
@@ -46,3 +48,21 @@ describe('ProcessConfigEditor MQE explorer coverage', () => {
expect(wrapper.find('.mqe-run').exists()).toBe(false);
});
});
+
+describe('editing a config written in the older spelling', () => {
+ it('rewrites it to one list, so edits land on the draft', async () => {
+ // The component is REUSED when a layer changes or Reset to remote runs,
+ // so normalising on mount alone left the rows bound to copies and threw
+ // every edit away.
+ const wrapper = mount(ProcessConfigEditor, {
+ props: { config: config(), layerKey: 'GENERAL' },
+ global: { plugins: [i18n] },
+ });
+ await nextTick();
+ const written = wrapper.emitted('update:config')?.at(-1)?.[0] as ProcessTopologyConfig;
+ expect(written.edgeMetrics).toEqual([
+ { id: 'write', label: 'Write', mqe: 'process_relation_client_write_cpm', aggregation: 'avg', side: 'client' },
+ { id: 'read', label: 'Read', mqe: 'process_relation_server_read_cpm', aggregation: 'avg', side: 'server' },
+ ]);
+ });
+});
diff --git a/apps/ui/src/features/admin/layer-templates/ProcessConfigEditor.vue b/apps/ui/src/features/admin/layer-templates/ProcessConfigEditor.vue
index e7402392..1e2c7caf 100644
--- a/apps/ui/src/features/admin/layer-templates/ProcessConfigEditor.vue
+++ b/apps/ui/src/features/admin/layer-templates/ProcessConfigEditor.vue
@@ -22,7 +22,7 @@
(no role / thresholds).
-->
<script setup lang="ts">
-import { computed, onMounted } from 'vue';
+import { computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import type {
ProcessRelationSide,
@@ -51,11 +51,21 @@ function ensure(): ProcessTopologyConfig & { edgeMetrics: EdgeMetric[] } {
}
return config.value as ProcessTopologyConfig & { edgeMetrics: EdgeMetric[] };
}
-onMounted(() => {
- if (!props.readOnly) ensure();
-});
+const edgeMetrics = computed<EdgeMetric[]>(() => config.value?.edgeMetrics ?? resolveEdgeMetrics(config.value));
+
+/** Normalise whenever the config is REPLACED, not just on mount: switching
+ * layer or resetting to remote reuses this component, and a replacement in
+ * the old spelling would leave the rows bound to `resolveEdgeMetrics`'s
+ * copies — edits landing on a clone the draft never sees. */
+watch(
+ config,
+ (c) => {
+ if (props.readOnly) return;
+ if (!c || !c.edgeMetrics) ensure();
+ },
+ { immediate: true },
+);
-const edgeMetrics = computed<EdgeMetric[]>(() => resolveEdgeMetrics(config.value));
function blankMetric(taken: readonly TopologyMetricDef[]): TopologyMetricDef {
const id = nextFreeId('metric_', taken.map((m) => m.id));
diff --git a/apps/ui/src/i18n/locales/de.json b/apps/ui/src/i18n/locales/de.json
index e4861000..03cf1666 100644
--- a/apps/ui/src/i18n/locales/de.json
+++ b/apps/ui/src/i18n/locales/de.json
@@ -1036,6 +1036,7 @@
"Side": "Seite",
"neither — describes the exchange": "keine — beschreibt den Austausch",
"Both sides": "Beide Seiten",
+ "{n} series": "{n} Reihen",
"No shared metrics configured.": "Keine gemeinsamen Metriken konfiguriert.",
"No service matches this": "Kein Service entspricht dem",
"No services match": "Keine Services passen",
diff --git a/apps/ui/src/i18n/locales/en.json b/apps/ui/src/i18n/locales/en.json
index 0c6d64bf..2335a860 100644
--- a/apps/ui/src/i18n/locales/en.json
+++ b/apps/ui/src/i18n/locales/en.json
@@ -1036,6 +1036,7 @@
"Side": "Side",
"neither — describes the exchange": "neither — describes the exchange",
"Both sides": "Both sides",
+ "{n} series": "{n} series",
"No shared metrics configured.": "No shared metrics configured.",
"No service matches this": "No service matches this",
"No services match": "No services match",
diff --git a/apps/ui/src/i18n/locales/es.json b/apps/ui/src/i18n/locales/es.json
index 25e6e066..19710159 100644
--- a/apps/ui/src/i18n/locales/es.json
+++ b/apps/ui/src/i18n/locales/es.json
@@ -1036,6 +1036,7 @@
"Side": "Lado",
"neither — describes the exchange": "ninguno — describe el intercambio",
"Both sides": "Ambos lados",
+ "{n} series": "{n} series",
"No shared metrics configured.": "No hay métricas compartidas configuradas.",
"No service matches this": "Ningún servicio coincide con esto",
"No services match": "Ningún servicio coincide",
diff --git a/apps/ui/src/i18n/locales/fr.json b/apps/ui/src/i18n/locales/fr.json
index 8e7a687f..a93d9320 100644
--- a/apps/ui/src/i18n/locales/fr.json
+++ b/apps/ui/src/i18n/locales/fr.json
@@ -1036,6 +1036,7 @@
"Side": "Côté",
"neither — describes the exchange": "aucun — décrit l’échange",
"Both sides": "Les deux côtés",
+ "{n} series": "{n} séries",
"No shared metrics configured.": "Aucune métrique partagée configurée.",
"No service matches this": "Aucun service ne correspond",
"No services match": "Aucun service ne correspond",
diff --git a/apps/ui/src/i18n/locales/ja.json b/apps/ui/src/i18n/locales/ja.json
index 4acba6e8..c60785a8 100644
--- a/apps/ui/src/i18n/locales/ja.json
+++ b/apps/ui/src/i18n/locales/ja.json
@@ -1036,6 +1036,7 @@
"Side": "側",
"neither — describes the exchange": "どちらでもない — やり取りを表す",
"Both sides": "両側",
+ "{n} series": "{n} 系列",
"No shared metrics configured.": "共有メトリクスが設定されていません。",
"No service matches this": "該当するサービスはありません",
"No services match": "一致するサービスがありません",
diff --git a/apps/ui/src/i18n/locales/ko.json b/apps/ui/src/i18n/locales/ko.json
index c46dffa0..f3e796e7 100644
--- a/apps/ui/src/i18n/locales/ko.json
+++ b/apps/ui/src/i18n/locales/ko.json
@@ -1036,6 +1036,7 @@
"Side": "측",
"neither — describes the exchange": "해당 없음 — 교환 자체를 설명",
"Both sides": "양측",
+ "{n} series": "{n}개 계열",
"No shared metrics configured.": "구성된 공유 메트릭이 없습니다.",
"No service matches this": "해당하는 서비스가 없습니다",
"No services match": "일치하는 서비스가 없습니다",
diff --git a/apps/ui/src/i18n/locales/pt.json b/apps/ui/src/i18n/locales/pt.json
index 7cc47007..290d5848 100644
--- a/apps/ui/src/i18n/locales/pt.json
+++ b/apps/ui/src/i18n/locales/pt.json
@@ -1036,6 +1036,7 @@
"Side": "Lado",
"neither — describes the exchange": "nenhum — descreve a troca",
"Both sides": "Ambos os lados",
+ "{n} series": "{n} séries",
"No shared metrics configured.": "Nenhuma métrica partilhada configurada.",
"No service matches this": "Nenhum serviço corresponde a isto",
"No services match": "Nenhum serviço corresponde",
diff --git a/apps/ui/src/i18n/locales/zh-CN.json b/apps/ui/src/i18n/locales/zh-CN.json
index 6e0a04b9..39508b91 100644
--- a/apps/ui/src/i18n/locales/zh-CN.json
+++ b/apps/ui/src/i18n/locales/zh-CN.json
@@ -1036,6 +1036,7 @@
"Side": "端",
"neither — describes the exchange": "都不是 — 描述交互本身",
"Both sides": "双端",
+ "{n} series": "{n} 条序列",
"No shared metrics configured.": "未配置双端共享指标。",
"No service matches this": "没有服务符合该条件",
"No services match": "无匹配的服务",
diff --git a/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue b/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
index b33d7fc1..f0d42293 100644
--- a/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
+++ b/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
@@ -509,10 +509,13 @@ function fmtTime(ms: number): string {
<div v-for="m in relationMetrics[side]" :key="m.id" class="edge-widget sw-card">
<div class="ew-head">
<span class="ew-label">{{ m.label }}</span>
- <span class="ew-val mono">{{ fmtMetric(latestValue(m.values), m.unit) }}</span>
+ <span v-if="!m.series?.length" class="ew-val mono">{{ fmtMetric(latestValue(m.values), m.unit) }}</span>
+ <span v-else class="ew-val mono dim">{{ t('{n} series', { n: m.series.length }) }}</span>
</div>
<TimeChart
- :series="[{ label: m.label, data: m.values, unit: m.unit }]"
+ :series="m.series?.length
+ ? m.series.map((s) => ({ label: s.label || m.label, data: s.values, unit: m.unit }))
+ : [{ label: m.label, data: m.values, unit: m.unit }]"
:x-labels="edgeXLabels"
:height="150"
:unit="m.unit"
diff --git a/packages/api-client/src/topology.ts b/packages/api-client/src/topology.ts
index 3bc95d65..818386fa 100644
--- a/packages/api-client/src/topology.ts
+++ b/packages/api-client/src/topology.ts
@@ -179,8 +179,13 @@ export interface ProcessRelationMetric {
unit?: string;
/** Per-bucket values over the duration window (MINUTE step). */
values: Array<number | null>;
+ series?: Array<{ label?: string; values: Array<number | null> }>;
}
+/** The further results of a multi-result metric, each under its own labels —
+ * a percentile's ranks, a status-code metric's codes. Absent when the metric
+ * answers with one series, whose values are in `values`. */
+
/** Response of `POST /api/ebpf/network/process-relation-metrics`. */
export interface ProcessRelationMetricsResponse {
client: ProcessRelationMetric[];
(skywalking-horizon-ui) 01/02: feat(traces): query OAP's natively stored OTLP spans, and let a layer configure what it shows
Posted by wu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch feat/traceql-otlp-and-layer-config
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit 265274f2bf99d4235f768a73ecd6c91c82ce8b43
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Sun Sep 20 23:55:22 2026 +0800
feat(traces): query OAP's natively stored OTLP spans, and let a layer configure what it shows
OAP can now keep OTLP traces exactly as they arrive (apache/skywalking#14096)
and serve them on a TraceQL datasource of its own. Horizon exposes that as a
TraceQL - OTLP row, and reading it turned up several places where what the
admin showed and what the page rendered had drifted apart.
**The OTLP datasource.** A third TraceQL store, end to end: the config, the
sidebar row, the route, the schema panel, the Trace stores checklist and the
Cluster status pane. Its fields differ from the other two — `kind`,
`resource.service.instance.id` and Tempo's `span:` spellings exist only where
the spans were stored as OTLP — so the editor's schema is per-datasource and
warns accordingly.
**Attributes keep their types.** The BFF read `stringValue` and nothing else,
which was invisible while every datasource converted to strings on the way
out. Against `/otlp` it dropped every number and boolean — measured on a real
trace: seven `intValue` attributes including `http.status_code`, plus
`process.pid` and an `arrayValue`. It now decodes the whole `AnyValue`.
**One endpoint, not three.** `oap.traceql` took a full URL per datasource,
which said they could live on three servers; OAP binds one host and port and
serves each under a context path. It takes the endpoint once, and the paths
default to OAP's own, so setting the URL is enough. A datasource this OAP does
not enable answers 404 — reported as unreachable like any other failure, with
the reason in its own row rather than in a banner under the table.
**A trace list shows failures without reading every trace.** OAP projects a
`status` onto every search-result span; where it answers, the row is settled
at once, and the per-trace pass only reads what is left.
**Process-relation config lives in the layer template.** The panel was drawn
from sixteen built-in metrics no template named, so the editor showed an empty
configuration for a panel that was rendering. Mesh and Kubernetes Service now
carry the full 38 from `ui-initialized-templates`, including the HTTP/1.x
families that the old client/server split could not hold at all — a metric now
SAYS which end it describes instead of having it read off its name.
**Every layer-menu row opens what configures it.** Rows the editor could not
name were drawn as their raw path; the five trace rows now open Trace stores;
Pod logs, Continuous profiling, pprof, Evaluation records, Browser logs and
Conversations had no tab at all; and three profiling rows had a tab nothing was
wired to. The scope tabs no longer title-case their labels, which had been
rendering eBPF as EBPF.
**`oap.mqe` is gone.** Horizon talks to two OAP endpoints, not three: executing
an MQE expression is `execExpression` in the query protocol, which `queryUrl`
already serves. The override and its config-dump discovery are removed.
---
.../src/bundled_templates/layers/k8s_service.json | 354 +++++++++++++++-
apps/bff/src/bundled_templates/layers/mesh.json | 456 +++++++++++++++++++--
apps/bff/src/client/traceql.ts | 99 ++++-
apps/bff/src/config/schema.ts | 13 +-
apps/bff/src/http/admin/inspect.ts | 9 +-
apps/bff/src/http/query/ebpf.ts | 22 +-
apps/bff/src/http/query/traceql.test.ts | 101 ++++-
apps/bff/src/http/query/traceql.ts | 10 +-
apps/bff/src/logic/inspect/exec.ts | 5 +-
apps/bff/src/logic/layers/loader.ts | 45 +-
apps/bff/src/logic/templates/bundled-schema.ts | 14 +-
apps/bff/src/logic/traceql/availability.ts | 23 +-
apps/bff/src/logic/traceql/status.test.ts | 6 +-
apps/bff/src/util/mqe-target.test.ts | 104 -----
apps/bff/src/util/mqe-target.ts | 234 -----------
apps/bff/src/util/time.test.ts | 27 +-
apps/bff/src/util/time.ts | 11 +-
apps/ui/src/api/client.ts | 8 +
.../admin/layer-templates/LayerDashboardsAdmin.vue | 5 +-
.../admin/layer-templates/LayerSetupEditor.vue | 27 ++
.../admin/layer-templates/ProcessConfigEditor.vue | 112 ++---
.../admin/layer-templates/ScopeTabsBar.vue | 3 +-
.../admin/layer-templates/TraceStoresEditor.vue | 4 +-
.../layer-templates/layer-dashboards.scopes.ts | 63 ++-
.../features/operate/cluster/ClusterStatusView.vue | 84 ++--
apps/ui/src/i18n/locales/de.json | 32 +-
apps/ui/src/i18n/locales/en.json | 32 +-
apps/ui/src/i18n/locales/es.json | 32 +-
apps/ui/src/i18n/locales/fr.json | 32 +-
apps/ui/src/i18n/locales/ja.json | 32 +-
apps/ui/src/i18n/locales/ko.json | 32 +-
apps/ui/src/i18n/locales/pt.json | 32 +-
apps/ui/src/i18n/locales/zh-CN.json | 32 +-
.../layer/profiling/LayerNetworkProfilingView.vue | 10 +-
apps/ui/src/layer/traceql/LayerTraceQLView.vue | 6 +-
apps/ui/src/layer/traceql/TraceQLSchemaPanel.vue | 6 +-
apps/ui/src/layer/traceql/traceqlLint.test.ts | 26 +-
apps/ui/src/layer/traceql/traceqlLint.ts | 12 +-
apps/ui/src/monaco/setup.ts | 6 +-
apps/ui/src/monaco/traceql-grammar.ts | 19 +-
apps/ui/src/shell/SidebarLayerChildren.vue | 1 +
apps/ui/src/shell/layerFromTemplate.test.ts | 2 +-
apps/ui/src/shell/router/index.ts | 1 +
apps/ui/src/shell/useTopbarTimeContext.ts | 1 +
docs/changelog/1.1.0.md | 14 +-
docs/compatibility/cluster-status.md | 5 +-
docs/customization/layer-templates.md | 5 +-
docs/operate/traces.md | 13 +-
docs/setup/container-image.md | 1 -
docs/setup/oap.md | 36 +-
horizon.yaml | 14 +-
packages/api-client/src/index.ts | 2 +
packages/api-client/src/layer-menu.test.ts | 2 +-
packages/api-client/src/layer-menu.ts | 3 +
packages/api-client/src/topology.test.ts | 63 +++
packages/api-client/src/topology.ts | 39 +-
packages/api-client/src/trace.ts | 8 +-
packages/api-client/src/traceql.ts | 14 +-
58 files changed, 1650 insertions(+), 754 deletions(-)
diff --git a/apps/bff/src/bundled_templates/layers/k8s_service.json b/apps/bff/src/bundled_templates/layers/k8s_service.json
index e56dde86..687f63c2 100644
--- a/apps/bff/src/bundled_templates/layers/k8s_service.json
+++ b/apps/bff/src/bundled_templates/layers/k8s_service.json
@@ -79,7 +79,7 @@
"latest(k8s_service_pod_status_waiting)"
],
"tableHeaders": [
- "Container \u00b7 Pod \u00b7 Reason",
+ "Container · Pod · Reason",
""
],
"showTableValues": false,
@@ -552,20 +552,358 @@
],
"instanceTopology": {
"nodeMetrics": [
- { "id": "httpCpm", "label": "RPM", "mqe": "kubernetes_service_instance_http_call_cpm", "unit": "rpm", "role": "center", "aggregation": "avg" },
- { "id": "httpSuccess", "label": "Success Rate", "mqe": "kubernetes_service_instance_http_call_successful_rate/100", "unit": "%", "role": "ring", "aggregation": "avg", "thresholds": { "invertHealth": true, "invertBase": 100, "ok": 0.1, "warn": 1, "danger": 5 } },
- { "id": "httpRespTime", "label": "Latency", "mqe": "kubernetes_service_instance_http_call_time", "unit": "ms", "role": "secondary", "aggregation": "avg" }
+ {
+ "id": "httpCpm",
+ "label": "RPM",
+ "mqe": "kubernetes_service_instance_http_call_cpm",
+ "unit": "rpm",
+ "role": "center",
+ "aggregation": "avg"
+ },
+ {
+ "id": "httpSuccess",
+ "label": "Success Rate",
+ "mqe": "kubernetes_service_instance_http_call_successful_rate/100",
+ "unit": "%",
+ "role": "ring",
+ "aggregation": "avg",
+ "thresholds": {
+ "invertHealth": true,
+ "invertBase": 100,
+ "ok": 0.1,
+ "warn": 1,
+ "danger": 5
+ }
+ },
+ {
+ "id": "httpRespTime",
+ "label": "Latency",
+ "mqe": "kubernetes_service_instance_http_call_time",
+ "unit": "ms",
+ "role": "secondary",
+ "aggregation": "avg"
+ }
],
"linkServerMetrics": [
- { "id": "cpm", "label": "RPM", "mqe": "kubernetes_service_instance_relation_server_http_call_cpm", "unit": "rpm", "role": "lineServer", "aggregation": "avg" },
- { "id": "respTime", "label": "Avg response time", "mqe": "kubernetes_service_instance_relation_server_http_call_time", "unit": "ms", "aggregation": "avg" }
+ {
+ "id": "cpm",
+ "label": "RPM",
+ "mqe": "kubernetes_service_instance_relation_server_http_call_cpm",
+ "unit": "rpm",
+ "role": "lineServer",
+ "aggregation": "avg"
+ },
+ {
+ "id": "respTime",
+ "label": "Avg response time",
+ "mqe": "kubernetes_service_instance_relation_server_http_call_time",
+ "unit": "ms",
+ "aggregation": "avg"
+ }
],
"linkClientMetrics": [
- { "id": "cpm", "label": "RPM", "mqe": "kubernetes_service_instance_relation_client_http_call_cpm", "unit": "rpm", "role": "lineClient", "aggregation": "avg" },
- { "id": "respTime", "label": "Avg response time", "mqe": "kubernetes_service_instance_relation_client_http_call_time", "unit": "ms", "aggregation": "avg" }
+ {
+ "id": "cpm",
+ "label": "RPM",
+ "mqe": "kubernetes_service_instance_relation_client_http_call_cpm",
+ "unit": "rpm",
+ "role": "lineClient",
+ "aggregation": "avg"
+ },
+ {
+ "id": "respTime",
+ "label": "Avg response time",
+ "mqe": "kubernetes_service_instance_relation_client_http_call_time",
+ "unit": "ms",
+ "aggregation": "avg"
+ }
]
}
},
+ "processTopology": {
+ "edgeMetrics": [
+ {
+ "id": "client_write_cpm",
+ "label": "Write OP Per Minute",
+ "mqe": "process_relation_client_write_cpm",
+ "aggregation": "avg",
+ "side": "client"
+ },
+ {
+ "id": "server_write_cpm",
+ "label": "Write OP Per Minute",
+ "mqe": "process_relation_server_write_cpm",
+ "aggregation": "avg",
+ "side": "server"
+ },
+ {
+ "id": "client_read_cpm",
+ "label": "Read OP Per Minute",
+ "mqe": "process_relation_client_read_cpm",
+ "aggregation": "avg",
+ "side": "client"
+ },
+ {
+ "id": "server_read_cpm",
+ "label": "Read OP Per Minute",
+ "mqe": "process_relation_server_read_cpm",
+ "aggregation": "avg",
+ "side": "server"
+ },
+ {
+ "id": "client_write_total_bytes",
+ "label": "Write Package Size (Client)",
+ "mqe": "process_relation_client_write_total_bytes/1024",
+ "aggregation": "avg",
+ "unit": "KB",
+ "side": "client"
+ },
+ {
+ "id": "client_read_total_bytes",
+ "label": "Read Package Size (Client)",
+ "mqe": "process_relation_client_read_total_bytes/1024",
+ "aggregation": "avg",
+ "unit": "KB",
+ "side": "client"
+ },
+ {
+ "id": "server_write_total_bytes",
+ "label": "Write Package Size (Server)",
+ "mqe": "process_relation_server_write_total_bytes/1024",
+ "aggregation": "avg",
+ "unit": "KB",
+ "side": "server"
+ },
+ {
+ "id": "server_read_total_bytes",
+ "label": "Read Package Size (Server)",
+ "mqe": "process_relation_server_read_total_bytes/1024",
+ "aggregation": "avg",
+ "unit": "KB",
+ "side": "server"
+ },
+ {
+ "id": "client_write_avg_exe_time",
+ "label": "Write OP Avg Time (Client)",
+ "mqe": "process_relation_client_write_avg_exe_time/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "client"
+ },
+ {
+ "id": "client_read_avg_exe_time",
+ "label": "Read OP Avg Time (Client)",
+ "mqe": "process_relation_client_read_avg_exe_time/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "client"
+ },
+ {
+ "id": "server_write_avg_exe_time",
+ "label": "Write OP Avg Time (Server)",
+ "mqe": "process_relation_server_write_avg_exe_time/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "server"
+ },
+ {
+ "id": "server_read_avg_exe_time",
+ "label": "Read OP Avg Time (Server)",
+ "mqe": "process_relation_server_read_avg_exe_time/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "server"
+ },
+ {
+ "id": "client_connect_cpm",
+ "label": "Connect OP Per Minute",
+ "mqe": "process_relation_client_connect_cpm",
+ "aggregation": "avg",
+ "side": "client"
+ },
+ {
+ "id": "client_connect_exe_time",
+ "label": "Connect Avg Time (Client)",
+ "mqe": "process_relation_client_connect_exe_time/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "client"
+ },
+ {
+ "id": "server_connect_cpm",
+ "label": "Connect OP Per Minute",
+ "mqe": "process_relation_server_connect_cpm",
+ "aggregation": "avg",
+ "side": "server"
+ },
+ {
+ "id": "server_connect_avg_exe_time",
+ "label": "Connect Avg Time (Server)",
+ "mqe": "process_relation_server_connect_avg_exe_time/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "server"
+ },
+ {
+ "id": "client_close_cpm",
+ "label": "Close OP Per Minute",
+ "mqe": "process_relation_client_close_cpm",
+ "aggregation": "avg",
+ "side": "client"
+ },
+ {
+ "id": "client_close_avg_exe_time",
+ "label": "Close Avg Time (Client)",
+ "mqe": "process_relation_client_close_avg_exe_time/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "client"
+ },
+ {
+ "id": "server_close_cpm",
+ "label": "Close OP Per Minute",
+ "mqe": "process_relation_server_close_cpm",
+ "aggregation": "avg",
+ "side": "server"
+ },
+ {
+ "id": "server_close_avg_exe_time",
+ "label": "Close Avg Time (Server)",
+ "mqe": "process_relation_server_close_avg_exe_time/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "server"
+ },
+ {
+ "id": "client_write_rtt_time_percentile",
+ "label": "Write OP RTT Percentile (microsecond)",
+ "mqe": "process_relation_client_write_rtt_time_percentile",
+ "aggregation": "avg",
+ "side": "client"
+ },
+ {
+ "id": "server_write_rtt_time_percentile",
+ "label": "Write OP RTT Percentile (microsecond)",
+ "mqe": "process_relation_server_write_rtt_time_percentile",
+ "aggregation": "avg",
+ "side": "server"
+ },
+ {
+ "id": "client_write_exe_time_percentile",
+ "label": "Write OP Exe Time Percentile",
+ "mqe": "process_relation_client_write_exe_time_percentile/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "client"
+ },
+ {
+ "id": "server_write_exe_time_percentile",
+ "label": "Write OP Exe Time Percentile",
+ "mqe": "process_relation_server_write_exe_time_percentile/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "server"
+ },
+ {
+ "id": "client_read_exe_time_percentile",
+ "label": "Read OP Exe Time Percentile",
+ "mqe": "process_relation_client_read_exe_time_percentile/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "client"
+ },
+ {
+ "id": "server_read_exe_time_percentile",
+ "label": "Read OP Exe Time Percentile",
+ "mqe": "process_relation_server_read_exe_time_percentile/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "server"
+ },
+ {
+ "id": "client_write_avg_rtt_time",
+ "label": "Write OP Avg RTT (Client) (microsecond)",
+ "mqe": "process_relation_client_write_avg_rtt_time",
+ "aggregation": "avg",
+ "side": "client"
+ },
+ {
+ "id": "server_write_avg_rtt_time",
+ "label": "Write OP Avg RTT (Server) (microsecond)",
+ "mqe": "process_relation_server_write_avg_rtt_time",
+ "aggregation": "avg",
+ "side": "server"
+ },
+ {
+ "id": "http1_request_cpm",
+ "label": "Request CPM",
+ "mqe": "process_relation_http1_request_cpm",
+ "aggregation": "avg"
+ },
+ {
+ "id": "http1_response_status_cpm",
+ "label": "HTTP Response CPM",
+ "mqe": "process_relation_http1_response_status_cpm",
+ "aggregation": "avg"
+ },
+ {
+ "id": "http1_request_package_size",
+ "label": "Request Package Avg Size",
+ "mqe": "process_relation_http1_request_package_size/1024",
+ "aggregation": "avg",
+ "unit": "KB"
+ },
+ {
+ "id": "http1_response_package_size",
+ "label": "Response Package Avg Size",
+ "mqe": "process_relation_http1_response_package_size/1024",
+ "aggregation": "avg",
+ "unit": "KB"
+ },
+ {
+ "id": "http1_request_package_size_percentile",
+ "label": "Request Package Size Percentile",
+ "mqe": "process_relation_http1_request_package_size_percentile/1024",
+ "aggregation": "avg",
+ "unit": "KB"
+ },
+ {
+ "id": "http1_response_package_size_percentile",
+ "label": "Response Package Size Percentile",
+ "mqe": "process_relation_http1_response_package_size_percentile/1024",
+ "aggregation": "avg",
+ "unit": "KB"
+ },
+ {
+ "id": "http1_client_duration",
+ "label": "Response Avg Duration",
+ "mqe": "process_relation_http1_client_duration",
+ "aggregation": "avg",
+ "side": "client"
+ },
+ {
+ "id": "http1_client_duration_percentile",
+ "label": "Response Duration Percentile",
+ "mqe": "process_relation_http1_client_duration_percentile",
+ "aggregation": "avg",
+ "side": "client"
+ },
+ {
+ "id": "http1_server_duration",
+ "label": "Response Avg Duration",
+ "mqe": "process_relation_http1_server_duration",
+ "aggregation": "avg",
+ "side": "server"
+ },
+ {
+ "id": "http1_server_duration_percentile",
+ "label": "Response Duration Percentile",
+ "mqe": "process_relation_http1_server_duration_percentile",
+ "aggregation": "avg",
+ "side": "server"
+ }
+ ]
+ },
"traces": {
"source": "zipkin"
}
diff --git a/apps/bff/src/bundled_templates/layers/mesh.json b/apps/bff/src/bundled_templates/layers/mesh.json
index 10be32d8..578ea184 100644
--- a/apps/bff/src/bundled_templates/layers/mesh.json
+++ b/apps/bff/src/bundled_templates/layers/mesh.json
@@ -111,7 +111,7 @@
{
"id": "apdex_line",
"title": "Apdex",
- "tip": "User-satisfaction score on a 0 \u2013 1 scale.",
+ "tip": "User-satisfaction score on a 0 – 1 scale.",
"type": "line",
"expressions": [
"service_apdex/10000"
@@ -133,7 +133,7 @@
{
"id": "percentile_line",
"title": "Response Time Percentile",
- "tip": "p50 / p75 / p90 / p95 / p99 latency \u2014 the tail of the response-time distribution.",
+ "tip": "p50 / p75 / p90 / p95 / p99 latency — the tail of the response-time distribution.",
"type": "line",
"unit": "ms",
"expressions": [
@@ -145,7 +145,7 @@
{
"id": "throughput_line",
"title": "Service Throughput",
- "tip": "Bytes per minute through the sidecar \u2014 sent + received.",
+ "tip": "Bytes per minute through the sidecar — sent + received.",
"type": "line",
"unit": "B/min",
"expressions": [
@@ -238,7 +238,7 @@
{
"id": "instance_throughput",
"title": "Sidecar Throughput",
- "tip": "Bytes through the sidecar \u2014 received + sent.",
+ "tip": "Bytes through the sidecar — received + sent.",
"type": "line",
"unit": "B/min",
"expressions": [
@@ -276,7 +276,11 @@
"expressions": [
"envoy_cluster_up_rq_active"
],
- "visibleWhen": { "kind": "mqe", "expression": "envoy_cluster_up_rq_active", "op": "exists" },
+ "visibleWhen": {
+ "kind": "mqe",
+ "expression": "envoy_cluster_up_rq_active",
+ "op": "exists"
+ },
"span": 4,
"rowSpan": 2
},
@@ -288,19 +292,27 @@
"expressions": [
"envoy_cluster_up_rq_incr"
],
- "visibleWhen": { "kind": "mqe", "expression": "envoy_cluster_up_rq_incr", "op": "exists" },
+ "visibleWhen": {
+ "kind": "mqe",
+ "expression": "envoy_cluster_up_rq_incr",
+ "op": "exists"
+ },
"span": 4,
"rowSpan": 2
},
{
"id": "envoy_up_rq_pending_active",
"title": "Envoy Upstream Pending Active",
- "tip": "Pending upstream requests \u2014 indicates connection-pool back-pressure.",
+ "tip": "Pending upstream requests — indicates connection-pool back-pressure.",
"type": "line",
"expressions": [
"envoy_cluster_up_rq_pending_active"
],
- "visibleWhen": { "kind": "mqe", "expression": "envoy_cluster_up_rq_pending_active", "op": "exists" },
+ "visibleWhen": {
+ "kind": "mqe",
+ "expression": "envoy_cluster_up_rq_pending_active",
+ "op": "exists"
+ },
"span": 4,
"rowSpan": 2
},
@@ -311,7 +323,11 @@
"expressions": [
"envoy_cluster_up_cx_active"
],
- "visibleWhen": { "kind": "mqe", "expression": "envoy_cluster_up_cx_active", "op": "exists" },
+ "visibleWhen": {
+ "kind": "mqe",
+ "expression": "envoy_cluster_up_cx_active",
+ "op": "exists"
+ },
"span": 4,
"rowSpan": 2
},
@@ -322,19 +338,27 @@
"expressions": [
"envoy_cluster_up_cx_incr"
],
- "visibleWhen": { "kind": "mqe", "expression": "envoy_cluster_up_cx_incr", "op": "exists" },
+ "visibleWhen": {
+ "kind": "mqe",
+ "expression": "envoy_cluster_up_cx_incr",
+ "op": "exists"
+ },
"span": 4,
"rowSpan": 2
},
{
"id": "envoy_membership_healthy",
"title": "Envoy Cluster Healthy Membership",
- "tip": "Healthy upstream members per cluster \u2014 a non-trivial drop signals upstream churn.",
+ "tip": "Healthy upstream members per cluster — a non-trivial drop signals upstream churn.",
"type": "line",
"expressions": [
"envoy_cluster_membership_healthy"
],
- "visibleWhen": { "kind": "mqe", "expression": "envoy_cluster_membership_healthy", "op": "exists" },
+ "visibleWhen": {
+ "kind": "mqe",
+ "expression": "envoy_cluster_membership_healthy",
+ "op": "exists"
+ },
"span": 4,
"rowSpan": 2
},
@@ -350,7 +374,11 @@
"total",
"parent"
],
- "visibleWhen": { "kind": "mqe", "expression": "envoy_total_connections_used", "op": "exists" },
+ "visibleWhen": {
+ "kind": "mqe",
+ "expression": "envoy_total_connections_used",
+ "op": "exists"
+ },
"span": 4,
"rowSpan": 2
},
@@ -376,7 +404,11 @@
"physical",
"physical max"
],
- "visibleWhen": { "kind": "mqe", "expression": "envoy_heap_memory_used/1024/1024", "op": "exists" },
+ "visibleWhen": {
+ "kind": "mqe",
+ "expression": "envoy_heap_memory_used/1024/1024",
+ "op": "exists"
+ },
"span": 6,
"rowSpan": 2
},
@@ -392,7 +424,11 @@
"live",
"max"
],
- "visibleWhen": { "kind": "mqe", "expression": "envoy_worker_threads", "op": "exists" },
+ "visibleWhen": {
+ "kind": "mqe",
+ "expression": "envoy_worker_threads",
+ "op": "exists"
+ },
"span": 3,
"rowSpan": 2
},
@@ -404,7 +440,11 @@
"expressions": [
"envoy_bug_failures"
],
- "visibleWhen": { "kind": "mqe", "expression": "envoy_bug_failures", "op": "exists" },
+ "visibleWhen": {
+ "kind": "mqe",
+ "expression": "envoy_bug_failures",
+ "op": "exists"
+ },
"span": 3,
"rowSpan": 2
}
@@ -573,24 +613,386 @@
],
"instanceTopology": {
"nodeMetrics": [
- { "id": "cpm", "label": "RPM", "mqe": "service_instance_cpm", "unit": "rpm", "role": "center", "aggregation": "avg" },
- { "id": "sla", "label": "SLA", "mqe": "service_instance_sla/100", "unit": "%", "role": "ring", "aggregation": "avg", "thresholds": { "invertHealth": true, "invertBase": 100, "ok": 0.1, "warn": 1, "danger": 5 } },
- { "id": "respTime", "label": "Latency", "mqe": "service_instance_resp_time", "unit": "ms", "role": "secondary", "aggregation": "avg" }
+ {
+ "id": "cpm",
+ "label": "RPM",
+ "mqe": "service_instance_cpm",
+ "unit": "rpm",
+ "role": "center",
+ "aggregation": "avg"
+ },
+ {
+ "id": "sla",
+ "label": "SLA",
+ "mqe": "service_instance_sla/100",
+ "unit": "%",
+ "role": "ring",
+ "aggregation": "avg",
+ "thresholds": {
+ "invertHealth": true,
+ "invertBase": 100,
+ "ok": 0.1,
+ "warn": 1,
+ "danger": 5
+ }
+ },
+ {
+ "id": "respTime",
+ "label": "Latency",
+ "mqe": "service_instance_resp_time",
+ "unit": "ms",
+ "role": "secondary",
+ "aggregation": "avg"
+ }
],
"linkServerMetrics": [
- { "id": "cpm", "label": "RPM", "mqe": "service_instance_relation_server_cpm", "unit": "rpm", "role": "lineServer", "aggregation": "avg" },
- { "id": "respTime", "label": "Avg response time", "mqe": "service_instance_relation_server_resp_time", "unit": "ms", "aggregation": "avg" },
- { "id": "p95", "label": "p95", "mqe": "service_instance_relation_server_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
- { "id": "sla", "label": "SLA", "mqe": "service_instance_relation_server_call_sla/100", "unit": "%", "aggregation": "avg" }
+ {
+ "id": "cpm",
+ "label": "RPM",
+ "mqe": "service_instance_relation_server_cpm",
+ "unit": "rpm",
+ "role": "lineServer",
+ "aggregation": "avg"
+ },
+ {
+ "id": "respTime",
+ "label": "Avg response time",
+ "mqe": "service_instance_relation_server_resp_time",
+ "unit": "ms",
+ "aggregation": "avg"
+ },
+ {
+ "id": "p95",
+ "label": "p95",
+ "mqe": "service_instance_relation_server_percentile{p='95'}",
+ "unit": "ms",
+ "aggregation": "avg"
+ },
+ {
+ "id": "sla",
+ "label": "SLA",
+ "mqe": "service_instance_relation_server_call_sla/100",
+ "unit": "%",
+ "aggregation": "avg"
+ }
],
"linkClientMetrics": [
- { "id": "cpm", "label": "RPM", "mqe": "service_instance_relation_client_cpm", "unit": "rpm", "role": "lineClient", "aggregation": "avg" },
- { "id": "respTime", "label": "Avg response time", "mqe": "service_instance_relation_client_resp_time", "unit": "ms", "aggregation": "avg" },
- { "id": "p95", "label": "p95", "mqe": "service_instance_relation_client_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
- { "id": "sla", "label": "SLA", "mqe": "service_instance_relation_client_call_sla/100", "unit": "%", "aggregation": "avg" }
+ {
+ "id": "cpm",
+ "label": "RPM",
+ "mqe": "service_instance_relation_client_cpm",
+ "unit": "rpm",
+ "role": "lineClient",
+ "aggregation": "avg"
+ },
+ {
+ "id": "respTime",
+ "label": "Avg response time",
+ "mqe": "service_instance_relation_client_resp_time",
+ "unit": "ms",
+ "aggregation": "avg"
+ },
+ {
+ "id": "p95",
+ "label": "p95",
+ "mqe": "service_instance_relation_client_percentile{p='95'}",
+ "unit": "ms",
+ "aggregation": "avg"
+ },
+ {
+ "id": "sla",
+ "label": "SLA",
+ "mqe": "service_instance_relation_client_call_sla/100",
+ "unit": "%",
+ "aggregation": "avg"
+ }
]
}
},
+ "processTopology": {
+ "edgeMetrics": [
+ {
+ "id": "client_write_cpm",
+ "label": "Write OP Per Minute",
+ "mqe": "process_relation_client_write_cpm",
+ "aggregation": "avg",
+ "side": "client"
+ },
+ {
+ "id": "server_write_cpm",
+ "label": "Write OP Per Minute",
+ "mqe": "process_relation_server_write_cpm",
+ "aggregation": "avg",
+ "side": "server"
+ },
+ {
+ "id": "client_read_cpm",
+ "label": "Read OP Per Minute",
+ "mqe": "process_relation_client_read_cpm",
+ "aggregation": "avg",
+ "side": "client"
+ },
+ {
+ "id": "server_read_cpm",
+ "label": "Read OP Per Minute",
+ "mqe": "process_relation_server_read_cpm",
+ "aggregation": "avg",
+ "side": "server"
+ },
+ {
+ "id": "client_write_total_bytes",
+ "label": "Write Package Size (Client)",
+ "mqe": "process_relation_client_write_total_bytes/1024",
+ "aggregation": "avg",
+ "unit": "KB",
+ "side": "client"
+ },
+ {
+ "id": "client_read_total_bytes",
+ "label": "Read Package Size (Client)",
+ "mqe": "process_relation_client_read_total_bytes/1024",
+ "aggregation": "avg",
+ "unit": "KB",
+ "side": "client"
+ },
+ {
+ "id": "server_write_total_bytes",
+ "label": "Write Package Size (Server)",
+ "mqe": "process_relation_server_write_total_bytes/1024",
+ "aggregation": "avg",
+ "unit": "KB",
+ "side": "server"
+ },
+ {
+ "id": "server_read_total_bytes",
+ "label": "Read Package Size (Server)",
+ "mqe": "process_relation_server_read_total_bytes/1024",
+ "aggregation": "avg",
+ "unit": "KB",
+ "side": "server"
+ },
+ {
+ "id": "client_write_avg_exe_time",
+ "label": "Write OP Avg Time (Client)",
+ "mqe": "process_relation_client_write_avg_exe_time/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "client"
+ },
+ {
+ "id": "client_read_avg_exe_time",
+ "label": "Read OP Avg Time (Client)",
+ "mqe": "process_relation_client_read_avg_exe_time/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "client"
+ },
+ {
+ "id": "server_write_avg_exe_time",
+ "label": "Write OP Avg Time (Server)",
+ "mqe": "process_relation_server_write_avg_exe_time/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "server"
+ },
+ {
+ "id": "server_read_avg_exe_time",
+ "label": "Read OP Avg Time (Server)",
+ "mqe": "process_relation_server_read_avg_exe_time/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "server"
+ },
+ {
+ "id": "client_connect_cpm",
+ "label": "Connect OP Per Minute",
+ "mqe": "process_relation_client_connect_cpm",
+ "aggregation": "avg",
+ "side": "client"
+ },
+ {
+ "id": "client_connect_exe_time",
+ "label": "Connect Avg Time (Client)",
+ "mqe": "process_relation_client_connect_exe_time/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "client"
+ },
+ {
+ "id": "server_connect_cpm",
+ "label": "Connect OP Per Minute",
+ "mqe": "process_relation_server_connect_cpm",
+ "aggregation": "avg",
+ "side": "server"
+ },
+ {
+ "id": "server_connect_avg_exe_time",
+ "label": "Connect Avg Time (Server)",
+ "mqe": "process_relation_server_connect_avg_exe_time/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "server"
+ },
+ {
+ "id": "client_close_cpm",
+ "label": "Close OP Per Minute",
+ "mqe": "process_relation_client_close_cpm",
+ "aggregation": "avg",
+ "side": "client"
+ },
+ {
+ "id": "client_close_avg_exe_time",
+ "label": "Close Avg Time (Client)",
+ "mqe": "process_relation_client_close_avg_exe_time/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "client"
+ },
+ {
+ "id": "server_close_cpm",
+ "label": "Close OP Per Minute",
+ "mqe": "process_relation_server_close_cpm",
+ "aggregation": "avg",
+ "side": "server"
+ },
+ {
+ "id": "server_close_avg_exe_time",
+ "label": "Close Avg Time (Server)",
+ "mqe": "process_relation_server_close_avg_exe_time/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "server"
+ },
+ {
+ "id": "client_write_rtt_time_percentile",
+ "label": "Write OP RTT Percentile (microsecond)",
+ "mqe": "process_relation_client_write_rtt_time_percentile",
+ "aggregation": "avg",
+ "side": "client"
+ },
+ {
+ "id": "server_write_rtt_time_percentile",
+ "label": "Write OP RTT Percentile (microsecond)",
+ "mqe": "process_relation_server_write_rtt_time_percentile",
+ "aggregation": "avg",
+ "side": "server"
+ },
+ {
+ "id": "client_write_exe_time_percentile",
+ "label": "Write OP Exe Time Percentile",
+ "mqe": "process_relation_client_write_exe_time_percentile/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "client"
+ },
+ {
+ "id": "server_write_exe_time_percentile",
+ "label": "Write OP Exe Time Percentile",
+ "mqe": "process_relation_server_write_exe_time_percentile/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "server"
+ },
+ {
+ "id": "client_read_exe_time_percentile",
+ "label": "Read OP Exe Time Percentile",
+ "mqe": "process_relation_client_read_exe_time_percentile/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "client"
+ },
+ {
+ "id": "server_read_exe_time_percentile",
+ "label": "Read OP Exe Time Percentile",
+ "mqe": "process_relation_server_read_exe_time_percentile/1000000",
+ "aggregation": "avg",
+ "unit": "ms",
+ "side": "server"
+ },
+ {
+ "id": "client_write_avg_rtt_time",
+ "label": "Write OP Avg RTT (Client) (microsecond)",
+ "mqe": "process_relation_client_write_avg_rtt_time",
+ "aggregation": "avg",
+ "side": "client"
+ },
+ {
+ "id": "server_write_avg_rtt_time",
+ "label": "Write OP Avg RTT (Server) (microsecond)",
+ "mqe": "process_relation_server_write_avg_rtt_time",
+ "aggregation": "avg",
+ "side": "server"
+ },
+ {
+ "id": "http1_request_cpm",
+ "label": "Request CPM",
+ "mqe": "process_relation_http1_request_cpm",
+ "aggregation": "avg"
+ },
+ {
+ "id": "http1_response_status_cpm",
+ "label": "HTTP Response CPM",
+ "mqe": "process_relation_http1_response_status_cpm",
+ "aggregation": "avg"
+ },
+ {
+ "id": "http1_request_package_size",
+ "label": "Request Package Avg Size",
+ "mqe": "process_relation_http1_request_package_size/1024",
+ "aggregation": "avg",
+ "unit": "KB"
+ },
+ {
+ "id": "http1_response_package_size",
+ "label": "Response Package Avg Size",
+ "mqe": "process_relation_http1_response_package_size/1024",
+ "aggregation": "avg",
+ "unit": "KB"
+ },
+ {
+ "id": "http1_request_package_size_percentile",
+ "label": "Request Package Size Percentile",
+ "mqe": "process_relation_http1_request_package_size_percentile/1024",
+ "aggregation": "avg",
+ "unit": "KB"
+ },
+ {
+ "id": "http1_response_package_size_percentile",
+ "label": "Response Package Size Percentile",
+ "mqe": "process_relation_http1_response_package_size_percentile/1024",
+ "aggregation": "avg",
+ "unit": "KB"
+ },
+ {
+ "id": "http1_client_duration",
+ "label": "Response Avg Duration",
+ "mqe": "process_relation_http1_client_duration",
+ "aggregation": "avg",
+ "side": "client"
+ },
+ {
+ "id": "http1_client_duration_percentile",
+ "label": "Response Duration Percentile",
+ "mqe": "process_relation_http1_client_duration_percentile",
+ "aggregation": "avg",
+ "side": "client"
+ },
+ {
+ "id": "http1_server_duration",
+ "label": "Response Avg Duration",
+ "mqe": "process_relation_http1_server_duration",
+ "aggregation": "avg",
+ "side": "server"
+ },
+ {
+ "id": "http1_server_duration_percentile",
+ "label": "Response Duration Percentile",
+ "mqe": "process_relation_http1_server_duration_percentile",
+ "aggregation": "avg",
+ "side": "server"
+ }
+ ]
+ },
"traces": {
"source": "zipkin"
}
diff --git a/apps/bff/src/client/traceql.ts b/apps/bff/src/client/traceql.ts
index 15b4e6e2..be60ad28 100644
--- a/apps/bff/src/client/traceql.ts
+++ b/apps/bff/src/client/traceql.ts
@@ -52,6 +52,19 @@ import { attributesReportError } from '@skywalking-horizon-ui/api-client';
import { wireFetch } from './wire-log.js';
import type { HorizonConfig } from '../config/schema.js';
+/**
+ * Where a datasource answers: the one TraceQL endpoint plus that datasource's
+ * context path. An empty endpoint, or an empty path, means this deployment
+ * does not serve it — the caller reports that as "not configured" rather than
+ * probing a URL nobody enabled.
+ */
+export function traceqlUrlFor(cfg: HorizonConfig, ds: TraceQLDatasource): string {
+ const { url, nativePath, zipkinPath, otlpPath } = cfg.oap.traceql;
+ const path = { native: nativePath, zipkin: zipkinPath, otlp: otlpPath }[ds];
+ if (!url || !path) return '';
+ return `${url.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
+}
+
export interface TraceQLClientOpts {
/** Full base URL including OAP's context path, e.g. `http://oap:3200/skywalking`. */
baseUrl: string;
@@ -72,7 +85,7 @@ export function buildTraceQLOpts(
fetch?: FetchLike,
signal?: AbortSignal,
): TraceQLClientOpts | null {
- const baseUrl = ds === 'native' ? cfg.oap.traceql.nativeUrl : cfg.oap.traceql.zipkinUrl;
+ const baseUrl = traceqlUrlFor(cfg, ds);
if (!baseUrl) return null;
return {
baseUrl,
@@ -186,9 +199,27 @@ export function displayTraceId(ds: TraceQLDatasource, traceId: string): string {
/* ------------------------------------------------------------- wire shapes */
+/** OTLP/JSON `AnyValue`. Every field is optional and exactly one is set.
+ *
+ * Only the `/otlp` datasource uses more than `stringValue`: it serves the
+ * spans as the SDK exported them, so an attribute keeps its own type. The
+ * Zipkin and SkyWalking datasources convert everything to a string on the way
+ * out, which is why reading `stringValue` alone worked until `/otlp` existed
+ * — against it, every number and boolean simply vanished from the span. */
+interface WireAnyValue {
+ stringValue?: string;
+ /** int64, so proto3 JSON renders it as a decimal STRING, not a number. */
+ intValue?: string | number;
+ boolValue?: boolean;
+ doubleValue?: number;
+ bytesValue?: string;
+ arrayValue?: { values?: WireAnyValue[] };
+ kvlistValue?: { values?: Array<{ key?: string; value?: WireAnyValue }> };
+}
+
interface WireAttribute {
key: string;
- value?: { stringValue?: string };
+ value?: WireAnyValue;
}
interface WireSearchSpan {
@@ -245,13 +276,43 @@ export interface TraceQLBuildInfo {
version: string;
}
+/**
+ * One attribute value as text, whatever type it arrived as.
+ *
+ * The UI renders attributes as text and filters on them as text, so the type
+ * is flattened here rather than carried through every view. A number keeps its
+ * decimal spelling (an int64 already arrives as a string, and must not go
+ * through `Number` — beyond 2^53 that loses digits), a boolean becomes
+ * `true`/`false`, and the two composite kinds are rendered as JSON so a
+ * `http.request.header.accept` array reads as a list rather than as nothing.
+ *
+ * Returns undefined when no field is set, which is not the same as an empty
+ * string — see the span-status table on why that distinction matters.
+ */
+function attrText(v: WireAnyValue | undefined): string | undefined {
+ if (!v) return undefined;
+ if (v.stringValue !== undefined) return v.stringValue;
+ if (v.intValue !== undefined) return String(v.intValue);
+ if (v.boolValue !== undefined) return String(v.boolValue);
+ if (v.doubleValue !== undefined) return String(v.doubleValue);
+ if (v.bytesValue !== undefined) return v.bytesValue;
+ if (v.arrayValue) return JSON.stringify((v.arrayValue.values ?? []).map(attrText));
+ if (v.kvlistValue) {
+ return JSON.stringify(Object.fromEntries(
+ (v.kvlistValue.values ?? []).map((e) => [e.key ?? '', attrText(e.value)]),
+ ));
+ }
+ return undefined;
+}
+
/** Attributes as they arrive: a LIST, which may repeat a key. OAP emits
* `net.host.ip` twice when an endpoint has both IPv4 and IPv6, so folding
* this into an object silently keeps only the last one. */
function attrList(attrs: WireAttribute[] | undefined): Array<{ key: string; value: string }> {
const out: Array<{ key: string; value: string }> = [];
for (const a of attrs ?? []) {
- if (a.key && a.value?.stringValue !== undefined) out.push({ key: a.key, value: a.value.stringValue });
+ const value = attrText(a.value);
+ if (a.key && value !== undefined) out.push({ key: a.key, value });
}
return out;
}
@@ -259,7 +320,7 @@ function attrList(attrs: WireAttribute[] | undefined): Array<{ key: string; valu
/** First value for a key — for the few derived reads (service name, kind)
* where one value is what the caller means. */
function attrOf(attrs: WireAttribute[] | undefined, key: string): string | undefined {
- return attrs?.find((a) => a.key === key)?.value?.stringValue;
+ return attrText(attrs?.find((a) => a.key === key)?.value);
}
function num(raw: string | number | undefined): number {
@@ -323,16 +384,25 @@ function toRow(ds: TraceQLDatasource, t: WireSearchTrace, focus: string | null):
const traceStartNs = num(t.startTimeUnixNano);
const services: string[] = [];
let anyError = false;
+ /** Whether the response ANSWERED for the status, so an unknown row is told
+ * apart from one OAP called healthy — the list only re-reads the unknowns. */
+ let statusKnown = false;
let entry: TraceQLServiceEntry | undefined;
let entryStartNs: number | null = null;
for (const s of spans) {
const svc = attrOf(s.attributes, 'service.name');
if (svc && !services.includes(svc)) services.push(svc);
- // Which attributes reach a search result is an OAP setting, so failure is
- // read from whichever of the known markers the span happens to carry.
- // `emptyIsAbsent` because THIS response is padded: OAP writes every
- // projected key on every span of a span set, the missing ones as `""`.
- if (!anyError && attributesReportError(attrList(s.attributes), { emptyIsAbsent: true })) anyError = true;
+ // OAP projects a `status` onto every search-result span — `error`, `ok` or
+ // `unset` — so a list can show failures without reading each trace. Older
+ // OAPs send none, and then failure is read from whichever of the known
+ // markers the span happens to carry. `emptyIsAbsent` because THIS response
+ // is padded: OAP writes every projected key on every span of a span set,
+ // the missing ones as `""`.
+ const listed = attrOf(s.attributes, 'status');
+ if (listed === 'error') anyError = true;
+ else if (listed !== 'ok' && !anyError
+ && attributesReportError(attrList(s.attributes), { emptyIsAbsent: true })) anyError = true;
+ if (listed === 'error' || listed === 'ok') statusKnown = true;
if (focus && svc === focus) {
const startNs = num(s.startTimeUnixNano);
// The service's FIRST span — where it enters the trace. A service can be
@@ -359,11 +429,12 @@ function toRow(ds: TraceQLDatasource, t: WireSearchTrace, focus: string | null):
spanCount: spans.length,
services,
serviceEntry: entry,
- // A marker declares FAILURE only. No marker is not success: the result
- // tags are configurable upstream and a span can fail carrying none of
- // them, so the row stays unknown for the status pass — or for the reader —
- // to settle.
- isError: anyError ? true : undefined,
+ // `undefined` is UNKNOWN, and the list re-reads those traces one by one.
+ // OAP answering with a `status` settles the row here, which is the whole
+ // point of it projecting one. Without that, a marker declares FAILURE only:
+ // no marker is not success, since the result tags are configurable upstream
+ // and a span can fail carrying none of them.
+ isError: anyError ? true : statusKnown ? false : undefined,
};
}
diff --git a/apps/bff/src/config/schema.ts b/apps/bff/src/config/schema.ts
index e6a5caa8..51c62717 100644
--- a/apps/bff/src/config/schema.ts
+++ b/apps/bff/src/config/schema.ts
@@ -132,18 +132,13 @@ const oapSchema = z
})
.strict()
.optional(),
- mqe: z
- .object({
- host: z.string().optional(),
- port: z.number().int().positive().optional(),
- })
- .strict()
- .default({}),
zipkinUrl: z.string().url().default('http://127.0.0.1:9412/zipkin'),
traceql: z
.object({
- nativeUrl: z.string().url().or(z.literal('')).default(''),
- zipkinUrl: z.string().url().or(z.literal('')).default(''),
+ url: z.string().url().or(z.literal('')).default(''),
+ nativePath: z.string().default('/skywalking'),
+ zipkinPath: z.string().default('/zipkin'),
+ otlpPath: z.string().default('/otlp'),
})
.strict()
.default({}),
diff --git a/apps/bff/src/http/admin/inspect.ts b/apps/bff/src/http/admin/inspect.ts
index 074d1436..17280491 100644
--- a/apps/bff/src/http/admin/inspect.ts
+++ b/apps/bff/src/http/admin/inspect.ts
@@ -59,7 +59,6 @@ import { sessionHasVerb } from '../../rbac/policy.js';
import type { Session } from '../../user/sessions.js';
import { buildOapClients, type OapClients } from '../../client/index.js';
import { AttributionCache, attributeOrUnknown } from '../../logic/inspect/attribution.js';
-import { MqeTargetCache } from '../../util/mqe-target.js';
import { parseExecBody, fireMqe, MqeFireError } from '../../logic/inspect/exec.js';
import { ServerTimeCache } from '../../util/time.js';
import { wireFetch } from '../../client/wire-log.js';
@@ -91,7 +90,6 @@ export function registerInspectRoutes(app: FastifyInstance, deps: InspectRouteDe
* /api/inspect/catalog busts it explicitly for the SPA's manual
* refresh button. */
const attribution = new AttributionCache();
- const mqeTarget = new MqeTargetCache();
const serverTime = new ServerTimeCache();
function clients(): OapClients {
@@ -184,7 +182,6 @@ export function registerInspectRoutes(app: FastifyInstance, deps: InspectRouteDe
const value = await serverTime.get({
config: () => deps.config.current,
fetch: fetchImpl,
- mqeTarget,
});
return reply.send(value);
},
@@ -204,11 +201,7 @@ export function registerInspectRoutes(app: FastifyInstance, deps: InspectRouteDe
const cfg = deps.config.current;
const fetchImpl = wireFetch(deps.fetch ?? globalThis.fetch.bind(globalThis));
try {
- const target = await mqeTarget.resolve({
- config: () => cfg,
- fetch: fetchImpl,
- });
- const result = await fireMqe(target, body, {
+ const result = await fireMqe(cfg.oap.queryUrl, body, {
fetch: fetchImpl,
timeoutMs: cfg.oap.timeoutMs,
...(cfg.oap.auth ? { auth: cfg.oap.auth } : {}),
diff --git a/apps/bff/src/http/query/ebpf.ts b/apps/bff/src/http/query/ebpf.ts
index 10ab22e1..325d4710 100644
--- a/apps/bff/src/http/query/ebpf.ts
+++ b/apps/bff/src/http/query/ebpf.ts
@@ -50,6 +50,7 @@ import type {
TopologyMetricDef,
UITemplateClient,
} from '@skywalking-horizon-ui/api-client';
+import { resolveEdgeMetrics } from '@skywalking-horizon-ui/api-client';
import type { AuthDeps } from '../../user/middleware.js';
import { requireAuth } from '../../user/middleware.js';
import type { GraphqlOptions } from '../../client/graphql.js';
@@ -729,7 +730,7 @@ export function registerEBPFRoutes(app: FastifyInstance, deps: EBPFRouteDeps): v
previewConfig?: string;
}
| undefined;
- const payload: ProcessRelationMetricsResponse = { client: [], server: [], reachable: true };
+ const payload: ProcessRelationMetricsResponse = { client: [], server: [], shared: [], reachable: true };
const src = body?.source;
const dst = body?.dest;
if (!src?.processName || !dst?.processName) {
@@ -769,17 +770,13 @@ export function registerEBPFRoutes(app: FastifyInstance, deps: EBPFRouteDeps): v
const w = { start: fmtMinute(startMs, offset), end: fmtMinute(endMs, offset) };
// Build one aliased execExpression per metric across both sides.
- const aliasMap = new Map<string, { side: 'client' | 'server'; metric: TopologyMetricDef }>();
+ const aliasMap = new Map<string, { side?: 'client' | 'server'; metric: TopologyMetricDef }>();
const fragments: string[] = [];
- const push = (side: 'client' | 'server', list: TopologyMetricDef[]) => {
- list.forEach((m, i) => {
- const alias = `${side}_${i}`;
- aliasMap.set(alias, { side, metric: m });
- fragments.push(processRelationFragment(alias, m.mqe, src, dst, w, false));
- });
- };
- push('client', cfg.edgeClientMetrics);
- push('server', cfg.edgeServerMetrics);
+ resolveEdgeMetrics(cfg).forEach((m, i) => {
+ const alias = `m_${i}`;
+ aliasMap.set(alias, { ...(m.side ? { side: m.side } : {}), metric: m });
+ fragments.push(processRelationFragment(alias, m.mqe, src, dst, w, false));
+ });
if (fragments.length === 0) return reply.send(payload);
const query = `query HorizonProcessRelationMetrics {\n ${fragments.join('\n ')}\n}`;
@@ -794,7 +791,8 @@ export function registerEBPFRoutes(app: FastifyInstance, deps: EBPFRouteDeps): v
values: relationSeries(raw[alias]),
};
if (side === 'client') payload.client.push(out);
- else payload.server.push(out);
+ else if (side === 'server') payload.server.push(out);
+ else payload.shared.push(out);
}
return reply.send(payload);
} catch (err) {
diff --git a/apps/bff/src/http/query/traceql.test.ts b/apps/bff/src/http/query/traceql.test.ts
index cc37f5e5..52fe4acf 100644
--- a/apps/bff/src/http/query/traceql.test.ts
+++ b/apps/bff/src/http/query/traceql.test.ts
@@ -43,9 +43,13 @@ import {
} from '../../client/traceql.js';
import { resetTraceQLAvailability } from '../../logic/traceql/availability.js';
import { registerTraceQLRoutes } from './traceql.js';
+import { traceqlUrlFor } from '../../client/traceql.js';
-const NATIVE_URL = 'http://oap.test:3200/skywalking';
-const ZIPKIN_URL = 'http://oap.test:3200/zipkin';
+/** One TraceQL service, one context path per datasource — the shape OAP has. */
+const TRACEQL_URL = 'http://oap.test:3200';
+const NATIVE_URL = `${TRACEQL_URL}/skywalking`;
+const ZIPKIN_URL = `${TRACEQL_URL}/zipkin`;
+const OTLP_URL = `${TRACEQL_URL}/otlp`;
/** A real id from the demo, in both shapes OAP actually emits. */
const DOTTED = '7ac11a5bf004469780c148737a161cb2.38.17897214704563381';
@@ -55,8 +59,19 @@ function json(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
}
-function cfgWith(traceql: { nativeUrl?: string; zipkinUrl?: string }): ReturnType<typeof configSchema.parse> {
- return configSchema.parse({ oap: { traceql } });
+/** Tests name the datasources they expose; the config carries the endpoint
+ * once and a path each, so an absent one is a path this OAP does not serve. */
+function cfgWith(ds: { nativeUrl?: string; zipkinUrl?: string; otlpUrl?: string }): ReturnType<typeof configSchema.parse> {
+ return configSchema.parse({
+ oap: {
+ traceql: {
+ url: TRACEQL_URL,
+ nativePath: ds.nativeUrl ? '/skywalking' : '',
+ zipkinPath: ds.zipkinUrl ? '/zipkin' : '',
+ otlpPath: ds.otlpUrl ? '/otlp' : '',
+ },
+ },
+ });
}
function attr(key: string, value: string) {
@@ -235,12 +250,83 @@ describe('search: the window is seconds, and the rows are reduced', () => {
const [zipkinRow] = await traceqlSearch(zipkin, { startMs: 1, endMs: 2, limit: 5 });
expect(zipkinRow!.isError).toBe(true);
});
+
+ it('settles the row from the status OAP projects onto a search span', async () => {
+ // OAP writes `status` on every search-result span so a list can show
+ // failures without reading each trace. Without it the row stays unknown
+ // and the page reads the trace to find out.
+ const withStatus = (status: string): FetchLike => async () => {
+ const body = searchBody();
+ body.traces[0]!.spanSets[0]!.spans[0]!.attributes.push(attr('status', status));
+ return json(body);
+ };
+ const row = async (status: string) => {
+ const opts = buildTraceQLOpts(cfgWith({ otlpUrl: OTLP_URL }), 'otlp', withStatus(status))!;
+ const [r] = await traceqlSearch(opts, { startMs: 1, endMs: 2, limit: 5 });
+ return r!.isError;
+ };
+ expect(await row('error')).toBe(true);
+ // `ok` is an ANSWER, so the row is settled and nothing re-reads it.
+ expect(await row('ok')).toBe(false);
+ // `unset` is the absence of one, and leaves the row to the status pass.
+ expect(await row('unset')).toBeUndefined();
+ });
+
+ it('reads an attribute of any OTLP type, not only a string', async () => {
+ // The `/otlp` datasource serves the spans as the SDK exported them, so an
+ // attribute keeps its own type — `http.status_code` arrives as an int.
+ // Reading `stringValue` alone dropped every one of them.
+ const typed: FetchLike = async () => {
+ const body = searchBody();
+ body.traces[0]!.spanSets[0]!.spans[0]!.attributes.push(
+ { key: 'http.status_code', value: { intValue: '503' } } as never,
+ );
+ return json(body);
+ };
+ const opts = buildTraceQLOpts(cfgWith({ otlpUrl: OTLP_URL }), 'otlp', typed)!;
+ const [r] = await traceqlSearch(opts, { startMs: 1, endMs: 2, limit: 5 });
+ expect(r!.isError).toBe(true);
+ });
+});
+
+describe('where a datasource answers', () => {
+ const cfg = (traceql: Record<string, string>) => configSchema.parse({ oap: { traceql } });
+
+ it('joins the one endpoint with each datasource path', () => {
+ const c = cfg({ url: TRACEQL_URL, nativePath: '/skywalking', zipkinPath: '/zipkin', otlpPath: '/otlp' });
+ expect(traceqlUrlFor(c, 'native')).toBe(NATIVE_URL);
+ expect(traceqlUrlFor(c, 'zipkin')).toBe(ZIPKIN_URL);
+ expect(traceqlUrlFor(c, 'otlp')).toBe(OTLP_URL);
+ });
+
+ it('does not double the slash, whichever side carries it', () => {
+ expect(traceqlUrlFor(cfg({ url: 'http://oap.test:3200/', nativePath: '/skywalking' }), 'native'))
+ .toBe(NATIVE_URL);
+ expect(traceqlUrlFor(cfg({ url: TRACEQL_URL, nativePath: 'skywalking' }), 'native'))
+ .toBe(NATIVE_URL);
+ });
+
+ it('uses OAP\'s own context paths without being told them', () => {
+ // The paths are OAP's defaults, so setting the endpoint is enough. A
+ // datasource this OAP does not enable answers 404 and is reported as not
+ // served — which the probe decides, not the config.
+ const c = cfg({ url: TRACEQL_URL });
+ expect(traceqlUrlFor(c, 'native')).toBe(NATIVE_URL);
+ expect(traceqlUrlFor(c, 'zipkin')).toBe(ZIPKIN_URL);
+ expect(traceqlUrlFor(c, 'otlp')).toBe(OTLP_URL);
+ });
+
+ it('answers empty when there is nothing to ask', () => {
+ // No service configured at all, or a path deliberately blanked here.
+ expect(traceqlUrlFor(cfg({ nativePath: '/skywalking' }), 'native')).toBe('');
+ expect(traceqlUrlFor(cfg({ url: TRACEQL_URL, zipkinPath: '' }), 'zipkin')).toBe('');
+ });
});
describe('the routes', () => {
beforeEach(() => resetTraceQLAvailability());
- async function build(fetchImpl: FetchLike, traceql: { nativeUrl?: string; zipkinUrl?: string }) {
+ async function build(fetchImpl: FetchLike, traceql: { nativeUrl?: string; zipkinUrl?: string; otlpUrl?: string }) {
const cfg = cfgWith(traceql);
const config: ConfigSource = {
current: cfg,
@@ -267,6 +353,7 @@ describe('the routes', () => {
expect(sources).toMatchObject([
{ ds: 'native', configured: true },
{ ds: 'zipkin', configured: false, reachable: false },
+ { ds: 'otlp', configured: false, reachable: false },
]);
});
@@ -306,7 +393,9 @@ describe('the routes', () => {
const { app, sid } = await build(async () => json({}), { nativeUrl: NATIVE_URL });
const res = await app.inject({
method: 'GET',
- url: '/api/traceql/otlp/search?startMs=1&endMs=2',
+ // A name OAP has no datasource for. `otlp` used to stand here and is a
+ // real one now, which is the whole point of the route validating.
+ url: '/api/traceql/jaeger/search?startMs=1&endMs=2',
headers: { cookie: `horizon_sid=${sid}` },
});
expect(res.statusCode).toBe(400);
diff --git a/apps/bff/src/http/query/traceql.ts b/apps/bff/src/http/query/traceql.ts
index d5493d1e..0ea615df 100644
--- a/apps/bff/src/http/query/traceql.ts
+++ b/apps/bff/src/http/query/traceql.ts
@@ -69,7 +69,7 @@ export interface TraceQLRouteDeps extends AuthDeps {
}
function datasourceOf(raw: string): TraceQLDatasource | null {
- return raw === 'native' || raw === 'zipkin' ? raw : null;
+ return raw === 'native' || raw === 'zipkin' || raw === 'otlp' ? raw : null;
}
/** The window every query carries, in milliseconds. A by-id lookup may omit it
@@ -106,8 +106,14 @@ function failureText(e: unknown): string {
/** The store a datasource fills for a layer — how a `:ds` route reaches the
* per-store template settings. */
+const STORE_OF: Readonly<Record<TraceQLDatasource, TraceStore>> = {
+ native: 'traceql-native',
+ zipkin: 'traceql-zipkin',
+ otlp: 'traceql-otlp',
+};
+
function storeOf(ds: TraceQLDatasource): TraceStore {
- return ds === 'native' ? 'traceql-native' : 'traceql-zipkin';
+ return STORE_OF[ds];
}
/**
diff --git a/apps/bff/src/logic/inspect/exec.ts b/apps/bff/src/logic/inspect/exec.ts
index dc2b64bf..332a3494 100644
--- a/apps/bff/src/logic/inspect/exec.ts
+++ b/apps/bff/src/logic/inspect/exec.ts
@@ -34,7 +34,6 @@
import type { FastifyReply } from 'fastify';
import type { FetchLike, ExpressionResult, MqeEntity } from '@skywalking-horizon-ui/api-client';
import { INSPECT_STEPS, isInspectDate, type InspectStep } from '@skywalking-horizon-ui/api-client';
-import type { MqeTarget } from '../../util/mqe-target.js';
interface DurationInput {
start: string;
@@ -149,11 +148,11 @@ interface GraphQlEnvelope {
* `ExpressionResult` on success. Throws a {@link MqeFireError} with
* the GraphQL error array attached on failure. */
export async function fireMqe(
- target: MqeTarget,
+ queryUrl: string,
req: ExecBody,
deps: ExecDeps,
): Promise<ExpressionResult> {
- const url = `${target.baseUrl.replace(/\/$/, '')}/graphql`;
+ const url = `${queryUrl.replace(/\/$/, '')}/graphql`;
const payload = {
query: GRAPHQL_QUERY,
variables: {
diff --git a/apps/bff/src/logic/layers/loader.ts b/apps/bff/src/logic/layers/loader.ts
index 616844f5..0887bdfe 100644
--- a/apps/bff/src/logic/layers/loader.ts
+++ b/apps/bff/src/logic/layers/loader.ts
@@ -372,42 +372,6 @@ export const BOOSTER_ENDPOINT_DEP_DEFAULTS: EndpointDependencyConfig = {
],
};
-/**
- * Defaults for the network-profiling process-topology edge panel. The
- * metric names come from OAP's `meter-analyzer-config/network-profiling.yaml`
- * (metricPrefix `process_relation`), validated live against the demo's
- * mesh process topology (envoy → pilot-agent returns non-null cpm). OAP
- * observes each conversation from both eBPF probe sides, so client and
- * server families both exist. cpm metrics are per-minute rates; the
- * `*_total_bytes` are cumulative counters summed over the window.
- */
-export const BOOSTER_PROCESS_TOPOLOGY_DEFAULTS: ProcessTopologyConfig = {
- // Default ProcessRelation TCP metric set — fully operator-editable via
- // the admin network-profiling config. ids pair across client/server so
- // the edge dashboard renders matching rows side-by-side; bytes are
- // /1024 → KB and exe times /1000000 → ms for readable units.
- edgeClientMetrics: [
- { id: 'write_cpm', label: 'Write OP / min', mqe: 'process_relation_client_write_cpm', aggregation: 'avg' },
- { id: 'read_cpm', label: 'Read OP / min', mqe: 'process_relation_client_read_cpm', aggregation: 'avg' },
- { id: 'write_kb', label: 'Write package size', mqe: 'process_relation_client_write_total_bytes/1024', unit: 'KB', aggregation: 'avg' },
- { id: 'read_kb', label: 'Read package size', mqe: 'process_relation_client_read_total_bytes/1024', unit: 'KB', aggregation: 'avg' },
- { id: 'write_avg_ms', label: 'Write OP avg time', mqe: 'process_relation_client_write_avg_exe_time/1000000', unit: 'ms', aggregation: 'avg' },
- { id: 'read_avg_ms', label: 'Read OP avg time', mqe: 'process_relation_client_read_avg_exe_time/1000000', unit: 'ms', aggregation: 'avg' },
- { id: 'connect_cpm', label: 'Connect OP / min', mqe: 'process_relation_client_connect_cpm', aggregation: 'avg' },
- { id: 'close_cpm', label: 'Close OP / min', mqe: 'process_relation_client_close_cpm', aggregation: 'avg' },
- ],
- edgeServerMetrics: [
- { id: 'write_cpm', label: 'Write OP / min', mqe: 'process_relation_server_write_cpm', aggregation: 'avg' },
- { id: 'read_cpm', label: 'Read OP / min', mqe: 'process_relation_server_read_cpm', aggregation: 'avg' },
- { id: 'write_kb', label: 'Write package size', mqe: 'process_relation_server_write_total_bytes/1024', unit: 'KB', aggregation: 'avg' },
- { id: 'read_kb', label: 'Read package size', mqe: 'process_relation_server_read_total_bytes/1024', unit: 'KB', aggregation: 'avg' },
- { id: 'write_avg_ms', label: 'Write OP avg time', mqe: 'process_relation_server_write_avg_exe_time/1000000', unit: 'ms', aggregation: 'avg' },
- { id: 'read_avg_ms', label: 'Read OP avg time', mqe: 'process_relation_server_read_avg_exe_time/1000000', unit: 'ms', aggregation: 'avg' },
- { id: 'connect_cpm', label: 'Connect OP / min', mqe: 'process_relation_server_connect_cpm', aggregation: 'avg' },
- { id: 'close_cpm', label: 'Close OP / min', mqe: 'process_relation_server_close_cpm', aggregation: 'avg' },
- ],
-};
-
const __dirname = dirname(fileURLToPath(import.meta.url));
/** Locate bundled_templates/layers/ at runtime.
*
@@ -700,12 +664,15 @@ export function endpointDependencyConfigFor(
}
/** Resolve the process-topology (network-profiling) config — same
- * fallback rule. */
+ * template. */
export function processTopologyConfigFor(
template: LayerTemplate | null,
): ProcessTopologyConfig {
- if (template?.processTopology) return template.processTopology;
- return BOOSTER_PROCESS_TOPOLOGY_DEFAULTS;
+ // The template decides. There used to be a built-in default here, which
+ // meant the panel rendered sixteen metrics that no template named — so the
+ // editor, which reads the template, showed an empty config for a page that
+ // was full. A layer that declares none has none.
+ return template?.processTopology ?? {};
}
/**
diff --git a/apps/bff/src/logic/templates/bundled-schema.ts b/apps/bff/src/logic/templates/bundled-schema.ts
index 46244ff5..a0123643 100644
--- a/apps/bff/src/logic/templates/bundled-schema.ts
+++ b/apps/bff/src/logic/templates/bundled-schema.ts
@@ -346,10 +346,16 @@ function buildLayerSchemas(complete: boolean) {
})
.strict();
+ // One list, each metric naming its own side — or none, which the HTTP/1.x
+ // families need. The two side lists are still READ from stored templates
+ // and resolved into it; nothing writes them.
const processTopologySchema = z
.object({
- edgeClientMetrics: filled(topologyMetricSchema),
- edgeServerMetrics: filled(topologyMetricSchema),
+ edgeMetrics: filled(
+ topologyMetricSchema.extend({ side: z.enum(['client', 'server']).optional() }),
+ ).optional(),
+ edgeClientMetrics: filled(topologyMetricSchema).optional(),
+ edgeServerMetrics: filled(topologyMetricSchema).optional(),
})
.strict();
@@ -483,7 +489,7 @@ function buildLayerSchemas(complete: boolean) {
// The checklist. Absent or empty means the layer has NO trace rows —
// there is deliberately no default, because which trace stores exist
// is a fact about a deployment.
- sources: z.array(z.enum(['native', 'zipkin', 'traceql-native', 'traceql-zipkin'])).optional(),
+ sources: z.array(z.enum(['native', 'zipkin', 'traceql-native', 'traceql-zipkin', 'traceql-otlp'])).optional(),
// Per-store settings: the row's sidebar label, and the regex that
// narrows its service picker — the Tempo API has no notion of a
// layer, so a layer owning a subset of a store's services has no
@@ -491,7 +497,7 @@ function buildLayerSchemas(complete: boolean) {
stores: z
.object(
Object.fromEntries(
- (['native', 'zipkin', 'traceql-native', 'traceql-zipkin'] as const).map((k) => [
+ (['native', 'zipkin', 'traceql-native', 'traceql-zipkin', 'traceql-otlp'] as const).map((k) => [
k,
z
.object({
diff --git a/apps/bff/src/logic/traceql/availability.ts b/apps/bff/src/logic/traceql/availability.ts
index b1bfd4ce..54defbef 100644
--- a/apps/bff/src/logic/traceql/availability.ts
+++ b/apps/bff/src/logic/traceql/availability.ts
@@ -31,7 +31,7 @@
*/
import type { FetchLike, TraceQLDatasource, TraceQLSourceStatus } from '@skywalking-horizon-ui/api-client';
-import { buildTraceQLOpts, traceqlBuildInfo, TraceQLHttpError } from '../../client/traceql.js';
+import { buildTraceQLOpts, traceqlBuildInfo, traceqlUrlFor, TraceQLHttpError } from '../../client/traceql.js';
import type { HorizonConfig } from '../../config/schema.js';
const TTL_MS = 5 * 60_000;
@@ -50,7 +50,7 @@ interface CacheEntry {
* same URL, and a probe for one must not answer as the other. */
const cache = new Map<string, CacheEntry>();
-export const TRACEQL_DATASOURCES: readonly TraceQLDatasource[] = ['native', 'zipkin'];
+export const TRACEQL_DATASOURCES: readonly TraceQLDatasource[] = ['native', 'zipkin', 'otlp'];
function failureText(e: unknown): string {
if (e instanceof TraceQLHttpError) return e.message;
@@ -65,8 +65,9 @@ async function probeOne(
signal?: AbortSignal,
): Promise<TraceQLSourceStatus> {
const opts = buildTraceQLOpts(cfg, ds, fetch, signal);
- const url = ds === 'native' ? cfg.oap.traceql.nativeUrl : cfg.oap.traceql.zipkinUrl;
- if (!opts) return { ds, url: '', configured: false, reachable: false };
+ const url = traceqlUrlFor(cfg, ds);
+ const endpoint = cfg.oap.traceql.url;
+ if (!opts) return { ds, url: '', endpoint, configured: false, reachable: false };
const key = `${ds}\u0000${url}`;
const cached = cache.get(key);
@@ -76,12 +77,20 @@ async function probeOne(
let status: TraceQLSourceStatus;
try {
const info = await traceqlBuildInfo(opts);
- status = { ds, url, configured: true, reachable: true, version: info.version };
+ status = { ds, url, endpoint, configured: true, reachable: true, version: info.version };
} catch (e) {
// An abandoned read measured nothing. Report it, but do not write a
// conclusion nobody reached into a cache every other request reads.
- if (signal?.aborted) return { ds, url, configured: true, reachable: false, error: failureText(e) };
- status = { ds, url, configured: true, reachable: false, error: failureText(e) };
+ if (signal?.aborted) return { ds, url, endpoint, configured: true, reachable: false, error: failureText(e) };
+ // A 404 is the service answering that this datasource is not enabled —
+ // the operator has nothing to fix at the socket, so it must not read as
+ // an outage next to datasources that really are down.
+ const notServed = e instanceof TraceQLHttpError && e.status === 404;
+ status = {
+ ds, url, endpoint, configured: true, reachable: false,
+ ...(notServed ? { served: false } : {}),
+ error: failureText(e),
+ };
}
cache.set(key, { status, expiresAt: now + (status.reachable ? TTL_MS : FAILED_TTL_MS) });
return status;
diff --git a/apps/bff/src/logic/traceql/status.test.ts b/apps/bff/src/logic/traceql/status.test.ts
index bbea87ea..1b43dee5 100644
--- a/apps/bff/src/logic/traceql/status.test.ts
+++ b/apps/bff/src/logic/traceql/status.test.ts
@@ -28,7 +28,7 @@ import { configSchema } from '../../config/schema.js';
import { buildTraceQLOpts, encodeNativeTraceId } from '../../client/traceql.js';
import { fillTraceStatuses, statusTermIn, withStatusError } from './status.js';
-const NATIVE_URL = 'http://oap.test:3200/skywalking';
+const TRACEQL_URL = 'http://oap.test:3200';
function row(traceId: string, isError?: boolean): TraceQLTraceRow {
return {
@@ -63,7 +63,7 @@ function backend(ids: string[]) {
{ status: 200, headers: { 'content-type': 'application/json' } },
);
};
- const opts = buildTraceQLOpts(configSchema.parse({ oap: { traceql: { nativeUrl: NATIVE_URL } } }), 'native', fetch)!;
+ const opts = buildTraceQLOpts(configSchema.parse({ oap: { traceql: { url: TRACEQL_URL, nativePath: '/skywalking' } } }), 'native', fetch)!;
return { opts, asked };
}
@@ -133,7 +133,7 @@ describe('traceql trace-list status', () => {
it('costs the statuses, not the list, when the second search fails', async () => {
const fetch: FetchLike = async () => new Response('nope', { status: 500 });
const opts = buildTraceQLOpts(
- configSchema.parse({ oap: { traceql: { nativeUrl: NATIVE_URL } } }),
+ configSchema.parse({ oap: { traceql: { url: TRACEQL_URL, nativePath: '/skywalking' } } }),
'native',
fetch,
)!;
diff --git a/apps/bff/src/util/mqe-target.test.ts b/apps/bff/src/util/mqe-target.test.ts
deleted file mode 100644
index 0b200dac..00000000
--- a/apps/bff/src/util/mqe-target.test.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/**
- * Where the Inspect page sends an MQE expression.
- *
- * Normally: nowhere new. `execExpression` is a query-protocol query on the
- * same `/graphql` as everything else, so with no override the configured
- * `oap.queryUrl` IS the MQE surface — scheme, host, port and basic auth all
- * already line up. There is no version story here; MQE has been part of the
- * query protocol throughout.
- *
- * The config dump is read only for a PARTIAL override, where an operator gave
- * a host or a port but not both. That path reads OAP's own keys, so the key
- * NAMES are the contract — and they were read as `sharing-server.default.*`
- * when the module is `receiver-sharing-server`, so the lookup matched nothing
- * in any dump. The dumps below are the shape a live OAP returns (verified
- * against the public demo): every port is a string, and a listener that is off
- * is `0`.
- */
-
-import { describe, it, expect } from 'vitest';
-import { MqeTargetCache } from './mqe-target.js';
-import { configSchema } from '../config/schema.js';
-import type { FetchLike } from '@skywalking-horizon-ui/api-client';
-
-const ADMIN = 'http://oap.test:17128';
-const QUERY = 'https://oap.test:12800';
-
-function targetFor(dump: Record<string, string>, mqe?: { host?: string; port?: number }) {
- const fetch: FetchLike = async () =>
- new Response(JSON.stringify(dump), { status: 200, headers: { 'content-type': 'application/json' } });
- const config = configSchema.parse({
- oap: { queryUrl: QUERY, adminUrl: ADMIN, ...(mqe ? { mqe } : {}) },
- });
- return new MqeTargetCache().resolve({ config: () => config, fetch });
-}
-
-/** What OAP ships: the query GraphQL on core, the sharing server off. */
-const DEFAULT_DUMP = {
- 'core.default.restHost': '0.0.0.0',
- 'core.default.restPort': '12800',
- 'receiver-sharing-server.default.restHost': '0.0.0.0',
- 'receiver-sharing-server.default.restPort': '0',
-};
-
-describe('mqe target', () => {
- it('sends to the configured query endpoint, scheme and all', async () => {
- const t = await targetFor(DEFAULT_DUMP);
- // Not rediscovered: rebuilding it from the dump would drop the https and
- // hand back a wildcard bind host.
- expect(t.baseUrl).toBe(QUERY);
- expect(t.via).toBe('oap.queryUrl');
- });
-
- it('discovers the half an operator did not give', async () => {
- const t = await targetFor(DEFAULT_DUMP, { host: 'mqe.internal' });
- expect(t.baseUrl).toBe('http://mqe.internal:12800');
- expect(t.via).toContain('core');
- });
-
- it('follows the sharing server when an operator has turned it on', async () => {
- // The key is `receiver-sharing-server`; read as `sharing-server` it
- // matched nothing and this preference never applied.
- const t = await targetFor(
- { ...DEFAULT_DUMP, 'receiver-sharing-server.default.restPort': '12801' },
- { host: 'mqe.internal' },
- );
- expect(t.baseUrl).toBe('http://mqe.internal:12801');
- expect(t.via).toContain('receiver-sharing-server');
- });
-
- it('ignores a sharing server that is present but not listening', async () => {
- // `0` is how OAP spells "disabled"; reading it as a port would send every
- // expression somewhere nothing answers.
- const t = await targetFor(DEFAULT_DUMP, { host: 'mqe.internal' });
- expect(t.baseUrl).toBe('http://mqe.internal:12800');
- });
-
- it('reaches a wildcard bind through the admin URL host', async () => {
- const t = await targetFor(DEFAULT_DUMP, { port: 12800 });
- expect(t.baseUrl).toBe('http://oap.test:12800');
- });
-
- it('takes a full override without asking OAP anything', async () => {
- const t = await targetFor(DEFAULT_DUMP, { host: 'mqe.internal', port: 9999 });
- expect(t.baseUrl).toBe('http://mqe.internal:9999');
- expect(t.via).toContain('override');
- });
-});
diff --git a/apps/bff/src/util/mqe-target.ts b/apps/bff/src/util/mqe-target.ts
deleted file mode 100644
index 36dd3866..00000000
--- a/apps/bff/src/util/mqe-target.ts
+++ /dev/null
@@ -1,234 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/**
- * Resolve the GraphQL base URL for MQE (`execExpression`) calls.
- *
- * Resolution order:
- *
- * 1. Operator override in horizon.yaml under `oap.mqe.{host,port}`.
- * Both fields are independently optional — if only host is set,
- * port is discovered, and vice versa.
- *
- * 2. Admin-server `GET /debugging/config/dump` — a flat
- * `Map<String,String>` carrying `core.default.restPort`, which is
- * where OAP serves the query GraphQL, and
- * `receiver-sharing-server.default.restPort`, a second listener
- * that ships disabled (`0`) and that an operator may turn on to
- * move receiver + query traffic off the core port. Prefer the
- * sharing server only when it is actually listening.
- *
- * 3. Host fallback: if the discovered bind host is empty / wildcard
- * (`0.0.0.0`, `::`), reuse the admin URL's hostname. Port-forward
- * and ingress setups frequently bind on a wildcard but expose a
- * reachable address as the admin URL.
- *
- * The result is cached in-process for `CACHE_TTL_MS`. Cache is busted
- * by either elapsed time or an explicit `refresh()` call (driven by
- * the SPA's manual refresh button).
- */
-
-import type { FetchLike } from '@skywalking-horizon-ui/api-client';
-import type { HorizonConfig } from '../config/schema.js';
-
-export interface MqeTarget {
- /** e.g. `http://oap-rest.cluster.local:12800` — no trailing slash. */
- baseUrl: string;
- /** Human-readable rationale for the operator: `receiver-sharing-server`,
- * `core.restPort`, `horizon.yaml override`, `admin host fallback`,
- * combinations thereof. */
- via: string;
- /** What the operator's horizon.yaml had set (echo, not discovery). */
- configured: { host?: string; port?: number };
-}
-
-export interface ResolveDeps {
- config(): HorizonConfig;
- fetch: FetchLike;
-}
-
-const CACHE_TTL_MS = 60_000;
-
-interface CacheEntry {
- target: MqeTarget;
- expiresAt: number;
- /** The OAP settings this entry was resolved from. A config hot-reload that
- * repoints OAP must MISS rather than serve the old target for the rest of
- * the TTL — the same reason the capability and window caches key on the
- * config they read (see `logic/oap/capabilities.ts`). */
- key: string;
-}
-
-function cacheKey(cfg: HorizonConfig): string {
- const { queryUrl, adminUrl, mqe } = cfg.oap;
- return [queryUrl, adminUrl, mqe?.host ?? '', mqe?.port ?? ''].join('|');
-}
-
-export class MqeTargetCache {
- private entry: CacheEntry | null = null;
-
- invalidate(): void {
- this.entry = null;
- }
-
- async resolve(deps: ResolveDeps): Promise<MqeTarget> {
- const now = Date.now();
- const key = cacheKey(deps.config());
- if (this.entry && this.entry.key === key && this.entry.expiresAt > now) return this.entry.target;
- const target = await resolveMqeTarget(deps);
- this.entry = { target, expiresAt: now + CACHE_TTL_MS, key };
- return target;
- }
-}
-
-async function resolveMqeTarget(deps: ResolveDeps): Promise<MqeTarget> {
- const cfg = deps.config().oap;
- const configured: { host?: string; port?: number } = {};
- if (cfg.mqe?.host !== undefined) configured.host = cfg.mqe.host;
- if (cfg.mqe?.port !== undefined) configured.port = cfg.mqe.port;
-
- // Fast path: full override — no admin call needed.
- if (configured.host !== undefined && configured.port !== undefined) {
- return {
- baseUrl: `http://${configured.host}:${configured.port}`,
- via: 'horizon.yaml override (host + port)',
- configured,
- };
- }
-
- // No override: the configured GraphQL query endpoint IS the MQE
- // surface — `execExpression` is a query-protocol query served by the
- // same `/graphql` the rest of the app uses. Use `oap.queryUrl`
- // verbatim so scheme (http/https), host, port, and the basic-auth the
- // GraphQL client already carries all line up. Rediscovering a REST
- // host:port from the admin dump only rebuilds this same endpoint while
- // dropping the scheme + auth (it always emits `http://` and a bind
- // host that's often a wildcard / cluster-internal IP).
- if (configured.host === undefined && configured.port === undefined) {
- return { baseUrl: cfg.queryUrl.replace(/\/$/, ''), via: 'oap.queryUrl', configured };
- }
-
- // Partial override (only host OR only port) — discover the missing
- // half from the admin config dump and combine.
- const adminUrl = cfg.adminUrl;
- const dump = await fetchConfigDump(adminUrl, deps.fetch, cfg.timeoutMs, cfg.auth);
- const adminHost = new URL(adminUrl).hostname;
-
- const picked = pickFromDump(dump, adminHost);
-
- const finalHost = configured.host ?? picked.host;
- const finalPort = configured.port ?? picked.port;
-
- if (finalPort === undefined) {
- throw new Error(
- 'mqe target: could not discover REST port from /debugging/config/dump (neither the sharing server nor core REST appears in the dump)',
- );
- }
-
- const viaParts: string[] = [];
- viaParts.push(
- configured.host !== undefined ? 'host from horizon.yaml' : `host from ${picked.hostFrom}`,
- );
- viaParts.push(
- configured.port !== undefined ? 'port from horizon.yaml' : `port from ${picked.portFrom}`,
- );
-
- return {
- baseUrl: `http://${finalHost}:${finalPort}`,
- via: viaParts.join(', '),
- configured,
- };
-}
-
-interface PickResult {
- host: string;
- port: number | undefined;
- hostFrom: string;
- portFrom: string;
-}
-
-function pickFromDump(dump: Record<string, string>, adminHost: string): PickResult {
- // OAP serves the query GraphQL on CORE's REST listener, and that has not
- // moved between releases. The sharing server is a separate listener an
- // operator may turn on to take receiver + query traffic off that port; it
- // ships disabled (`restPort: 0`), and `parsePort` rejects 0, so this prefers
- // it only where it is actually listening.
- //
- // The module is `receiver-sharing-server`. Reading it as `sharing-server`
- // matched nothing in any config dump, so this branch never ran — harmless
- // while core is right, wrong for a deployment that moved the port.
- const sharingHost = dump['receiver-sharing-server.default.restHost'];
- const sharingPortStr = dump['receiver-sharing-server.default.restPort'];
- const coreHost = dump['core.default.restHost'];
- const corePortStr = dump['core.default.restPort'];
-
- const sharingPort = parsePort(sharingPortStr);
- const corePort = parsePort(corePortStr);
-
- const preferSharing = sharingPort !== undefined;
- const host = preferSharing ? sharingHost : coreHost;
- const port = preferSharing ? sharingPort : corePort;
- const moduleLabel = preferSharing ? 'receiver-sharing-server.restHost' : 'core.restHost';
- const portLabel = preferSharing ? 'receiver-sharing-server.restPort' : 'core.restPort';
-
- // Wildcard host fallback. OAP commonly binds on 0.0.0.0; the operator
- // reaches it through the admin URL's host.
- const isWildcard = !host || host === '0.0.0.0' || host === '::' || host === '';
- const resolvedHost = isWildcard ? adminHost : host;
- const hostFrom = isWildcard ? `admin URL host (${moduleLabel} was wildcard)` : moduleLabel;
-
- return { host: resolvedHost, port, hostFrom, portFrom: portLabel };
-}
-
-function parsePort(raw: string | undefined): number | undefined {
- if (raw === undefined || raw === '') return undefined;
- const n = Number.parseInt(raw, 10);
- if (!Number.isFinite(n) || n <= 0 || n > 65535) return undefined;
- return n;
-}
-
-async function fetchConfigDump(
- adminUrl: string,
- fetch: FetchLike,
- timeoutMs: number,
- auth?: { username: string; password: string },
-): Promise<Record<string, string>> {
- const base = adminUrl.replace(/\/$/, '');
- const url = `${base}/debugging/config/dump`;
- const headers: Record<string, string> = { Accept: 'application/json' };
- if (auth) {
- const b64 = Buffer.from(`${auth.username}:${auth.password}`, 'utf8').toString('base64');
- headers.authorization = `Basic ${b64}`;
- }
- let init: RequestInit = { method: 'GET', headers };
- let timer: ReturnType<typeof setTimeout> | null = null;
- if (timeoutMs > 0) {
- const ctrl = new AbortController();
- timer = setTimeout(() => ctrl.abort(), timeoutMs);
- init = { ...init, signal: ctrl.signal };
- }
- try {
- const res = await fetch(url, init);
- if (!res.ok) {
- const body = await res.text();
- throw new Error(`config dump failed: HTTP ${res.status} from ${url} — ${body.slice(0, 200)}`);
- }
- return (await res.json()) as Record<string, string>;
- } finally {
- if (timer) clearTimeout(timer);
- }
-}
diff --git a/apps/bff/src/util/time.test.ts b/apps/bff/src/util/time.test.ts
index daafbcc4..7d53c602 100644
--- a/apps/bff/src/util/time.test.ts
+++ b/apps/bff/src/util/time.test.ts
@@ -24,7 +24,6 @@ import {
} from './time.js';
import { fmtMinute } from './window.js';
import type { HorizonConfig } from '../config/schema.js';
-import type { MqeTargetCache } from './mqe-target.js';
import type { FetchLike } from '@skywalking-horizon-ui/api-client';
afterEach(() => {
@@ -138,15 +137,8 @@ function makeDeps(
lastInit = init;
return respond(init);
};
- const mqeTarget = {
- resolve: async () => ({
- baseUrl: opts.baseUrl ?? 'http://oap:12800',
- via: 'test',
- configured: {},
- }),
- } as unknown as MqeTargetCache;
return {
- deps: { config: () => cfg, fetch, mqeTarget },
+ deps: { config: () => cfg, fetch },
calls: () => calls,
lastInit: () => lastInit,
lastUrl: () => lastUrl,
@@ -277,23 +269,6 @@ describe('ServerTimeCache — resolving the OAP clock', () => {
expect(out.error).toContain('not parseable');
});
- it('falls back when the MQE target cannot be resolved at all', async () => {
- const p = makeDeps(async () => jsonResponse(okBody('+0800')));
- const deps: ServerTimeDeps = {
- ...p.deps,
- mqeTarget: {
- resolve: async () => {
- throw new Error('admin dump unreachable');
- },
- } as unknown as MqeTargetCache,
- };
- const out = await new ServerTimeCache().get(deps);
- expect(out.source).toBe('fallback');
- expect(out.error).toContain('admin dump unreachable');
- expect(out.mqeBaseUrl).toBeUndefined();
- expect(p.calls()).toBe(0);
- });
-
it('reports the BFF’s own clock on a fallback, never a silent UTC zero', async () => {
// The fallback is documented as "the local BFF clock". Returning 0 here
// would be indistinguishable from a genuine UTC server on the wire.
diff --git a/apps/bff/src/util/time.ts b/apps/bff/src/util/time.ts
index 6a174ccf..bcd489ff 100644
--- a/apps/bff/src/util/time.ts
+++ b/apps/bff/src/util/time.ts
@@ -34,7 +34,6 @@
import type { FetchLike } from '@skywalking-horizon-ui/api-client';
import type { HorizonConfig } from '../config/schema.js';
-import type { MqeTargetCache } from './mqe-target.js';
export interface ServerTime {
/** OAP server's UTC offset in minutes. `+480` for UTC+8, `-300`
@@ -84,7 +83,6 @@ export class ServerTimeCache {
export interface ServerTimeDeps {
config(): HorizonConfig;
fetch: FetchLike;
- mqeTarget: MqeTargetCache;
}
async function resolveServerTime(deps: ServerTimeDeps): Promise<ServerTime> {
@@ -96,11 +94,8 @@ async function resolveServerTime(deps: ServerTimeDeps): Promise<ServerTime> {
const ctrl = timeoutMs > 0 ? new AbortController() : null;
const timer = ctrl ? setTimeout(() => ctrl.abort(), timeoutMs) : null;
try {
- const target = await deps.mqeTarget.resolve({
- config: () => deps.config(),
- fetch: deps.fetch,
- });
- mqeBaseUrl = target.baseUrl;
+ const queryUrl = deps.config().oap.queryUrl;
+ mqeBaseUrl = queryUrl;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Accept: 'application/json',
@@ -116,7 +111,7 @@ async function resolveServerTime(deps: ServerTimeDeps): Promise<ServerTime> {
body: JSON.stringify({ query: GRAPHQL_QUERY }),
...(ctrl ? { signal: ctrl.signal } : {}),
};
- const res = await deps.fetch(`${target.baseUrl.replace(/\/$/, '')}/graphql`, init);
+ const res = await deps.fetch(`${queryUrl.replace(/\/$/, '')}/graphql`, init);
if (!res.ok) {
const txt = (await res.text()).slice(0, 200);
return fallback(`HTTP ${res.status}: ${txt}`, mqeBaseUrl);
diff --git a/apps/ui/src/api/client.ts b/apps/ui/src/api/client.ts
index c7521999..d608576a 100644
--- a/apps/ui/src/api/client.ts
+++ b/apps/ui/src/api/client.ts
@@ -380,6 +380,14 @@ export interface AdminLayerTemplate {
* three targets as `ebpfProfiling`, so it belongs on the layers that
* carry it — there is no continuous trace / async / pprof profiling. */
continuousProfiling?: boolean;
+ // These three are in the stored template and drive a sidebar row, and
+ // were missing here — so the editor had no checkbox for them and a layer
+ // carrying one showed a row the operator could neither explain nor turn
+ // off. `mesh` and `k8s_service` carry networkProfiling, `general` pprof,
+ // `ai_agent` conversations.
+ networkProfiling?: boolean;
+ pprofProfiling?: boolean;
+ aiConversations?: boolean;
};
metrics: {
orderBy?: string;
diff --git a/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue b/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
index 03d019b8..e8c942f4 100644
--- a/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
+++ b/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
@@ -738,7 +738,7 @@ const namingTest = computed<NamingTestResult>(() => {
the Components toggle in the right sidebar. -->
<section
v-else-if="activeScopeRuntimeOnly"
- class="sw-card editor-card topo-cfg-card"
+ class="sw-card editor-card topo-cfg-card scope-titled"
>
<div class="card-head">
<h4>{{ t('{scope} tab', { scope: scopeLabel(activeScope) }) }}</h4>
@@ -1092,6 +1092,9 @@ const namingTest = computed<NamingTestResult>(() => {
color: var(--sw-fg-0);
text-transform: capitalize;
}
+/* A heading built from a scope label carries the label's own casing — see
+ SCOPE_LABELS. Capitalizing it renders eBPF as EBPF. */
+.scope-titled .card-head h4 { text-transform: none; }
.card-head .sub {
font-size: 10.5px;
color: var(--sw-fg-3);
diff --git a/apps/ui/src/features/admin/layer-templates/LayerSetupEditor.vue b/apps/ui/src/features/admin/layer-templates/LayerSetupEditor.vue
index 8256a2ee..86bbfee9 100644
--- a/apps/ui/src/features/admin/layer-templates/LayerSetupEditor.vue
+++ b/apps/ui/src/features/admin/layer-templates/LayerSetupEditor.vue
@@ -79,9 +79,12 @@ const COMPONENT_TOGGLES = computed((): Array<{ key: ComponentKey; label: string;
{ key: 'evaluationRecord', label: t('Evaluation Record'), hint: t('GenAI evaluation record explorer scoped to this layer.') },
{ key: 'browserErrors', label: t('Browser Logs'), hint: t('BROWSER-layer JS error logs with source-map de-obfuscation of the minified stack.') },
{ key: 'podLogs', label: t('Pod Logs'), hint: t('On-demand Kubernetes pod-log live tail. Only K8s-deployed layers (k8s_service, mesh) carry pods that resolve.') },
+ { key: 'aiConversations', label: t('Conversations'), hint: t('AI agent conversations recorded against this layer’s services.') },
{ key: 'traceProfiling', label: t('Trace Profiling'), hint: t('Trace-driven thread profiling — the original SkyWalking profile.') },
{ key: 'ebpfProfiling', label: t('eBPF Profiling'), hint: t('Kernel-level CPU / off-CPU profiling via eBPF agents.') },
+ { key: 'networkProfiling', label: t('Network Profiling'), hint: t('Process-to-process network profiling via the Rover eBPF agent.') },
{ key: 'asyncProfiling', label: t('Async Profiling'), hint: t('JVM async-profiler integration (Java-only).') },
+ { key: 'pprofProfiling', label: t('pprof (Go)'), hint: t('Go runtime pprof profiles (Go-only).') },
{ key: 'continuousProfiling', label: t('Continuous Profiling'), hint: t('Auto-trigger policies — rules that make an eBPF agent start an ON_CPU / OFF_CPU / NETWORK task by itself. Needs the same Rover agent as eBPF Profiling; there is no continuous trace / async / pprof profiling.') },
]);
@@ -191,7 +194,26 @@ const ROW_TO_ADMIN_SCOPE: Record<string, AdminScope | undefined> = {
dependency: 'dependency',
trace: 'trace',
logs: 'logs',
+ // EVERY row opens the tab that configures it. A row that opened nothing was
+ // a button the operator had no way to read as a label.
+ //
+ // All five trace rows share ONE tab: what a layer configures about traces is
+ // which stores it exposes and how each is named, which is one checklist, not
+ // one panel per store.
+ 'zipkin-trace': 'trace',
+ 'traceql-native-trace': 'trace',
+ 'traceql-zipkin-trace': 'trace',
+ 'traceql-otlp-trace': 'trace',
+ 'evaluation-record': 'evaluationRecord',
+ 'browser-errors': 'browserErrors',
+ 'pod-logs': 'podLogs',
+ conversations: 'aiConversations',
+ 'trace-profiling': 'traceProfiling',
+ 'ebpf-profiling': 'ebpfProfiling',
'network-profiling': 'networkProfiling',
+ 'continuous-profiling': 'continuousProfiling',
+ pprof: 'pprofProfiling',
+ 'async-profiling': 'asyncProfiling',
};
/** The order this layer resolves to with NO stored arrangement — what
@@ -259,7 +281,12 @@ function labelForRow(path: string): string {
case 'zipkin-trace': return storeName('zipkin') || t('Zipkin Traces');
case 'traceql-native-trace': return storeName('traceql-native') || t('TraceQL - Native');
case 'traceql-zipkin-trace': return storeName('traceql-zipkin') || t('TraceQL - Zipkin');
+ case 'traceql-otlp-trace': return storeName('traceql-otlp') || t('TraceQL - OTLP');
case 'logs': return t('Logs');
+ // Rows whose name was never added here fell through to `default` and were
+ // drawn as their raw path — `evaluation-record` rather than its name.
+ case 'evaluation-record': return t('Evaluation records');
+ case 'conversations': return t('Conversations');
case 'browser-errors': return t('Browser Logs');
case 'pod-logs': return t('Pod Logs');
case 'trace-profiling': return t('Trace Profiling');
diff --git a/apps/ui/src/features/admin/layer-templates/ProcessConfigEditor.vue b/apps/ui/src/features/admin/layer-templates/ProcessConfigEditor.vue
index a9341428..e7402392 100644
--- a/apps/ui/src/features/admin/layer-templates/ProcessConfigEditor.vue
+++ b/apps/ui/src/features/admin/layer-templates/ProcessConfigEditor.vue
@@ -24,7 +24,14 @@
<script setup lang="ts">
import { computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
-import type { ProcessTopologyConfig, TopologyMetricDef } from '@skywalking-horizon-ui/api-client';
+import type {
+ ProcessRelationSide,
+ ProcessTopologyConfig,
+ TopologyMetricDef,
+} from '@skywalking-horizon-ui/api-client';
+import { resolveEdgeMetrics } from '@skywalking-horizon-ui/api-client';
+
+type EdgeMetric = TopologyMetricDef & { side?: ProcessRelationSide };
import { nextFreeId } from './free-id';
import MetricDefinitionRow from './MetricDefinitionRow.vue';
import { rowKey } from './row-key';
@@ -34,30 +41,37 @@ const { t } = useI18n({ useScope: 'global' });
const config = defineModel<ProcessTopologyConfig | undefined>('config');
const props = defineProps<{ layerKey?: string; readOnly?: boolean }>();
-function ensure(): ProcessTopologyConfig {
- if (!config.value) config.value = { edgeClientMetrics: [], edgeServerMetrics: [] };
- if (!config.value.edgeClientMetrics) config.value.edgeClientMetrics = [];
- if (!config.value.edgeServerMetrics) config.value.edgeServerMetrics = [];
- return config.value;
+/** Opens on the RESOLVED list, so a template still written as two side lists
+ * is edited as the one list it means — and the first edit writes that form
+ * out, as the trace-store checklist does. */
+function ensure(): ProcessTopologyConfig & { edgeMetrics: EdgeMetric[] } {
+ if (!config.value) config.value = {};
+ if (!config.value.edgeMetrics) {
+ config.value = { edgeMetrics: resolveEdgeMetrics(config.value) };
+ }
+ return config.value as ProcessTopologyConfig & { edgeMetrics: EdgeMetric[] };
}
onMounted(() => {
if (!props.readOnly) ensure();
});
-const clientMetrics = computed(() => config.value?.edgeClientMetrics ?? []);
-const serverMetrics = computed(() => config.value?.edgeServerMetrics ?? []);
+const edgeMetrics = computed<EdgeMetric[]>(() => resolveEdgeMetrics(config.value));
function blankMetric(taken: readonly TopologyMetricDef[]): TopologyMetricDef {
const id = nextFreeId('metric_', taken.map((m) => m.id));
return { id, label: `Metric ${id.slice('metric_'.length)}`, mqe: '', unit: '', aggregation: 'avg' };
}
-function addClient(): void {
+function addMetric(side?: ProcessRelationSide): void {
if (props.readOnly) return;
- ensure().edgeClientMetrics.push(blankMetric(clientMetrics.value));
+ const m = blankMetric(edgeMetrics.value);
+ ensure().edgeMetrics.push(side ? { ...m, side } : m);
}
-function addServer(): void {
+function setSide(i: number, raw: string): void {
if (props.readOnly) return;
- ensure().edgeServerMetrics.push(blankMetric(serverMetrics.value));
+ const m = ensure().edgeMetrics[i];
+ if (!m) return;
+ if (raw === 'client' || raw === 'server') m.side = raw;
+ else delete m.side;
}
function move(list: TopologyMetricDef[], i: number, dir: -1 | 1): void {
if (props.readOnly) return;
@@ -80,51 +94,39 @@ function remove(list: TopologyMetricDef[], i: number): void {
<div class="topo-cfg-body">
<div class="topo-cfg-section">
<header class="topo-cfg-head">
- <h5>{{ t('Client-side metrics') }}</h5>
- <span class="sub">{{ t('edge metrics queried as') }} <code>process_relation_client_*</code></span>
- <button class="sw-btn add" type="button" :disabled="readOnly" @click="addClient">{{ t('+ Add') }}</button>
- </header>
- <div v-if="clientMetrics.length === 0" class="topo-cfg-empty">{{ t('No client-side metrics.') }}</div>
- <div v-else class="metric-list">
- <MetricDefinitionRow
- v-for="(m, i) in clientMetrics"
- :key="rowKey(m)"
- v-model:metric="clientMetrics[i]"
- :layer-key="layerKey"
- :read-only="readOnly"
- site-scope="process-relation"
- mqe-placeholder="process_relation_client_write_cpm"
- :can-move-up="i > 0"
- :can-move-down="i < clientMetrics.length - 1"
- @move-up="move(clientMetrics, i, -1)"
- @move-down="move(clientMetrics, i, 1)"
- @remove="remove(clientMetrics, i)"
- />
- </div>
- </div>
-
- <div class="topo-cfg-section">
- <header class="topo-cfg-head">
- <h5>{{ t('Server-side metrics') }}</h5>
- <span class="sub">{{ t('edge metrics queried as') }} <code>process_relation_server_*</code></span>
- <button class="sw-btn add" type="button" :disabled="readOnly" @click="addServer">{{ t('+ Add') }}</button>
+ <h5>{{ t('Edge metrics') }}</h5>
+ <span class="sub">{{ t('Each names the end of the conversation it describes. The eBPF probe watches both, and a metric that describes the exchange itself — the HTTP/1.x families — belongs to neither.') }}</span>
+ <button class="sw-btn add" type="button" :disabled="readOnly" @click="addMetric('client')">{{ t('+ Add') }}</button>
</header>
- <div v-if="serverMetrics.length === 0" class="topo-cfg-empty">{{ t('No server-side metrics.') }}</div>
+ <div v-if="edgeMetrics.length === 0" class="topo-cfg-empty">{{ t('No edge metrics.') }}</div>
<div v-else class="metric-list">
- <MetricDefinitionRow
- v-for="(m, i) in serverMetrics"
- :key="rowKey(m)"
- v-model:metric="serverMetrics[i]"
- :layer-key="layerKey"
- :read-only="readOnly"
- site-scope="process-relation"
- mqe-placeholder="process_relation_server_write_cpm"
- :can-move-up="i > 0"
- :can-move-down="i < serverMetrics.length - 1"
- @move-up="move(serverMetrics, i, -1)"
- @move-down="move(serverMetrics, i, 1)"
- @remove="remove(serverMetrics, i)"
- />
+ <div v-for="(m, i) in edgeMetrics" :key="rowKey(m)" class="edge-metric">
+ <label class="side-pick">
+ <span class="side-label">{{ t('Side') }}</span>
+ <select
+ class="cf-input"
+ :disabled="readOnly"
+ :value="m.side ?? ''"
+ @change="setSide(i, ($event.target as HTMLSelectElement).value)"
+ >
+ <option value="client">{{ t('client') }}</option>
+ <option value="server">{{ t('server') }}</option>
+ <option value="">{{ t('neither — describes the exchange') }}</option>
+ </select>
+ </label>
+ <MetricDefinitionRow
+ v-model:metric="edgeMetrics[i]"
+ :layer-key="layerKey"
+ :read-only="readOnly"
+ site-scope="process-relation"
+ mqe-placeholder="process_relation_client_write_cpm"
+ :can-move-up="i > 0"
+ :can-move-down="i < edgeMetrics.length - 1"
+ @move-up="move(edgeMetrics, i, -1)"
+ @move-down="move(edgeMetrics, i, 1)"
+ @remove="remove(edgeMetrics, i)"
+ />
+ </div>
</div>
</div>
</div>
diff --git a/apps/ui/src/features/admin/layer-templates/ScopeTabsBar.vue b/apps/ui/src/features/admin/layer-templates/ScopeTabsBar.vue
index 8282fb9e..f13139bb 100644
--- a/apps/ui/src/features/admin/layer-templates/ScopeTabsBar.vue
+++ b/apps/ui/src/features/admin/layer-templates/ScopeTabsBar.vue
@@ -174,7 +174,8 @@ onBeforeUnmount(() => {
border: none;
border-radius: 4px;
cursor: pointer;
- text-transform: capitalize;
+ /* The labels carry their own casing — capitalize would render eBPF as EBPF. */
+ text-transform: none;
font: inherit;
display: inline-flex;
align-items: center;
diff --git a/apps/ui/src/features/admin/layer-templates/TraceStoresEditor.vue b/apps/ui/src/features/admin/layer-templates/TraceStoresEditor.vue
index c4b36a30..10af2986 100644
--- a/apps/ui/src/features/admin/layer-templates/TraceStoresEditor.vue
+++ b/apps/ui/src/features/admin/layer-templates/TraceStoresEditor.vue
@@ -47,7 +47,7 @@ const { t } = useI18n({ useScope: 'global' });
/** The TraceQL stores are the two that answer over Tempo's API, and the only
* ones a service filter applies to — the others know their layer already. */
-const TRACEQL_STORES = new Set<TraceStore>(['traceql-native', 'traceql-zipkin']);
+const TRACEQL_STORES = new Set<TraceStore>(['traceql-native', 'traceql-zipkin', 'traceql-otlp']);
const STORE_ROWS = computed(() =>
TRACE_STORES.map((store) => ({
@@ -65,6 +65,7 @@ function defaultName(store: TraceStore): string {
case 'zipkin': return t('Zipkin Traces');
case 'traceql-native': return t('TraceQL - Native');
case 'traceql-zipkin': return t('TraceQL - Zipkin');
+ case 'traceql-otlp': return t('TraceQL - OTLP');
}
}
@@ -74,6 +75,7 @@ function hintFor(store: TraceStore): string {
case 'zipkin': return t('OAP’s Zipkin v2 API, which also serves the OpenTelemetry spans OAP converts into Zipkin form.');
case 'traceql-native': return t('Grafana Tempo’s API over the native spans. Needs OAP’s traceQL module and a configured datasource URL.');
case 'traceql-zipkin': return t('Grafana Tempo’s API over the Zipkin spans. Needs OAP’s traceQL module and a configured datasource URL.');
+ case 'traceql-otlp': return t('Grafana Tempo’s API over the OTLP spans OAP stored as they arrived, converting nothing. Needs OAP’s traceQL module and a configured datasource URL.');
}
}
diff --git a/apps/ui/src/features/admin/layer-templates/layer-dashboards.scopes.ts b/apps/ui/src/features/admin/layer-templates/layer-dashboards.scopes.ts
index 81cfcc80..fd2d6cb7 100644
--- a/apps/ui/src/features/admin/layer-templates/layer-dashboards.scopes.ts
+++ b/apps/ui/src/features/admin/layer-templates/layer-dashboards.scopes.ts
@@ -35,7 +35,16 @@ export type SlotKey = keyof NonNullable<AdminLayerTemplate['slots']>;
* is the process-topology edge editor; `deployment` is the
* instance-deployment config (node + edge MQE + clusterBy). Both
* live outside `DashboardScope` but surface as editable config tabs. */
-export type AdminScope = DashboardScope | 'networkProfiling' | 'deployment';
+export type AdminScope =
+ | DashboardScope
+ | 'networkProfiling'
+ | 'deployment'
+ | 'podLogs'
+ | 'continuousProfiling'
+ | 'pprofProfiling'
+ | 'evaluationRecord'
+ | 'browserErrors'
+ | 'aiConversations';
export const SCOPES: AdminScope[] = [
'service',
@@ -48,28 +57,44 @@ export const SCOPES: AdminScope[] = [
'dependency',
'trace',
'logs',
+ 'evaluationRecord',
+ 'browserErrors',
+ 'podLogs',
+ 'aiConversations',
'traceProfiling',
'ebpfProfiling',
- 'asyncProfiling',
'networkProfiling',
+ 'continuousProfiling',
+ 'pprofProfiling',
+ 'asyncProfiling',
];
/** Display label for each scope — kebab-cases the profiling scopes
* so the scope tab strip reads as "trace profiling" instead of the
* camelCase key. */
+/** Written as they are DISPLAYED. They used to be lower-case and title-cased
+ * by CSS, which uppercases the first letter of each word and so rendered
+ * `eBPF profiling` as `EBPF Profiling` — a product name CSS cannot know how
+ * to case. Anything that shows these does not transform them. */
export const SCOPE_LABELS: Record<AdminScope, string> = {
- service: 'service',
- instance: 'instance',
- endpoint: 'endpoint',
- dependency: 'dependency',
- topology: 'topology',
- deployment: 'deployment',
- trace: 'trace',
- logs: 'logs',
- traceProfiling: 'trace profiling',
+ service: 'Service',
+ instance: 'Instance',
+ endpoint: 'Endpoint',
+ dependency: 'Dependency',
+ topology: 'Topology',
+ deployment: 'Deployment',
+ trace: 'Trace',
+ logs: 'Logs',
+ traceProfiling: 'Trace profiling',
ebpfProfiling: 'eBPF profiling',
- asyncProfiling: 'async profiling',
- networkProfiling: 'network profiling',
+ asyncProfiling: 'Async profiling',
+ networkProfiling: 'Network profiling',
+ continuousProfiling: 'Continuous profiling',
+ pprofProfiling: 'pprof',
+ podLogs: 'Pod logs',
+ evaluationRecord: 'Evaluation records',
+ browserErrors: 'Browser logs',
+ aiConversations: 'Conversations',
};
/** Scopes whose page is a built-in, runtime-configured explore view with no
@@ -82,6 +107,12 @@ export const RUNTIME_ONLY_SCOPES = new Set<AdminScope>([
'traceProfiling',
'ebpfProfiling',
'asyncProfiling',
+ 'continuousProfiling',
+ 'pprofProfiling',
+ 'podLogs',
+ 'evaluationRecord',
+ 'browserErrors',
+ 'aiConversations',
]);
/** Scopes that hold a widget list (so a tab count is meaningful). Config /
@@ -104,6 +135,12 @@ export const SCOPE_COMPONENT: Record<AdminScope, ComponentKey> = {
ebpfProfiling: 'ebpfProfiling' as ComponentKey,
asyncProfiling: 'asyncProfiling' as ComponentKey,
networkProfiling: 'networkProfiling' as ComponentKey,
+ continuousProfiling: 'continuousProfiling' as ComponentKey,
+ pprofProfiling: 'pprofProfiling' as ComponentKey,
+ podLogs: 'podLogs' as ComponentKey,
+ evaluationRecord: 'evaluationRecord' as ComponentKey,
+ browserErrors: 'browserErrors' as ComponentKey,
+ aiConversations: 'aiConversations' as ComponentKey,
};
/** Scope → slot-alias key, for the scopes that carry a configurable noun.
diff --git a/apps/ui/src/features/operate/cluster/ClusterStatusView.vue b/apps/ui/src/features/operate/cluster/ClusterStatusView.vue
index 99051f88..6344664f 100644
--- a/apps/ui/src/features/operate/cluster/ClusterStatusView.vue
+++ b/apps/ui/src/features/operate/cluster/ClusterStatusView.vue
@@ -18,7 +18,7 @@
import { computed, ref, onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuery } from '@tanstack/vue-query';
-import type { PreflightModule, TraceQLSourceStatus } from '@skywalking-horizon-ui/api-client';
+import type { PreflightModule, TraceQLSourceStatus, TraceQLDatasource } from '@skywalking-horizon-ui/api-client';
import { bffClient } from '@/api/client';
import { useOapInfo } from '@/shell/useOapInfo';
import { useAdminFeatures } from '@/shell/useAdminFeatures';
@@ -71,13 +71,14 @@ const traceqlQuery = useQuery({
staleTime: 60_000,
retry: false,
});
-const TRACEQL_ROWS = computed<ReadonlyArray<{ ds: 'native' | 'zipkin'; label: string; path: string }>>(() => [
+const TRACEQL_ROWS = computed<ReadonlyArray<{ ds: TraceQLDatasource; label: string; path: string }>>(() => [
{ ds: 'native', label: t('SkyWalking-native spans'), path: '/skywalking' },
{ ds: 'zipkin', label: t('Zipkin spans'), path: '/zipkin' },
+ { ds: 'otlp', label: t('Natively stored OTLP spans'), path: '/otlp' },
]);
const traceqlSources = computed<TraceQLSourceStatus[]>(() => traceqlQuery.data.value ?? []);
const traceqlDenied = computed(() => traceqlQuery.isError.value);
-function traceqlOf(ds: 'native' | 'zipkin'): TraceQLSourceStatus | null {
+function traceqlOf(ds: TraceQLDatasource): TraceQLSourceStatus | null {
return traceqlSources.value.find((x) => x.ds === ds) ?? null;
}
/** Configured sources only: an unconfigured datasource is a deployment that
@@ -91,7 +92,12 @@ interface TraceApiRow {
url: string;
state: { cls: string; label: string };
version?: string;
- ds?: 'native' | 'zipkin';
+ ds?: TraceQLDatasource;
+ /** What the endpoint said, and what to do about it — carried BY the row.
+ * These were two stacks of banners under the table, so the reader had to
+ * match a message back to the row it belonged to. */
+ error?: string;
+ hint?: string;
}
const traceApiRows = computed<TraceApiRow[]>(() => {
const rows: TraceApiRow[] = [
@@ -105,6 +111,12 @@ const traceApiRows = computed<TraceApiRow[]>(() => {
: zipkinReachable.value
? { cls: 'is-ok', label: t('reachable') }
: { cls: 'is-err', label: t('unreachable') },
+ ...(zipkinReachable.value === false
+ ? {
+ ...(info.value?.zipkinError ? { error: info.value.zipkinError } : {}),
+ hint: t("Tried {url}. Confirm OAP's Zipkin receiver / query is enabled and the oap.zipkinUrl in horizon's config points at the right host:port (shared GraphQL port → <queryUrl>/zipkin; standalone → :9412/zipkin). Only the Zipkin trace menu is affected.", { url: `${info.value?.zipkinUrl ?? ''}/api/v2/services` }),
+ }
+ : {}),
},
];
for (const r of TRACEQL_ROWS.value) {
@@ -112,15 +124,30 @@ const traceApiRows = computed<TraceApiRow[]>(() => {
rows.push({
api: 'TraceQL',
source: r.label,
- url: st?.url || t('no URL configured'),
+ // A 404 and a dead port are ONE state: the datasource did not answer.
+ // What differs is why, which the row's note below says — a status that
+ // splits on the reason makes the reader decode two words for one fact.
+ url: st?.url
+ || (st?.endpoint ? `${st.endpoint} — ${t('no context path')}` : t('no URL configured')),
state: traceqlDenied.value || !st
? { cls: 'is-unknown', label: t('unknown') }
: !st.configured
- ? { cls: 'is-unknown', label: t('not configured') }
+ ? { cls: 'is-unknown', label: st.endpoint ? t('not exposed') : t('not configured') }
: st.reachable
? { cls: 'is-ok', label: t('reachable') }
: { cls: 'is-err', label: t('unreachable') },
...(st?.version ? { version: st.version } : {}),
+ // A 404 is OAP answering; it needs a sentence, not an error. Only a
+ // real failure carries the endpoint's own words.
+ ...(st?.configured && st.served === false
+ ? { hint: t('This OAP does not enable the {name} datasource. Switch it on with SW_TRACEQL_ENABLE_DATASOURCE_* and it appears here; until then the rows fed by it stay empty.', { name: r.label }) }
+ : {}),
+ ...(st?.configured && st.served !== false && st.reachable === false
+ ? {
+ ...(st.error ? { error: st.error } : {}),
+ hint: t("Tried {url}. Enable OAP's traceQL module (SW_TRACEQL=default with the datasource switched on) and confirm oap.traceql points at its host:port and context path — {path} by default on port 3200.", { url: `${st.url}/api/status/buildinfo`, path: r.path }),
+ }
+ : {}),
ds: r.ds,
});
}
@@ -499,7 +526,8 @@ const storeLastSyncShort = computed<string>(() => {
</tr>
</thead>
<tbody>
- <tr v-for="r in traceApiRows" :key="`${r.api}-${r.source}`" :class="{ off: r.state.cls === 'is-err' }">
+ <template v-for="r in traceApiRows" :key="`${r.api}-${r.source}`">
+ <tr :class="{ off: r.state.cls === 'is-err' }">
<td class="modname"><code>{{ r.api }}</code></td>
<td class="modname">{{ r.source }}</td>
<td>
@@ -512,31 +540,37 @@ const storeLastSyncShort = computed<string>(() => {
</td>
<td class="modpath"><code>{{ r.url }}</code></td>
</tr>
+ <tr v-if="r.error || r.hint" class="row-note">
+ <td colspan="4">
+ <code v-if="r.error">{{ r.error }}</code>
+ <p v-if="r.hint" class="hint">{{ r.hint }}</p>
+ </td>
+ </tr>
+ </template>
</tbody>
</table>
-
- <div v-if="zipkinReachable === false" class="last-error block">
- <strong>{{ t('Zipkin endpoint unreachable') }}</strong>
- <code v-if="info?.zipkinError">{{ info.zipkinError }}</code>
- <p class="hint">
- {{ t("Tried {url}. Confirm OAP's Zipkin receiver / query is enabled and the oap.zipkinUrl in horizon's config points at the right host:port (shared GraphQL port → <queryUrl>/zipkin; standalone → :9412/zipkin). Only the Zipkin trace menu is affected.", { url: `${info?.zipkinUrl ?? ''}/api/v2/services` }) }}
- </p>
- </div>
-
- <div v-for="row in TRACEQL_ROWS" :key="`e-${row.ds}`">
- <div v-if="traceqlOf(row.ds)?.configured && traceqlOf(row.ds)?.reachable === false" class="last-error block">
- <strong>{{ t('{name} datasource unreachable', { name: row.label }) }}</strong>
- <code v-if="traceqlOf(row.ds)?.error">{{ traceqlOf(row.ds)?.error }}</code>
- <p class="hint">
- {{ t("Tried {url}. Enable OAP's traceQL module (SW_TRACEQL=default with the datasource switched on) and confirm oap.traceql points at its host:port and context path — {path} by default on port 3200.", { url: `${traceqlOf(row.ds)?.url ?? ''}/api/status/buildinfo`, path: row.path }) }}
- </p>
- </div>
- </div>
</section>
</div>
</template>
<style scoped>
+.row-note td {
+ padding: 2px 10px 10px;
+ border-top: none;
+}
+.row-note code {
+ display: block;
+ font-size: 10.5px;
+ color: var(--sw-err);
+ margin-bottom: 4px;
+ word-break: break-all;
+}
+.row-note .hint {
+ margin: 0;
+ font-size: 10.5px;
+ color: var(--sw-fg-3);
+ line-height: 1.5;
+}
.cluster {
padding: 20px 20px 60px;
max-width: 1440px;
diff --git a/apps/ui/src/i18n/locales/de.json b/apps/ui/src/i18n/locales/de.json
index b15879fd..e4861000 100644
--- a/apps/ui/src/i18n/locales/de.json
+++ b/apps/ui/src/i18n/locales/de.json
@@ -354,7 +354,6 @@
"Client address": "Client-Adresse",
"Client service…": "Client-Service…",
"Client side": "Clientseite",
- "Client-side metrics": "Clientseitige Metriken",
"Close": "Schließen",
"Close (Esc)": "Schließen (Esc)",
"Close hierarchy": "Hierarchie schließen",
@@ -749,6 +748,9 @@
"Keep this version": "Diese Fassung behalten",
"Keep this version to overwrite the other one, or load the other version to discard the turns shown here.": "Behalten Sie diese Fassung, um die andere zu überschreiben, oder laden Sie die andere Fassung und verwerfen Sie die hier gezeigten Beiträge.",
"Kernel-level CPU / off-CPU profiling via eBPF agents.": "CPU- und Off-CPU-Profiling auf Kernel-Ebene über eBPF-Agenten.",
+ "Process-to-process network profiling via the Rover eBPF agent.": "Prozess-zu-Prozess-Netzwerkprofiling über den Rover-eBPF-Agenten.",
+ "Go runtime pprof profiles (Go-only).": "pprof-Profile der Go-Laufzeit (nur Go).",
+ "AI agent conversations recorded against this layer’s services.": "KI-Agenten-Konversationen, die für die Services dieses Layers aufgezeichnet wurden.",
"Key": "Schlüssel",
"Key metrics": "Wichtige Metriken",
"Keyword": "Stichwort",
@@ -946,7 +948,6 @@
"No async profile tasks yet.": "Noch keine Async-Profiling-Aufgaben.",
"No browser logs in this window.": "Keine Browser-Logs in diesem Fenster.",
"No client metrics configured.": "Keine clientseitigen Metriken konfiguriert.",
- "No client-side metrics.": "Keine clientseitigen Metriken.",
"No cluster rule configured — the topology view renders without cluster bounding boxes. Enable a rule for layers whose service names encode a cluster dimension (k8s namespace, fleet, tenant) so topology can cluster nodes accordingly.": "Keine Cluster-Regel konfiguriert — die Topologie-Ansicht zeigt keine Cluster-Rahmen. Aktivieren Sie eine Regel für Ebenen, deren Service-Namen eine Cluster-Dimension enthalten (k8s-Namespace, Fleet, Mandant), damit die Topologie die Knoten entsprechend [...]
"No components enabled — toggle one on the right to populate the menu.": "Keine Komponenten aktiviert — schalten Sie rechts eine ein, um das Menü zu füllen.",
"No conditions — every instance matching the name filter is shown.": "Keine Bedingungen — jede Instanz, die dem Namensfilter entspricht, wird angezeigt.",
@@ -1029,7 +1030,13 @@
"No schedules captured for this task.": "Für diese Aufgabe wurden keine Zeitpläne erfasst.",
"No screen or request checks this — granting it has no effect.": "Keine Ansicht und keine Anfrage prüft dies — die Vergabe bleibt wirkungslos.",
"No server metrics configured.": "Keine serverseitigen Metriken konfiguriert.",
- "No server-side metrics.": "Keine serverseitigen Metriken.",
+ "Edge metrics": "Kantenmetriken",
+ "Each names the end of the conversation it describes. The eBPF probe watches both, and a metric that describes the exchange itself — the HTTP/1.x families — belongs to neither.": "Jede benennt das Ende der Konversation, das sie beschreibt. Die eBPF-Sonde beobachtet beide, und eine Metrik, die den Austausch selbst beschreibt — die HTTP/1.x-Familien — gehört zu keinem.",
+ "No edge metrics.": "Keine Kantenmetriken.",
+ "Side": "Seite",
+ "neither — describes the exchange": "keine — beschreibt den Austausch",
+ "Both sides": "Beide Seiten",
+ "No shared metrics configured.": "Keine gemeinsamen Metriken konfiguriert.",
"No service matches this": "Kein Service entspricht dem",
"No services match": "Keine Services passen",
"No services reported.": "Keine Dienste gemeldet.",
@@ -1498,7 +1505,6 @@
"Server side": "Serverseite",
"Server timezone": "Server-Zeitzone",
"Server-side": "Serverseitig",
- "Server-side metrics": "Serverseitige Metriken",
"Service": "Service",
"Service bind": "Service-Bind",
"Service bind DN": "Service-Bind-DN",
@@ -1895,7 +1901,6 @@
"Your sidebar adapts to your role: anything you can't use is simply not shown. The sections below group SkyWalking's capabilities by feature area. For each area you'll see": "Deine Sidebar passt sich deiner Rolle an: Was du nicht nutzen kannst, wird einfach nicht angezeigt. Die Abschnitte unten gruppieren die Fähigkeiten von SkyWalking nach Funktionsbereich. Für jeden Bereich siehst du",
"Your sidebar adapts to your role: anything you can't use is simply not shown. The sections below group SkyWalking's capabilities by feature area. For each area you'll see {menuScope} (which sidebar entries fall in the area) and {actions} (the concrete things you can do, marked per role). Multiple roles add up — a user holding more than one role gets every permission any of their roles grant.": "Deine Sidebar passt sich deiner Rolle an: Was du nicht nutzen kannst, wird einfach nicht an [...]
"Your username is your identity here, and your roles are resolved from it rather than stored against you. A token you hold re-reads them on every request, so removing you from the directory or configuration ends it at once. This browser session keeps the roles it was given until you sign in again.": "Ihr Benutzername ist hier Ihre Identität, und Ihre Rollen werden daraus aufgelöst, statt bei Ihnen gespeichert zu werden. Ein Token, das Sie besitzen, liest sie bei jeder Anfrage neu — wir [...]
- "Zipkin endpoint unreachable": "Zipkin-Endpunkt nicht erreichbar",
"Zipkin query failed.": "Zipkin-Abfrage fehlgeschlagen.",
"Zipkin trace": "Zipkin-Trace",
"Zoom in": "Vergrößern",
@@ -2092,7 +2097,6 @@
"edge": "Kante",
"edge MQE for the process-topology detail panel. Queried under ProcessRelation when an operator clicks a process→process call.": "Kanten-MQE für das Detail-Panel der Prozess-Topologie. Wird unter ProcessRelation abgefragt, wenn ein Operator einen Prozess→Prozess-Aufruf anklickt.",
"edge metrics for “{from} → {to}” — queried as": "Kantenmetriken für „{from} → {to}“ — abgefragt als",
- "edge metrics queried as": "Kantenmetriken, abgefragt als",
"edges": "Kanten",
"edit": "bearbeiten",
"editing: {source}": "Bearbeitung: {source}",
@@ -3355,6 +3359,7 @@
"OAP’s Zipkin v2 API, which also serves the OpenTelemetry spans OAP converts into Zipkin form.": "Die Zipkin-v2-API von OAP, die auch die OpenTelemetry-Spans liefert, die OAP ins Zipkin-Format umwandelt.",
"Grafana Tempo’s API over the native spans. Needs OAP’s traceQL module and a configured datasource URL.": "Die API von Grafana Tempo über den nativen Spans. Erfordert das traceQL-Modul von OAP und eine konfigurierte Datenquellen-URL.",
"Grafana Tempo’s API over the Zipkin spans. Needs OAP’s traceQL module and a configured datasource URL.": "Die API von Grafana Tempo über den Zipkin-Spans. Erfordert das traceQL-Modul von OAP und eine konfigurierte Datenquellen-URL.",
+ "Grafana Tempo’s API over the OTLP spans OAP stored as they arrived, converting nothing. Needs OAP’s traceQL module and a configured datasource URL.": "Die API von Grafana Tempo über den OTLP-Spans, die OAP unverändert gespeichert hat. Erfordert das traceQL-Modul von OAP und eine konfigurierte Datenquellen-URL.",
"Flags": "Flags",
"The Tempo API has no notion of a layer, so it lists every service of the store. This pattern narrows the service PICKER only — result rows are never filtered, so cross-service traces still appear.": "Die Tempo-API kennt keine Ebenen und listet daher jeden Service des Speichers auf. Dieses Muster schränkt nur die Service-AUSWAHL ein — Ergebniszeilen werden nie gefiltert, serviceübergreifende Traces erscheinen also weiterhin.",
"checking {n} statuses…": "{n} Status werden geprüft…",
@@ -3368,8 +3373,10 @@
"Tempo API": "Tempo-API",
"not available": "nicht verfügbar",
"no URL configured": "keine URL konfiguriert",
+ "no context path": "kein Kontextpfad",
+ "not exposed": "nicht bereitgestellt",
+ "This OAP does not enable the {name} datasource. Switch it on with SW_TRACEQL_ENABLE_DATASOURCE_* and it appears here; until then the rows fed by it stay empty.": "Dieses OAP aktiviert die Datenquelle {name} nicht. Schalten Sie sie mit SW_TRACEQL_ENABLE_DATASOURCE_* ein, dann erscheint sie hier; bis dahin bleiben die davon gespeisten Zeilen leer.",
"SkyWalking-native spans": "SkyWalking-native Spans",
- "{name} datasource unreachable": "Datenquelle {name} nicht erreichbar",
"Tried {url}. Enable OAP's traceQL module (SW_TRACEQL=default with the datasource switched on) and confirm oap.traceql points at its host:port and context path — {path} by default on port 3200.": "{url} wurde versucht. Aktivieren Sie das traceQL-Modul von OAP (SW_TRACEQL=default mit eingeschalteter Datenquelle) und prüfen Sie, dass oap.traceql auf dessen host:port und Kontextpfad zeigt — standardmäßig {path} auf Port 3200.",
"Trace APIs": "Trace-APIs",
"Zipkin v2 · Tempo": "Zipkin v2 · Tempo",
@@ -3392,13 +3399,17 @@
"A status other than “ok”, “error” or “unset”.": "Ein anderer Status als „ok“, „error“ oder „unset“.",
"Span tags are those this store reported for the window being queried, so the list follows the time range.": "Span-Tags sind die, die dieser Store für den abgefragten Zeitraum gemeldet hat — die Liste folgt also dem Zeitbereich.",
"ok, error or unset": "ok, error oder unset",
+ "Span kind — server, client, producer, consumer, internal, unspecified (OTLP)": "Span-Art — server, client, producer, consumer, internal, unspecified (OTLP)",
"Service instance (native)": "Service-Instanz (nativ)",
- "Peer service (Zipkin)": "Peer-Service (Zipkin)",
+ "Service instance (OTLP)": "Service-Instanz (OTLP)",
+ "Peer service (Zipkin, OTLP)": "Peer-Service (Zipkin, OTLP)",
"Trace duration — >, >=, <, <= with us/µs/ms/s/m/h": "Trace-Dauer — >, >=, <, <= mit us/µs/ms/s/m/h",
"Zipkin Traces": "Zipkin-Traces",
"TraceQL - Native": "TraceQL - Native",
"TraceQL - Zipkin": "TraceQL - Zipkin",
+ "TraceQL - OTLP": "TraceQL - OTLP",
"Zipkin spans": "Zipkin-Spans",
+ "Natively stored OTLP spans": "Nativ gespeicherte OTLP-Spans",
"Menu name": "Menüname",
"Ignore case": "Groß-/Kleinschreibung ignorieren",
"Horizon will not run this expression, so the picker keeps showing every service. It either does not compile, or it can backtrack exponentially — avoid a repeat applied to a group that itself repeats or branches, such as (a+)+.": "Horizon führt diesen Ausdruck nicht aus, daher zeigt die Auswahl weiterhin alle Services. Er lässt sich entweder nicht übersetzen oder kann exponentiell zurücksetzen — vermeiden Sie eine Wiederholung auf einer Gruppe, die selbst wiederholt oder verzweigt, etw [...]
@@ -3406,5 +3417,8 @@
"Open the calendar": "Kalender öffnen",
"Previous month": "Vorheriger Monat",
"Next month": "Nächster Monat",
- "Now": "Jetzt"
+ "Now": "Jetzt",
+ "edge metrics queried as": "Kantenmetriken, abgefragt als",
+ "No server-side metrics.": "Keine serverseitigen Metriken.",
+ "No client-side metrics.": "Keine clientseitigen Metriken."
}
diff --git a/apps/ui/src/i18n/locales/en.json b/apps/ui/src/i18n/locales/en.json
index 79081740..0c6d64bf 100644
--- a/apps/ui/src/i18n/locales/en.json
+++ b/apps/ui/src/i18n/locales/en.json
@@ -354,7 +354,6 @@
"Client address": "Client address",
"Client service…": "Client service…",
"Client side": "Client side",
- "Client-side metrics": "Client-side metrics",
"Close": "Close",
"Close (Esc)": "Close (Esc)",
"Close hierarchy": "Close hierarchy",
@@ -749,6 +748,9 @@
"Keep this version": "Keep this version",
"Keep this version to overwrite the other one, or load the other version to discard the turns shown here.": "Keep this version to overwrite the other one, or load the other version to discard the turns shown here.",
"Kernel-level CPU / off-CPU profiling via eBPF agents.": "Kernel-level CPU / off-CPU profiling via eBPF agents.",
+ "Process-to-process network profiling via the Rover eBPF agent.": "Process-to-process network profiling via the Rover eBPF agent.",
+ "Go runtime pprof profiles (Go-only).": "Go runtime pprof profiles (Go-only).",
+ "AI agent conversations recorded against this layer’s services.": "AI agent conversations recorded against this layer’s services.",
"Key": "Key",
"Key metrics": "Key metrics",
"Keyword": "Keyword",
@@ -946,7 +948,6 @@
"No async profile tasks yet.": "No async profile tasks yet.",
"No browser logs in this window.": "No browser logs in this window.",
"No client metrics configured.": "No client metrics configured.",
- "No client-side metrics.": "No client-side metrics.",
"No cluster rule configured — the topology view renders without cluster bounding boxes. Enable a rule for layers whose service names encode a cluster dimension (k8s namespace, fleet, tenant) so topology can cluster nodes accordingly.": "No cluster rule configured — the topology view renders without cluster bounding boxes. Enable a rule for layers whose service names encode a cluster dimension (k8s namespace, fleet, tenant) so topology can cluster nodes accordingly.",
"No components enabled — toggle one on the right to populate the menu.": "No components enabled — toggle one on the right to populate the menu.",
"No conditions — every instance matching the name filter is shown.": "No conditions — every instance matching the name filter is shown.",
@@ -1029,7 +1030,13 @@
"No schedules captured for this task.": "No schedules captured for this task.",
"No screen or request checks this — granting it has no effect.": "No screen or request checks this — granting it has no effect.",
"No server metrics configured.": "No server metrics configured.",
- "No server-side metrics.": "No server-side metrics.",
+ "Edge metrics": "Edge metrics",
+ "Each names the end of the conversation it describes. The eBPF probe watches both, and a metric that describes the exchange itself — the HTTP/1.x families — belongs to neither.": "Each names the end of the conversation it describes. The eBPF probe watches both, and a metric that describes the exchange itself — the HTTP/1.x families — belongs to neither.",
+ "No edge metrics.": "No edge metrics.",
+ "Side": "Side",
+ "neither — describes the exchange": "neither — describes the exchange",
+ "Both sides": "Both sides",
+ "No shared metrics configured.": "No shared metrics configured.",
"No service matches this": "No service matches this",
"No services match": "No services match",
"No services reported.": "No services reported.",
@@ -1498,7 +1505,6 @@
"Server side": "Server side",
"Server timezone": "Server timezone",
"Server-side": "Server-side",
- "Server-side metrics": "Server-side metrics",
"Service": "Service",
"Service bind": "Service bind",
"Service bind DN": "Service bind DN",
@@ -1895,7 +1901,6 @@
"Your sidebar adapts to your role: anything you can't use is simply not shown. The sections below group SkyWalking's capabilities by feature area. For each area you'll see": "Your sidebar adapts to your role: anything you can't use is simply not shown. The sections below group SkyWalking's capabilities by feature area. For each area you'll see",
"Your sidebar adapts to your role: anything you can't use is simply not shown. The sections below group SkyWalking's capabilities by feature area. For each area you'll see {menuScope} (which sidebar entries fall in the area) and {actions} (the concrete things you can do, marked per role). Multiple roles add up — a user holding more than one role gets every permission any of their roles grant.": "Your sidebar adapts to your role: anything you can't use is simply not shown. The sections [...]
"Your username is your identity here, and your roles are resolved from it rather than stored against you. A token you hold re-reads them on every request, so removing you from the directory or configuration ends it at once. This browser session keeps the roles it was given until you sign in again.": "Your username is your identity here, and your roles are resolved from it rather than stored against you. A token you hold re-reads them on every request, so removing you from the directory [...]
- "Zipkin endpoint unreachable": "Zipkin endpoint unreachable",
"Zipkin query failed.": "Zipkin query failed.",
"Zipkin trace": "Zipkin trace",
"Zoom in": "Zoom in",
@@ -2092,7 +2097,6 @@
"edge": "edge",
"edge MQE for the process-topology detail panel. Queried under ProcessRelation when an operator clicks a process→process call.": "edge MQE for the process-topology detail panel. Queried under ProcessRelation when an operator clicks a process→process call.",
"edge metrics for “{from} → {to}” — queried as": "edge metrics for “{from} → {to}” — queried as",
- "edge metrics queried as": "edge metrics queried as",
"edges": "edges",
"edit": "edit",
"editing: {source}": "editing: {source}",
@@ -3355,6 +3359,7 @@
"OAP’s Zipkin v2 API, which also serves the OpenTelemetry spans OAP converts into Zipkin form.": "OAP’s Zipkin v2 API, which also serves the OpenTelemetry spans OAP converts into Zipkin form.",
"Grafana Tempo’s API over the native spans. Needs OAP’s traceQL module and a configured datasource URL.": "Grafana Tempo’s API over the native spans. Needs OAP’s traceQL module and a configured datasource URL.",
"Grafana Tempo’s API over the Zipkin spans. Needs OAP’s traceQL module and a configured datasource URL.": "Grafana Tempo’s API over the Zipkin spans. Needs OAP’s traceQL module and a configured datasource URL.",
+ "Grafana Tempo’s API over the OTLP spans OAP stored as they arrived, converting nothing. Needs OAP’s traceQL module and a configured datasource URL.": "Grafana Tempo’s API over the OTLP spans OAP stored as they arrived, converting nothing. Needs OAP’s traceQL module and a configured datasource URL.",
"Flags": "Flags",
"The Tempo API has no notion of a layer, so it lists every service of the store. This pattern narrows the service PICKER only — result rows are never filtered, so cross-service traces still appear.": "The Tempo API has no notion of a layer, so it lists every service of the store. This pattern narrows the service PICKER only — result rows are never filtered, so cross-service traces still appear.",
"checking {n} statuses…": "checking {n} statuses…",
@@ -3368,8 +3373,10 @@
"Tempo API": "Tempo API",
"not available": "not available",
"no URL configured": "no URL configured",
+ "no context path": "no context path",
+ "not exposed": "not exposed",
+ "This OAP does not enable the {name} datasource. Switch it on with SW_TRACEQL_ENABLE_DATASOURCE_* and it appears here; until then the rows fed by it stay empty.": "This OAP does not enable the {name} datasource. Switch it on with SW_TRACEQL_ENABLE_DATASOURCE_* and it appears here; until then the rows fed by it stay empty.",
"SkyWalking-native spans": "SkyWalking-native spans",
- "{name} datasource unreachable": "{name} datasource unreachable",
"Tried {url}. Enable OAP's traceQL module (SW_TRACEQL=default with the datasource switched on) and confirm oap.traceql points at its host:port and context path — {path} by default on port 3200.": "Tried {url}. Enable OAP's traceQL module (SW_TRACEQL=default with the datasource switched on) and confirm oap.traceql points at its host:port and context path — {path} by default on port 3200.",
"Trace APIs": "Trace APIs",
"Zipkin v2 · Tempo": "Zipkin v2 · Tempo",
@@ -3392,13 +3399,17 @@
"A status other than “ok”, “error” or “unset”.": "A status other than “ok”, “error” or “unset”.",
"Span tags are those this store reported for the window being queried, so the list follows the time range.": "Span tags are those this store reported for the window being queried, so the list follows the time range.",
"ok, error or unset": "ok, error or unset",
+ "Span kind — server, client, producer, consumer, internal, unspecified (OTLP)": "Span kind — server, client, producer, consumer, internal, unspecified (OTLP)",
"Service instance (native)": "Service instance (native)",
- "Peer service (Zipkin)": "Peer service (Zipkin)",
+ "Service instance (OTLP)": "Service instance (OTLP)",
+ "Peer service (Zipkin, OTLP)": "Peer service (Zipkin, OTLP)",
"Trace duration — >, >=, <, <= with us/µs/ms/s/m/h": "Trace duration — >, >=, <, <= with us/µs/ms/s/m/h",
"Zipkin Traces": "Zipkin Traces",
"TraceQL - Native": "TraceQL - Native",
"TraceQL - Zipkin": "TraceQL - Zipkin",
+ "TraceQL - OTLP": "TraceQL - OTLP",
"Zipkin spans": "Zipkin spans",
+ "Natively stored OTLP spans": "Natively stored OTLP spans",
"Menu name": "Menu name",
"Ignore case": "Ignore case",
"Horizon will not run this expression, so the picker keeps showing every service. It either does not compile, or it can backtrack exponentially — avoid a repeat applied to a group that itself repeats or branches, such as (a+)+.": "Horizon will not run this expression, so the picker keeps showing every service. It either does not compile, or it can backtrack exponentially — avoid a repeat applied to a group that itself repeats or branches, such as (a+)+.",
@@ -3406,5 +3417,8 @@
"Open the calendar": "Open the calendar",
"Previous month": "Previous month",
"Next month": "Next month",
- "Now": "Now"
+ "Now": "Now",
+ "edge metrics queried as": "edge metrics queried as",
+ "No server-side metrics.": "No server-side metrics.",
+ "No client-side metrics.": "No client-side metrics."
}
diff --git a/apps/ui/src/i18n/locales/es.json b/apps/ui/src/i18n/locales/es.json
index f6803b52..25e6e066 100644
--- a/apps/ui/src/i18n/locales/es.json
+++ b/apps/ui/src/i18n/locales/es.json
@@ -354,7 +354,6 @@
"Client address": "Dirección del cliente",
"Client service…": "Servicio cliente…",
"Client side": "Lado cliente",
- "Client-side metrics": "Métricas del lado cliente",
"Close": "Cerrar",
"Close (Esc)": "Cerrar (Esc)",
"Close hierarchy": "Cerrar la jerarquía",
@@ -749,6 +748,9 @@
"Keep this version": "Conservar esta versión",
"Keep this version to overwrite the other one, or load the other version to discard the turns shown here.": "Conserva esta versión para sobrescribir la otra, o carga la otra versión para descartar los turnos que se muestran aquí.",
"Kernel-level CPU / off-CPU profiling via eBPF agents.": "Perfilado de CPU / off-CPU a nivel de kernel mediante agentes eBPF.",
+ "Process-to-process network profiling via the Rover eBPF agent.": "Perfilado de red entre procesos mediante el agente eBPF Rover.",
+ "Go runtime pprof profiles (Go-only).": "Perfiles pprof del runtime de Go (solo Go).",
+ "AI agent conversations recorded against this layer’s services.": "Conversaciones de agentes de IA registradas para los servicios de esta capa.",
"Key": "Clave",
"Key metrics": "Métricas clave",
"Keyword": "Palabra clave",
@@ -946,7 +948,6 @@
"No async profile tasks yet.": "Aún no hay tareas de perfilado asíncrono.",
"No browser logs in this window.": "No hay logs del navegador en esta ventana.",
"No client metrics configured.": "No hay métricas de cliente configuradas.",
- "No client-side metrics.": "Sin métricas del lado cliente.",
"No cluster rule configured — the topology view renders without cluster bounding boxes. Enable a rule for layers whose service names encode a cluster dimension (k8s namespace, fleet, tenant) so topology can cluster nodes accordingly.": "No hay ninguna regla de clúster configurada — la vista de topología se dibuja sin recuadros de clúster. Habilita una regla para las capas cuyos nombres de servicio codifican una dimensión de clúster (namespace de k8s, fleet, tenant) para que la topologí [...]
"No components enabled — toggle one on the right to populate the menu.": "No hay componentes habilitados — activa uno a la derecha para llenar el menú.",
"No conditions — every instance matching the name filter is shown.": "Sin condiciones: se muestran todas las instancias que coinciden con el filtro de nombre.",
@@ -1029,7 +1030,13 @@
"No schedules captured for this task.": "No se capturó ninguna programación para esta tarea.",
"No screen or request checks this — granting it has no effect.": "Ninguna pantalla ni petición lo comprueba: concederlo no cambia nada.",
"No server metrics configured.": "No hay métricas de servidor configuradas.",
- "No server-side metrics.": "Sin métricas del lado servidor.",
+ "Edge metrics": "Métricas de arista",
+ "Each names the end of the conversation it describes. The eBPF probe watches both, and a metric that describes the exchange itself — the HTTP/1.x families — belongs to neither.": "Cada una indica el extremo de la conversación que describe. La sonda eBPF observa ambos, y una métrica que describe el intercambio en sí — las familias HTTP/1.x — no pertenece a ninguno.",
+ "No edge metrics.": "Sin métricas de arista.",
+ "Side": "Lado",
+ "neither — describes the exchange": "ninguno — describe el intercambio",
+ "Both sides": "Ambos lados",
+ "No shared metrics configured.": "No hay métricas compartidas configuradas.",
"No service matches this": "Ningún servicio coincide con esto",
"No services match": "Ningún servicio coincide",
"No services reported.": "No se reportaron servicios.",
@@ -1498,7 +1505,6 @@
"Server side": "Lado servidor",
"Server timezone": "Zona horaria del servidor",
"Server-side": "Lado servidor",
- "Server-side metrics": "Métricas del lado servidor",
"Service": "Service",
"Service bind": "Service bind",
"Service bind DN": "DN de service bind",
@@ -1895,7 +1901,6 @@
"Your sidebar adapts to your role: anything you can't use is simply not shown. The sections below group SkyWalking's capabilities by feature area. For each area you'll see": "Tu menú lateral se adapta a tu rol: lo que no puedes usar simplemente no se muestra. Las secciones de abajo agrupan las capacidades de SkyWalking por área de funcionalidad. Para cada área verás",
"Your sidebar adapts to your role: anything you can't use is simply not shown. The sections below group SkyWalking's capabilities by feature area. For each area you'll see {menuScope} (which sidebar entries fall in the area) and {actions} (the concrete things you can do, marked per role). Multiple roles add up — a user holding more than one role gets every permission any of their roles grant.": "Tu menú lateral se adapta a tu rol: lo que no puedes usar simplemente no se muestra. Las se [...]
"Your username is your identity here, and your roles are resolved from it rather than stored against you. A token you hold re-reads them on every request, so removing you from the directory or configuration ends it at once. This browser session keeps the roles it was given until you sign in again.": "Tu nombre de usuario es tu identidad aquí, y tus roles se resuelven a partir de él en lugar de almacenarse contigo. Un token que poseas los vuelve a leer en cada solicitud, así que elimina [...]
- "Zipkin endpoint unreachable": "Endpoint de Zipkin no accesible",
"Zipkin query failed.": "La consulta a Zipkin falló.",
"Zipkin trace": "Traza Zipkin",
"Zoom in": "Acercar",
@@ -2092,7 +2097,6 @@
"edge": "arista",
"edge MQE for the process-topology detail panel. Queried under ProcessRelation when an operator clicks a process→process call.": "MQE de arista para el panel de detalle de la topología de procesos. Se consulta bajo ProcessRelation cuando un operador hace clic en una llamada proceso→proceso.",
"edge metrics for “{from} → {to}” — queried as": "métricas de arista de “{from} → {to}” — consultadas como",
- "edge metrics queried as": "métricas de arista consultadas como",
"edges": "aristas",
"edit": "editar",
"editing: {source}": "editando: {source}",
@@ -3355,6 +3359,7 @@
"OAP’s Zipkin v2 API, which also serves the OpenTelemetry spans OAP converts into Zipkin form.": "La API Zipkin v2 de OAP, que también sirve los spans de OpenTelemetry que OAP convierte al formato Zipkin.",
"Grafana Tempo’s API over the native spans. Needs OAP’s traceQL module and a configured datasource URL.": "La API de Grafana Tempo sobre los spans nativos. Requiere el módulo traceQL de OAP y una URL de origen de datos configurada.",
"Grafana Tempo’s API over the Zipkin spans. Needs OAP’s traceQL module and a configured datasource URL.": "La API de Grafana Tempo sobre los spans de Zipkin. Requiere el módulo traceQL de OAP y una URL de origen de datos configurada.",
+ "Grafana Tempo’s API over the OTLP spans OAP stored as they arrived, converting nothing. Needs OAP’s traceQL module and a configured datasource URL.": "La API de Grafana Tempo sobre los spans OTLP que OAP almacenó tal como llegaron, sin convertir nada. Requiere el módulo traceQL de OAP y una URL de origen de datos configurada.",
"Flags": "Flags",
"The Tempo API has no notion of a layer, so it lists every service of the store. This pattern narrows the service PICKER only — result rows are never filtered, so cross-service traces still appear.": "La API de Tempo no conoce el concepto de capa, así que lista todos los servicios del almacén. Este patrón solo acota el SELECTOR de servicios: las filas de resultados nunca se filtran, por lo que las trazas entre servicios siguen apareciendo.",
"checking {n} statuses…": "comprobando {n} estados…",
@@ -3368,8 +3373,10 @@
"Tempo API": "API de Tempo",
"not available": "no disponible",
"no URL configured": "sin URL configurada",
+ "no context path": "sin ruta de contexto",
+ "not exposed": "no expuesto",
+ "This OAP does not enable the {name} datasource. Switch it on with SW_TRACEQL_ENABLE_DATASOURCE_* and it appears here; until then the rows fed by it stay empty.": "Este OAP no habilita el origen de datos {name}. Actívelo con SW_TRACEQL_ENABLE_DATASOURCE_* y aparecerá aquí; hasta entonces las filas que dependen de él quedan vacías.",
"SkyWalking-native spans": "Spans nativos de SkyWalking",
- "{name} datasource unreachable": "Fuente de datos {name} inaccesible",
"Tried {url}. Enable OAP's traceQL module (SW_TRACEQL=default with the datasource switched on) and confirm oap.traceql points at its host:port and context path — {path} by default on port 3200.": "Se intentó {url}. Habilita el módulo traceQL de OAP (SW_TRACEQL=default con la fuente de datos activada) y comprueba que oap.traceql apunte a su host:puerto y ruta de contexto: {path} por defecto en el puerto 3200.",
"Trace APIs": "APIs de trazas",
"Zipkin v2 · Tempo": "Zipkin v2 · Tempo",
@@ -3392,13 +3399,17 @@
"A status other than “ok”, “error” or “unset”.": "Un estado distinto de «ok», «error» o «unset».",
"Span tags are those this store reported for the window being queried, so the list follows the time range.": "Las etiquetas de span son las que este almacén informó para la ventana consultada, así que la lista sigue al rango de tiempo.",
"ok, error or unset": "ok, error o unset",
+ "Span kind — server, client, producer, consumer, internal, unspecified (OTLP)": "Tipo de span — server, client, producer, consumer, internal, unspecified (OTLP)",
"Service instance (native)": "Instancia de servicio (nativa)",
- "Peer service (Zipkin)": "Servicio par (Zipkin)",
+ "Service instance (OTLP)": "Instancia del servicio (OTLP)",
+ "Peer service (Zipkin, OTLP)": "Servicio par (Zipkin, OTLP)",
"Trace duration — >, >=, <, <= with us/µs/ms/s/m/h": "Duración de la traza — >, >=, <, <= con us/µs/ms/s/m/h",
"Zipkin Traces": "Trazas de Zipkin",
"TraceQL - Native": "TraceQL - Native",
"TraceQL - Zipkin": "TraceQL - Zipkin",
+ "TraceQL - OTLP": "TraceQL - OTLP",
"Zipkin spans": "Spans de Zipkin",
+ "Natively stored OTLP spans": "Spans OTLP almacenados de forma nativa",
"Menu name": "Nombre en el menú",
"Ignore case": "Ignorar mayúsculas",
"Horizon will not run this expression, so the picker keeps showing every service. It either does not compile, or it can backtrack exponentially — avoid a repeat applied to a group that itself repeats or branches, such as (a+)+.": "Horizon no ejecutará esta expresión, así que el selector seguirá mostrando todos los servicios. O no compila, o puede retroceder exponencialmente: evite una repetición aplicada a un grupo que a su vez se repite o se bifurca, como (a+)+.",
@@ -3406,5 +3417,8 @@
"Open the calendar": "Abrir el calendario",
"Previous month": "Mes anterior",
"Next month": "Mes siguiente",
- "Now": "Ahora"
+ "Now": "Ahora",
+ "edge metrics queried as": "métricas de arista consultadas como",
+ "No server-side metrics.": "Sin métricas del lado servidor.",
+ "No client-side metrics.": "Sin métricas del lado cliente."
}
diff --git a/apps/ui/src/i18n/locales/fr.json b/apps/ui/src/i18n/locales/fr.json
index 5f96c24d..8e7a687f 100644
--- a/apps/ui/src/i18n/locales/fr.json
+++ b/apps/ui/src/i18n/locales/fr.json
@@ -354,7 +354,6 @@
"Client address": "Adresse du client",
"Client service…": "Service client…",
"Client side": "Côté client",
- "Client-side metrics": "Métriques côté client",
"Close": "Fermer",
"Close (Esc)": "Fermer (Échap)",
"Close hierarchy": "Fermer la hiérarchie",
@@ -749,6 +748,9 @@
"Keep this version": "Conserver cette version",
"Keep this version to overwrite the other one, or load the other version to discard the turns shown here.": "Conservez cette version pour écraser l'autre, ou chargez l'autre version pour abandonner les tours affichés ici.",
"Kernel-level CPU / off-CPU profiling via eBPF agents.": "Profilage CPU / off-CPU au niveau du noyau via des agents eBPF.",
+ "Process-to-process network profiling via the Rover eBPF agent.": "Profilage réseau de processus à processus via l’agent eBPF Rover.",
+ "Go runtime pprof profiles (Go-only).": "Profils pprof du runtime Go (Go uniquement).",
+ "AI agent conversations recorded against this layer’s services.": "Conversations d’agents IA enregistrées pour les services de cette couche.",
"Key": "Clé",
"Key metrics": "Métriques clés",
"Keyword": "Mot-clé",
@@ -946,7 +948,6 @@
"No async profile tasks yet.": "Aucune tâche de profilage asynchrone pour l'instant.",
"No browser logs in this window.": "Aucun journal navigateur dans cette fenêtre.",
"No client metrics configured.": "Aucune métrique client configurée.",
- "No client-side metrics.": "Aucune métrique côté client.",
"No cluster rule configured — the topology view renders without cluster bounding boxes. Enable a rule for layers whose service names encode a cluster dimension (k8s namespace, fleet, tenant) so topology can cluster nodes accordingly.": "Aucune règle de cluster configurée — la vue de topologie s'affiche sans cadres de cluster. Activez une règle pour les couches dont les noms de services encodent une dimension de cluster (namespace k8s, flotte, tenant) afin que la topologie puisse regrou [...]
"No components enabled — toggle one on the right to populate the menu.": "Aucun composant activé — activez-en un à droite pour remplir le menu.",
"No conditions — every instance matching the name filter is shown.": "Aucune condition — toutes les instances correspondant au filtre de nom sont affichées.",
@@ -1029,7 +1030,13 @@
"No schedules captured for this task.": "Aucune planification capturée pour cette tâche.",
"No screen or request checks this — granting it has no effect.": "Aucun écran ni aucune requête ne le vérifie — l’accorder ne change rien.",
"No server metrics configured.": "Aucune métrique serveur configurée.",
- "No server-side metrics.": "Aucune métrique côté serveur.",
+ "Edge metrics": "Métriques d’arête",
+ "Each names the end of the conversation it describes. The eBPF probe watches both, and a metric that describes the exchange itself — the HTTP/1.x families — belongs to neither.": "Chacune nomme l’extrémité de la conversation qu’elle décrit. La sonde eBPF observe les deux, et une métrique qui décrit l’échange lui-même — les familles HTTP/1.x — n’appartient à aucune.",
+ "No edge metrics.": "Aucune métrique d’arête.",
+ "Side": "Côté",
+ "neither — describes the exchange": "aucun — décrit l’échange",
+ "Both sides": "Les deux côtés",
+ "No shared metrics configured.": "Aucune métrique partagée configurée.",
"No service matches this": "Aucun service ne correspond",
"No services match": "Aucun service ne correspond",
"No services reported.": "Aucun service signalé.",
@@ -1498,7 +1505,6 @@
"Server side": "Côté serveur",
"Server timezone": "Fuseau horaire serveur",
"Server-side": "Côté serveur",
- "Server-side metrics": "Métriques côté serveur",
"Service": "Service",
"Service bind": "Service bind",
"Service bind DN": "DN du service bind",
@@ -1895,7 +1901,6 @@
"Your sidebar adapts to your role: anything you can't use is simply not shown. The sections below group SkyWalking's capabilities by feature area. For each area you'll see": "Votre barre latérale s'adapte à votre rôle : tout ce que vous ne pouvez pas utiliser n'est simplement pas affiché. Les sections ci-dessous regroupent les capacités de SkyWalking par domaine fonctionnel. Pour chaque domaine, vous verrez",
"Your sidebar adapts to your role: anything you can't use is simply not shown. The sections below group SkyWalking's capabilities by feature area. For each area you'll see {menuScope} (which sidebar entries fall in the area) and {actions} (the concrete things you can do, marked per role). Multiple roles add up — a user holding more than one role gets every permission any of their roles grant.": "Votre barre latérale s'adapte à votre rôle : tout ce que vous ne pouvez pas utiliser n'est [...]
"Your username is your identity here, and your roles are resolved from it rather than stored against you. A token you hold re-reads them on every request, so removing you from the directory or configuration ends it at once. This browser session keeps the roles it was given until you sign in again.": "Votre nom d'utilisateur est votre identité ici, et vos rôles en sont déduits plutôt que stockés à votre nom. Un jeton que vous détenez les relit à chaque requête : vous retirer de l'annuai [...]
- "Zipkin endpoint unreachable": "Endpoint Zipkin inaccessible",
"Zipkin query failed.": "Échec de la requête Zipkin.",
"Zipkin trace": "Trace Zipkin",
"Zoom in": "Zoomer",
@@ -2092,7 +2097,6 @@
"edge": "lien",
"edge MQE for the process-topology detail panel. Queried under ProcessRelation when an operator clicks a process→process call.": "MQE de lien pour le panneau de détail de la topologie de processus. Interrogée sous ProcessRelation lorsqu'un opérateur clique sur un appel processus→processus.",
"edge metrics for “{from} → {to}” — queried as": "métriques de lien pour « {from} → {to} » — interrogées en tant que",
- "edge metrics queried as": "métriques de lien interrogées en tant que",
"edges": "liens",
"edit": "éditer",
"editing: {source}": "édition : {source}",
@@ -3355,6 +3359,7 @@
"OAP’s Zipkin v2 API, which also serves the OpenTelemetry spans OAP converts into Zipkin form.": "L’API Zipkin v2 d’OAP, qui sert aussi les spans OpenTelemetry qu’OAP convertit au format Zipkin.",
"Grafana Tempo’s API over the native spans. Needs OAP’s traceQL module and a configured datasource URL.": "L’API de Grafana Tempo sur les spans natifs. Nécessite le module traceQL d’OAP et une URL de source de données configurée.",
"Grafana Tempo’s API over the Zipkin spans. Needs OAP’s traceQL module and a configured datasource URL.": "L’API de Grafana Tempo sur les spans Zipkin. Nécessite le module traceQL d’OAP et une URL de source de données configurée.",
+ "Grafana Tempo’s API over the OTLP spans OAP stored as they arrived, converting nothing. Needs OAP’s traceQL module and a configured datasource URL.": "L’API de Grafana Tempo sur les spans OTLP qu’OAP a stockés tels quels, sans conversion. Nécessite le module traceQL d’OAP et une URL de source de données configurée.",
"Flags": "Drapeaux",
"The Tempo API has no notion of a layer, so it lists every service of the store. This pattern narrows the service PICKER only — result rows are never filtered, so cross-service traces still appear.": "L’API Tempo ignore la notion de couche : elle liste tous les services du magasin. Ce motif restreint uniquement le SÉLECTEUR de services — les lignes de résultats ne sont jamais filtrées, les traces inter-services restent donc visibles.",
"checking {n} statuses…": "vérification de {n} statuts…",
@@ -3368,8 +3373,10 @@
"Tempo API": "API Tempo",
"not available": "indisponible",
"no URL configured": "aucune URL configurée",
+ "no context path": "aucun chemin de contexte",
+ "not exposed": "non exposé",
+ "This OAP does not enable the {name} datasource. Switch it on with SW_TRACEQL_ENABLE_DATASOURCE_* and it appears here; until then the rows fed by it stay empty.": "Cet OAP n’active pas la source de données {name}. Activez-la avec SW_TRACEQL_ENABLE_DATASOURCE_* et elle apparaîtra ici ; d’ici là, les lignes qu’elle alimente restent vides.",
"SkyWalking-native spans": "Spans natifs SkyWalking",
- "{name} datasource unreachable": "Source de données {name} injoignable",
"Tried {url}. Enable OAP's traceQL module (SW_TRACEQL=default with the datasource switched on) and confirm oap.traceql points at its host:port and context path — {path} by default on port 3200.": "{url} a été tenté. Activez le module traceQL d’OAP (SW_TRACEQL=default avec la source de données activée) et vérifiez que oap.traceql pointe vers son host:port et son chemin de contexte — {path} par défaut sur le port 3200.",
"Trace APIs": "API de traces",
"Zipkin v2 · Tempo": "Zipkin v2 · Tempo",
@@ -3392,13 +3399,17 @@
"A status other than “ok”, “error” or “unset”.": "Un statut autre que « ok », « error » ou « unset ».",
"Span tags are those this store reported for the window being queried, so the list follows the time range.": "Les étiquettes de span sont celles que ce magasin a signalées pour la fenêtre interrogée : la liste suit donc la plage de temps.",
"ok, error or unset": "ok, error ou unset",
+ "Span kind — server, client, producer, consumer, internal, unspecified (OTLP)": "Type de span — server, client, producer, consumer, internal, unspecified (OTLP)",
"Service instance (native)": "Instance de service (native)",
- "Peer service (Zipkin)": "Service pair (Zipkin)",
+ "Service instance (OTLP)": "Instance de service (OTLP)",
+ "Peer service (Zipkin, OTLP)": "Service pair (Zipkin, OTLP)",
"Trace duration — >, >=, <, <= with us/µs/ms/s/m/h": "Durée de la trace — >, >=, <, <= avec us/µs/ms/s/m/h",
"Zipkin Traces": "Traces Zipkin",
"TraceQL - Native": "TraceQL - Native",
"TraceQL - Zipkin": "TraceQL - Zipkin",
+ "TraceQL - OTLP": "TraceQL - OTLP",
"Zipkin spans": "Spans Zipkin",
+ "Natively stored OTLP spans": "Spans OTLP stockés nativement",
"Menu name": "Nom dans le menu",
"Ignore case": "Ignorer la casse",
"Horizon will not run this expression, so the picker keeps showing every service. It either does not compile, or it can backtrack exponentially — avoid a repeat applied to a group that itself repeats or branches, such as (a+)+.": "Horizon n'exécutera pas cette expression, le sélecteur continue donc d'afficher tous les services. Soit elle ne compile pas, soit elle peut revenir en arrière de façon exponentielle — évitez une répétition appliquée à un groupe qui se répète ou se ramifie lui [...]
@@ -3406,5 +3417,8 @@
"Open the calendar": "Ouvrir le calendrier",
"Previous month": "Mois précédent",
"Next month": "Mois suivant",
- "Now": "Maintenant"
+ "Now": "Maintenant",
+ "edge metrics queried as": "métriques de lien interrogées en tant que",
+ "No server-side metrics.": "Aucune métrique côté serveur.",
+ "No client-side metrics.": "Aucune métrique côté client."
}
diff --git a/apps/ui/src/i18n/locales/ja.json b/apps/ui/src/i18n/locales/ja.json
index 4251b7b4..4acba6e8 100644
--- a/apps/ui/src/i18n/locales/ja.json
+++ b/apps/ui/src/i18n/locales/ja.json
@@ -354,7 +354,6 @@
"Client address": "クライアントアドレス",
"Client service…": "クライアントサービス…",
"Client side": "クライアント側",
- "Client-side metrics": "クライアント側メトリクス",
"Close": "閉じる",
"Close (Esc)": "閉じる(Esc)",
"Close hierarchy": "階層を閉じる",
@@ -749,6 +748,9 @@
"Keep this version": "このバージョンを残す",
"Keep this version to overwrite the other one, or load the other version to discard the turns shown here.": "このバージョンを残すともう一方を上書きします。もう一方のバージョンを読み込むと、ここに表示されているやり取りは破棄されます。",
"Kernel-level CPU / off-CPU profiling via eBPF agents.": "eBPF エージェントによるカーネルレベルの CPU / off-CPU プロファイリングです。",
+ "Process-to-process network profiling via the Rover eBPF agent.": "Rover eBPF エージェントによるプロセス間ネットワークプロファイリング。",
+ "Go runtime pprof profiles (Go-only).": "Go ランタイムの pprof プロファイル(Go のみ)。",
+ "AI agent conversations recorded against this layer’s services.": "このレイヤーのサービスに記録された AI エージェントの会話。",
"Key": "キー",
"Key metrics": "主要メトリクス",
"Keyword": "キーワード",
@@ -946,7 +948,6 @@
"No async profile tasks yet.": "async プロファイルタスクはまだありません。",
"No browser logs in this window.": "このウィンドウにブラウザログはありません。",
"No client metrics configured.": "クライアント側メトリクスが未設定です。",
- "No client-side metrics.": "クライアント側メトリクスがありません。",
"No cluster rule configured — the topology view renders without cluster bounding boxes. Enable a rule for layers whose service names encode a cluster dimension (k8s namespace, fleet, tenant) so topology can cluster nodes accordingly.": "クラスタルールが未設定です — トポロジービューはクラスタの囲みなしで描画されます。サービス名にクラスタ軸(k8s namespace、フリート、テナント)が含まれるレイヤーでルールを有効にすると、トポロジーがノードをそれに沿ってまとめます。",
"No components enabled — toggle one on the right to populate the menu.": "有効なコンポーネントがありません — 右側でオンにするとメニューに表示されます。",
"No conditions — every instance matching the name filter is shown.": "条件はありません。名前フィルターに一致するすべてのインスタンスが表示されます。",
@@ -1029,7 +1030,13 @@
"No schedules captured for this task.": "このタスクではスケジュールが取得されていません。",
"No screen or request checks this — granting it has no effect.": "この権限を確認する画面もリクエストもありません。付与しても効果はありません。",
"No server metrics configured.": "サーバー側メトリクスが未設定です。",
- "No server-side metrics.": "サーバー側メトリクスがありません。",
+ "Edge metrics": "エッジメトリクス",
+ "Each names the end of the conversation it describes. The eBPF probe watches both, and a metric that describes the exchange itself — the HTTP/1.x families — belongs to neither.": "各メトリクスは、自身が表す通信の端を指定します。eBPF プローブは両端を観測し、やり取りそのものを表すメトリクス(HTTP/1.x のファミリー)はどちらにも属しません。",
+ "No edge metrics.": "エッジメトリクスがありません。",
+ "Side": "側",
+ "neither — describes the exchange": "どちらでもない — やり取りを表す",
+ "Both sides": "両側",
+ "No shared metrics configured.": "共有メトリクスが設定されていません。",
"No service matches this": "該当するサービスはありません",
"No services match": "一致するサービスがありません",
"No services reported.": "報告されたサービスはありません。",
@@ -1498,7 +1505,6 @@
"Server side": "サーバー側",
"Server timezone": "サーバーのタイムゾーン",
"Server-side": "サーバー側",
- "Server-side metrics": "サーバー側メトリクス",
"Service": "Service",
"Service bind": "Service bind",
"Service bind DN": "Service bind DN",
@@ -1895,7 +1901,6 @@
"Your sidebar adapts to your role: anything you can't use is simply not shown. The sections below group SkyWalking's capabilities by feature area. For each area you'll see": "サイドバーはロールに合わせて変化します: 使用できないものは単に表示されません。以下のセクションでは SkyWalking の機能を領域ごとにグループ化しています。各領域では次が表示されます:",
"Your sidebar adapts to your role: anything you can't use is simply not shown. The sections below group SkyWalking's capabilities by feature area. For each area you'll see {menuScope} (which sidebar entries fall in the area) and {actions} (the concrete things you can do, marked per role). Multiple roles add up — a user holding more than one role gets every permission any of their roles grant.": "サイドバーはロールに合わせて変化します: 使用できないものは単に表示されません。以下のセクションでは SkyWalking の機能を領域ごとにグループ化しています。各領域では {me [...]
"Your username is your identity here, and your roles are resolved from it rather than stored against you. A token you hold re-reads them on every request, so removing you from the directory or configuration ends it at once. This browser session keeps the roles it was given until you sign in again.": "ここではユーザー名があなたの識別子であり、ロールはあなたに保存されるのではなく、そこから解決されます。保有しているトークンはリクエストのたびにロールを読み直すため、ディレクトリや設定からあなたを削除すると直ちに無効になります。このブラウザーセッションは、再度サインインするまで付与されたロールを保持します。",
- "Zipkin endpoint unreachable": "Zipkin エンドポイントに到達できません",
"Zipkin query failed.": "Zipkin クエリに失敗しました。",
"Zipkin trace": "Zipkin トレース",
"Zoom in": "拡大",
@@ -2092,7 +2097,6 @@
"edge": "エッジ",
"edge MQE for the process-topology detail panel. Queried under ProcessRelation when an operator clicks a process→process call.": "プロセストポロジーの詳細パネル用のエッジ MQE です。プロセス→プロセスの呼び出しをクリックしたときに ProcessRelation スコープで問い合わせます。",
"edge metrics for “{from} → {to}” — queried as": "「{from} → {to}」のエッジメトリクス — 次として問い合わせます:",
- "edge metrics queried as": "エッジメトリクスの問い合わせ対象:",
"edges": "エッジ",
"edit": "編集",
"editing: {source}": "編集中: {source}",
@@ -3355,6 +3359,7 @@
"OAP’s Zipkin v2 API, which also serves the OpenTelemetry spans OAP converts into Zipkin form.": "OAP の Zipkin v2 API。OAP が Zipkin 形式に変換した OpenTelemetry のスパンもここから返されます。",
"Grafana Tempo’s API over the native spans. Needs OAP’s traceQL module and a configured datasource URL.": "ネイティブスパンに対する Grafana Tempo の API。OAP の traceQL モジュールとデータソース URL の設定が必要です。",
"Grafana Tempo’s API over the Zipkin spans. Needs OAP’s traceQL module and a configured datasource URL.": "Zipkin スパンに対する Grafana Tempo の API。OAP の traceQL モジュールとデータソース URL の設定が必要です。",
+ "Grafana Tempo’s API over the OTLP spans OAP stored as they arrived, converting nothing. Needs OAP’s traceQL module and a configured datasource URL.": "OAP が届いたまま保存した OTLP スパンに対する Grafana Tempo の API。変換は行われません。OAP の traceQL モジュールとデータソース URL の設定が必要です。",
"Flags": "フラグ",
"The Tempo API has no notion of a layer, so it lists every service of the store. This pattern narrows the service PICKER only — result rows are never filtered, so cross-service traces still appear.": "Tempo API にレイヤーの概念はなく、ストア内のすべてのサービスを列挙します。このパターンが絞り込むのはサービスの選択リストだけで、結果の行は一切フィルタされないため、サービスをまたぐトレースもそのまま表示されます。",
"checking {n} statuses…": "{n} 件のステータスを確認中…",
@@ -3368,8 +3373,10 @@
"Tempo API": "Tempo API",
"not available": "利用不可",
"no URL configured": "URL 未設定",
+ "no context path": "コンテキストパス未設定",
+ "not exposed": "未提供",
+ "This OAP does not enable the {name} datasource. Switch it on with SW_TRACEQL_ENABLE_DATASOURCE_* and it appears here; until then the rows fed by it stay empty.": "この OAP は {name} データソースを有効にしていません。SW_TRACEQL_ENABLE_DATASOURCE_* で有効にするとここに表示されます。それまでは、このデータソースが供給する行は空のままです。",
"SkyWalking-native spans": "SkyWalking ネイティブ span",
- "{name} datasource unreachable": "データソース {name} に到達できません",
"Tried {url}. Enable OAP's traceQL module (SW_TRACEQL=default with the datasource switched on) and confirm oap.traceql points at its host:port and context path — {path} by default on port 3200.": "{url} を試しました。OAP の traceQL モジュールを有効化し(SW_TRACEQL=default とデータソースの有効化)、oap.traceql がその host:port とコンテキストパスを指しているか確認してください。既定はポート 3200 の {path} です。",
"Trace APIs": "トレース API",
"Zipkin v2 · Tempo": "Zipkin v2・Tempo",
@@ -3392,13 +3399,17 @@
"A status other than “ok”, “error” or “unset”.": "「ok」「error」「unset」以外の status。",
"Span tags are those this store reported for the window being queried, so the list follows the time range.": "スパンタグは、このストアが照会対象の期間について報告したものです。そのため一覧は時間範囲に従います。",
"ok, error or unset": "ok、error、unset",
+ "Span kind — server, client, producer, consumer, internal, unspecified (OTLP)": "スパン種別 — server、client、producer、consumer、internal、unspecified(OTLP)",
"Service instance (native)": "サービスインスタンス(ネイティブ)",
- "Peer service (Zipkin)": "ピアサービス(Zipkin)",
+ "Service instance (OTLP)": "サービスインスタンス(OTLP)",
+ "Peer service (Zipkin, OTLP)": "対向サービス(Zipkin、OTLP)",
"Trace duration — >, >=, <, <= with us/µs/ms/s/m/h": "トレース時間 — us/µs/ms/s/m/h を使って >、>=、<、<=",
"Zipkin Traces": "Zipkin トレース",
"TraceQL - Native": "TraceQL - Native",
"TraceQL - Zipkin": "TraceQL - Zipkin",
+ "TraceQL - OTLP": "TraceQL - OTLP",
"Zipkin spans": "Zipkin スパン",
+ "Natively stored OTLP spans": "ネイティブ保存された OTLP スパン",
"Menu name": "メニュー名",
"Ignore case": "大文字と小文字を区別しない",
"Horizon will not run this expression, so the picker keeps showing every service. It either does not compile, or it can backtrack exponentially — avoid a repeat applied to a group that itself repeats or branches, such as (a+)+.": "Horizon はこの式を実行しないため、ピッカーには引き続きすべてのサービスが表示されます。コンパイルできないか、バックトラックが指数的に増える可能性があります。(a+)+ のように、繰り返しや分岐を含むグループへの繰り返しは避けてください。",
@@ -3406,5 +3417,8 @@
"Open the calendar": "カレンダーを開く",
"Previous month": "前の月",
"Next month": "次の月",
- "Now": "現在"
+ "Now": "現在",
+ "edge metrics queried as": "エッジメトリクスの問い合わせ対象:",
+ "No server-side metrics.": "サーバー側メトリクスがありません。",
+ "No client-side metrics.": "クライアント側メトリクスがありません。"
}
diff --git a/apps/ui/src/i18n/locales/ko.json b/apps/ui/src/i18n/locales/ko.json
index df091318..c46dffa0 100644
--- a/apps/ui/src/i18n/locales/ko.json
+++ b/apps/ui/src/i18n/locales/ko.json
@@ -354,7 +354,6 @@
"Client address": "클라이언트 주소",
"Client service…": "클라이언트 서비스…",
"Client side": "클라이언트 측",
- "Client-side metrics": "클라이언트 측 메트릭",
"Close": "닫기",
"Close (Esc)": "닫기 (Esc)",
"Close hierarchy": "계층 닫기",
@@ -749,6 +748,9 @@
"Keep this version": "이 버전 유지",
"Keep this version to overwrite the other one, or load the other version to discard the turns shown here.": "이 버전을 유지하면 다른 버전을 덮어쓰고, 다른 버전을 불러오면 여기에 표시된 대화가 사라집니다.",
"Kernel-level CPU / off-CPU profiling via eBPF agents.": "eBPF 에이전트를 통한 커널 수준 CPU / off-CPU 프로파일링입니다.",
+ "Process-to-process network profiling via the Rover eBPF agent.": "Rover eBPF 에이전트를 통한 프로세스 간 네트워크 프로파일링.",
+ "Go runtime pprof profiles (Go-only).": "Go 런타임 pprof 프로파일 (Go 전용).",
+ "AI agent conversations recorded against this layer’s services.": "이 레이어의 서비스에 기록된 AI 에이전트 대화.",
"Key": "키",
"Key metrics": "주요 메트릭",
"Keyword": "키워드",
@@ -946,7 +948,6 @@
"No async profile tasks yet.": "아직 async 프로파일 작업이 없습니다.",
"No browser logs in this window.": "이 구간에 브라우저 로그가 없습니다.",
"No client metrics configured.": "설정된 클라이언트 메트릭이 없습니다.",
- "No client-side metrics.": "클라이언트 측 메트릭이 없습니다.",
"No cluster rule configured — the topology view renders without cluster bounding boxes. Enable a rule for layers whose service names encode a cluster dimension (k8s namespace, fleet, tenant) so topology can cluster nodes accordingly.": "설정된 클러스터 규칙이 없습니다 — 토폴로지 화면이 클러스터 경계 상자 없이 그려집니다. 서비스 이름에 클러스터 차원(k8s 네임스페이스, 플릿, 테넌트)이 들어 있는 레이어라면 규칙을 켜서 노드를 그에 맞게 묶으세요.",
"No components enabled — toggle one on the right to populate the menu.": "활성화된 구성 요소가 없습니다 — 오른쪽에서 하나를 켜면 메뉴가 채워집니다.",
"No conditions — every instance matching the name filter is shown.": "조건이 없습니다. 이름 필터에 일치하는 모든 인스턴스가 표시됩니다.",
@@ -1029,7 +1030,13 @@
"No schedules captured for this task.": "이 작업에서 캡처된 스케줄이 없습니다.",
"No screen or request checks this — granting it has no effect.": "이 권한을 확인하는 화면이나 요청이 없습니다. 부여해도 아무 변화가 없습니다.",
"No server metrics configured.": "설정된 서버 메트릭이 없습니다.",
- "No server-side metrics.": "서버 측 메트릭이 없습니다.",
+ "Edge metrics": "엣지 메트릭",
+ "Each names the end of the conversation it describes. The eBPF probe watches both, and a metric that describes the exchange itself — the HTTP/1.x families — belongs to neither.": "각 메트릭은 자신이 설명하는 통신의 끝단을 지정합니다. eBPF 프로브는 양쪽을 모두 관측하며, 교환 자체를 설명하는 메트릭(HTTP/1.x 계열)은 어느 쪽에도 속하지 않습니다.",
+ "No edge metrics.": "엣지 메트릭이 없습니다.",
+ "Side": "측",
+ "neither — describes the exchange": "해당 없음 — 교환 자체를 설명",
+ "Both sides": "양측",
+ "No shared metrics configured.": "구성된 공유 메트릭이 없습니다.",
"No service matches this": "해당하는 서비스가 없습니다",
"No services match": "일치하는 서비스가 없습니다",
"No services reported.": "보고된 서비스가 없습니다.",
@@ -1498,7 +1505,6 @@
"Server side": "서버 측",
"Server timezone": "서버 시간대",
"Server-side": "서버 측",
- "Server-side metrics": "서버 측 메트릭",
"Service": "서비스",
"Service bind": "서비스 바인드",
"Service bind DN": "서비스 바인드 DN",
@@ -1895,7 +1901,6 @@
"Your sidebar adapts to your role: anything you can't use is simply not shown. The sections below group SkyWalking's capabilities by feature area. For each area you'll see": "사이드바는 역할에 맞춰 자동으로 조정됩니다: 사용할 수 없는 항목은 표시되지 않습니다. 아래 섹션은 SkyWalking의 기능을 영역별로 분류합니다. 각 영역에서 다음을 볼 수 있습니다:",
"Your sidebar adapts to your role: anything you can't use is simply not shown. The sections below group SkyWalking's capabilities by feature area. For each area you'll see {menuScope} (which sidebar entries fall in the area) and {actions} (the concrete things you can do, marked per role). Multiple roles add up — a user holding more than one role gets every permission any of their roles grant.": "사이드바는 역할에 맞춰 자동으로 조정됩니다: 사용할 수 없는 항목은 표시되지 않습니다. 아래 섹션은 SkyWalking의 기능을 영역별로 분류합니다. 각 영역에서는 [...]
"Your username is your identity here, and your roles are resolved from it rather than stored against you. A token you hold re-reads them on every request, so removing you from the directory or configuration ends it at once. This browser session keeps the roles it was given until you sign in again.": "여기서는 사용자 이름이 곧 신원이며, 역할은 사용자에게 저장되는 것이 아니라 이름으로부터 확인됩니다. 보유한 토큰은 요청마다 역할을 다시 읽으므로 디렉터리나 설정에서 계정을 제거하면 즉시 무효화됩니다. 이 브라우저 세션은 다시 로그인할 때까지 부여받은 역할을 유지합니다.",
- "Zipkin endpoint unreachable": "Zipkin 엔드포인트에 연결할 수 없음",
"Zipkin query failed.": "Zipkin 쿼리를 실행하지 못했습니다.",
"Zipkin trace": "Zipkin 트레이스",
"Zoom in": "확대",
@@ -2092,7 +2097,6 @@
"edge": "간선",
"edge MQE for the process-topology detail panel. Queried under ProcessRelation when an operator clicks a process→process call.": "프로세스 토폴로지 상세 패널에 쓰이는 간선 MQE입니다. 운영자가 프로세스→프로세스 호출을 클릭하면 ProcessRelation 범위로 조회됩니다.",
"edge metrics for “{from} → {to}” — queried as": "“{from} → {to}” 간선 메트릭 — 조회 대상:",
- "edge metrics queried as": "간선 메트릭 조회 대상:",
"edges": "간선",
"edit": "편집",
"editing: {source}": "편집 중: {source}",
@@ -3355,6 +3359,7 @@
"OAP’s Zipkin v2 API, which also serves the OpenTelemetry spans OAP converts into Zipkin form.": "OAP의 Zipkin v2 API로, OAP가 Zipkin 형식으로 변환한 OpenTelemetry 스팬도 함께 제공합니다.",
"Grafana Tempo’s API over the native spans. Needs OAP’s traceQL module and a configured datasource URL.": "네이티브 스팬에 대한 Grafana Tempo API. OAP의 traceQL 모듈과 데이터소스 URL 설정이 필요합니다.",
"Grafana Tempo’s API over the Zipkin spans. Needs OAP’s traceQL module and a configured datasource URL.": "Zipkin 스팬에 대한 Grafana Tempo API. OAP의 traceQL 모듈과 데이터소스 URL 설정이 필요합니다.",
+ "Grafana Tempo’s API over the OTLP spans OAP stored as they arrived, converting nothing. Needs OAP’s traceQL module and a configured datasource URL.": "OAP가 도착한 그대로 저장한 OTLP 스팬에 대한 Grafana Tempo API. 변환하지 않습니다. OAP의 traceQL 모듈과 데이터소스 URL 설정이 필요합니다.",
"Flags": "플래그",
"The Tempo API has no notion of a layer, so it lists every service of the store. This pattern narrows the service PICKER only — result rows are never filtered, so cross-service traces still appear.": "Tempo API에는 레이어 개념이 없어 저장소의 모든 서비스를 나열합니다. 이 패턴은 서비스 선택 목록만 좁히며 결과 행은 걸러내지 않으므로, 서비스를 넘나드는 트레이스는 그대로 표시됩니다.",
"checking {n} statuses…": "{n}개의 상태를 확인하는 중…",
@@ -3368,8 +3373,10 @@
"Tempo API": "Tempo API",
"not available": "사용할 수 없음",
"no URL configured": "URL이 설정되지 않음",
+ "no context path": "컨텍스트 경로 없음",
+ "not exposed": "미제공",
+ "This OAP does not enable the {name} datasource. Switch it on with SW_TRACEQL_ENABLE_DATASOURCE_* and it appears here; until then the rows fed by it stay empty.": "이 OAP는 {name} 데이터소스를 활성화하지 않았습니다. SW_TRACEQL_ENABLE_DATASOURCE_* 로 켜면 여기에 표시되며, 그전까지 이 데이터소스가 채우는 행은 비어 있습니다.",
"SkyWalking-native spans": "SkyWalking 네이티브 스팬",
- "{name} datasource unreachable": "{name} 데이터소스에 연결할 수 없음",
"Tried {url}. Enable OAP's traceQL module (SW_TRACEQL=default with the datasource switched on) and confirm oap.traceql points at its host:port and context path — {path} by default on port 3200.": "{url}을(를) 시도했습니다. OAP의 traceQL 모듈을 활성화하고(SW_TRACEQL=default 및 데이터소스 켜기), oap.traceql이 해당 host:port와 컨텍스트 경로를 가리키는지 확인하세요 — 기본값은 3200 포트의 {path}입니다.",
"Trace APIs": "트레이스 API",
"Zipkin v2 · Tempo": "Zipkin v2 · Tempo",
@@ -3392,13 +3399,17 @@
"A status other than “ok”, “error” or “unset”.": "“ok”, “error”, “unset”이 아닌 status.",
"Span tags are those this store reported for the window being queried, so the list follows the time range.": "스팬 태그는 이 저장소가 조회 구간에 대해 보고한 것이므로, 목록은 시간 범위를 따릅니다.",
"ok, error or unset": "ok, error 또는 unset",
+ "Span kind — server, client, producer, consumer, internal, unspecified (OTLP)": "스팬 종류 — server, client, producer, consumer, internal, unspecified (OTLP)",
"Service instance (native)": "서비스 인스턴스(네이티브)",
- "Peer service (Zipkin)": "피어 서비스(Zipkin)",
+ "Service instance (OTLP)": "서비스 인스턴스 (OTLP)",
+ "Peer service (Zipkin, OTLP)": "피어 서비스 (Zipkin, OTLP)",
"Trace duration — >, >=, <, <= with us/µs/ms/s/m/h": "트레이스 소요 시간 — us/µs/ms/s/m/h 와 함께 >, >=, <, <=",
"Zipkin Traces": "Zipkin 트레이스",
"TraceQL - Native": "TraceQL - Native",
"TraceQL - Zipkin": "TraceQL - Zipkin",
+ "TraceQL - OTLP": "TraceQL - OTLP",
"Zipkin spans": "Zipkin 스팬",
+ "Natively stored OTLP spans": "네이티브로 저장된 OTLP 스팬",
"Menu name": "메뉴 이름",
"Ignore case": "대소문자 무시",
"Horizon will not run this expression, so the picker keeps showing every service. It either does not compile, or it can backtrack exponentially — avoid a repeat applied to a group that itself repeats or branches, such as (a+)+.": "Horizon은 이 식을 실행하지 않으므로 선택기에는 모든 서비스가 계속 표시됩니다. 컴파일되지 않거나 역추적이 기하급수적으로 늘어날 수 있습니다. (a+)+ 처럼 반복이나 분기를 포함한 그룹에 반복을 적용하지 마세요.",
@@ -3406,5 +3417,8 @@
"Open the calendar": "달력 열기",
"Previous month": "이전 달",
"Next month": "다음 달",
- "Now": "지금"
+ "Now": "지금",
+ "edge metrics queried as": "간선 메트릭 조회 대상:",
+ "No server-side metrics.": "서버 측 메트릭이 없습니다.",
+ "No client-side metrics.": "클라이언트 측 메트릭이 없습니다."
}
diff --git a/apps/ui/src/i18n/locales/pt.json b/apps/ui/src/i18n/locales/pt.json
index aaf78597..7cc47007 100644
--- a/apps/ui/src/i18n/locales/pt.json
+++ b/apps/ui/src/i18n/locales/pt.json
@@ -354,7 +354,6 @@
"Client address": "Endereço do cliente",
"Client service…": "Serviço cliente…",
"Client side": "Lado cliente",
- "Client-side metrics": "Métricas do lado cliente",
"Close": "Fechar",
"Close (Esc)": "Fechar (Esc)",
"Close hierarchy": "Fechar a hierarquia",
@@ -749,6 +748,9 @@
"Keep this version": "Manter esta versão",
"Keep this version to overwrite the other one, or load the other version to discard the turns shown here.": "Mantenha esta versão para sobrescrever a outra, ou carregue a outra versão para descartar os turnos exibidos aqui.",
"Kernel-level CPU / off-CPU profiling via eBPF agents.": "Profiling de CPU / off-CPU em nível de kernel via agentes eBPF.",
+ "Process-to-process network profiling via the Rover eBPF agent.": "Perfilamento de rede entre processos através do agente eBPF Rover.",
+ "Go runtime pprof profiles (Go-only).": "Perfis pprof do runtime Go (apenas Go).",
+ "AI agent conversations recorded against this layer’s services.": "Conversas de agentes de IA registadas para os serviços desta camada.",
"Key": "Chave",
"Key metrics": "Métricas principais",
"Keyword": "Palavra-chave",
@@ -946,7 +948,6 @@
"No async profile tasks yet.": "Ainda não há tarefas de profiling assíncrono.",
"No browser logs in this window.": "Nenhum log de navegador nesta janela.",
"No client metrics configured.": "Nenhuma métrica de cliente configurada.",
- "No client-side metrics.": "Nenhuma métrica do lado cliente.",
"No cluster rule configured — the topology view renders without cluster bounding boxes. Enable a rule for layers whose service names encode a cluster dimension (k8s namespace, fleet, tenant) so topology can cluster nodes accordingly.": "Nenhuma regra de cluster configurada — a topologia é renderizada sem caixas de agrupamento. Habilite uma regra para camadas cujos nomes de serviço codificam uma dimensão de cluster (namespace k8s, fleet, tenant), para que a topologia agrupe os nós de acordo.",
"No components enabled — toggle one on the right to populate the menu.": "Nenhum componente habilitado — ative um à direita para preencher o menu.",
"No conditions — every instance matching the name filter is shown.": "Sem condições — todas as instâncias que correspondem ao filtro de nome são mostradas.",
@@ -1029,7 +1030,13 @@
"No schedules captured for this task.": "Nenhum agendamento capturado para esta tarefa.",
"No screen or request checks this — granting it has no effect.": "Nenhuma tela ou requisição verifica isto — concedê-lo não muda nada.",
"No server metrics configured.": "Nenhuma métrica de servidor configurada.",
- "No server-side metrics.": "Nenhuma métrica do lado servidor.",
+ "Edge metrics": "Métricas de aresta",
+ "Each names the end of the conversation it describes. The eBPF probe watches both, and a metric that describes the exchange itself — the HTTP/1.x families — belongs to neither.": "Cada uma indica a extremidade da conversa que descreve. A sonda eBPF observa ambas, e uma métrica que descreve a própria troca — as famílias HTTP/1.x — não pertence a nenhuma.",
+ "No edge metrics.": "Sem métricas de aresta.",
+ "Side": "Lado",
+ "neither — describes the exchange": "nenhum — descreve a troca",
+ "Both sides": "Ambos os lados",
+ "No shared metrics configured.": "Nenhuma métrica partilhada configurada.",
"No service matches this": "Nenhum serviço corresponde a isto",
"No services match": "Nenhum serviço corresponde",
"No services reported.": "Nenhum serviço reportado.",
@@ -1498,7 +1505,6 @@
"Server side": "Lado servidor",
"Server timezone": "Fuso horário do servidor",
"Server-side": "No servidor",
- "Server-side metrics": "Métricas do lado servidor",
"Service": "Serviço",
"Service bind": "Service bind",
"Service bind DN": "DN do service bind",
@@ -1895,7 +1901,6 @@
"Your sidebar adapts to your role: anything you can't use is simply not shown. The sections below group SkyWalking's capabilities by feature area. For each area you'll see": "Sua barra lateral se adapta ao seu papel: o que você não pode usar simplesmente não é exibido. As seções abaixo agrupam as capacidades do SkyWalking por área funcional. Para cada área, você verá",
"Your sidebar adapts to your role: anything you can't use is simply not shown. The sections below group SkyWalking's capabilities by feature area. For each area you'll see {menuScope} (which sidebar entries fall in the area) and {actions} (the concrete things you can do, marked per role). Multiple roles add up — a user holding more than one role gets every permission any of their roles grant.": "Sua barra lateral se adapta ao seu papel: o que você não pode usar simplesmente não é exibi [...]
"Your username is your identity here, and your roles are resolved from it rather than stored against you. A token you hold re-reads them on every request, so removing you from the directory or configuration ends it at once. This browser session keeps the roles it was given until you sign in again.": "O seu nome de utilizador é a sua identidade aqui, e os seus papéis são resolvidos a partir dele em vez de ficarem guardados consigo. Um token que detenha volta a lê-los em cada pedido, pel [...]
- "Zipkin endpoint unreachable": "Endpoint Zipkin inacessível",
"Zipkin query failed.": "A consulta ao Zipkin falhou.",
"Zipkin trace": "Trace do Zipkin",
"Zoom in": "Aproximar",
@@ -2092,7 +2097,6 @@
"edge": "aresta",
"edge MQE for the process-topology detail panel. Queried under ProcessRelation when an operator clicks a process→process call.": "MQE de aresta para o painel de detalhes da topologia de processos. Consultado sob ProcessRelation quando um operador clica em uma chamada processo→processo.",
"edge metrics for “{from} → {to}” — queried as": "métricas de aresta de “{from} → {to}” — consultadas como",
- "edge metrics queried as": "métricas de aresta consultadas como",
"edges": "arestas",
"edit": "editar",
"editing: {source}": "editando: {source}",
@@ -3355,6 +3359,7 @@
"OAP’s Zipkin v2 API, which also serves the OpenTelemetry spans OAP converts into Zipkin form.": "A API Zipkin v2 do OAP, que também serve os spans do OpenTelemetry que o OAP converte para o formato Zipkin.",
"Grafana Tempo’s API over the native spans. Needs OAP’s traceQL module and a configured datasource URL.": "A API do Grafana Tempo sobre os spans nativos. Requer o módulo traceQL do OAP e uma URL de fonte de dados configurada.",
"Grafana Tempo’s API over the Zipkin spans. Needs OAP’s traceQL module and a configured datasource URL.": "A API do Grafana Tempo sobre os spans do Zipkin. Requer o módulo traceQL do OAP e uma URL de fonte de dados configurada.",
+ "Grafana Tempo’s API over the OTLP spans OAP stored as they arrived, converting nothing. Needs OAP’s traceQL module and a configured datasource URL.": "A API do Grafana Tempo sobre os spans OTLP que o OAP armazenou como chegaram, sem converter nada. Requer o módulo traceQL do OAP e uma URL de origem de dados configurada.",
"Flags": "Flags",
"The Tempo API has no notion of a layer, so it lists every service of the store. This pattern narrows the service PICKER only — result rows are never filtered, so cross-service traces still appear.": "A API do Tempo não conhece o conceito de camada, então lista todos os serviços do armazenamento. Este padrão restringe apenas o SELETOR de serviços — as linhas de resultado nunca são filtradas, portanto traces entre serviços continuam aparecendo.",
"checking {n} statuses…": "verificando {n} status…",
@@ -3368,8 +3373,10 @@
"Tempo API": "API do Tempo",
"not available": "indisponível",
"no URL configured": "nenhuma URL configurada",
+ "no context path": "sem caminho de contexto",
+ "not exposed": "não exposto",
+ "This OAP does not enable the {name} datasource. Switch it on with SW_TRACEQL_ENABLE_DATASOURCE_* and it appears here; until then the rows fed by it stay empty.": "Este OAP não ativa a origem de dados {name}. Ative-a com SW_TRACEQL_ENABLE_DATASOURCE_* e ela aparecerá aqui; até lá, as linhas que dependem dela ficam vazias.",
"SkyWalking-native spans": "Spans nativos do SkyWalking",
- "{name} datasource unreachable": "Fonte de dados {name} inacessível",
"Tried {url}. Enable OAP's traceQL module (SW_TRACEQL=default with the datasource switched on) and confirm oap.traceql points at its host:port and context path — {path} by default on port 3200.": "Tentou-se {url}. Ative o módulo traceQL do OAP (SW_TRACEQL=default com a fonte de dados ligada) e confirme que oap.traceql aponta para o host:porta e o caminho de contexto dele — {path} por padrão na porta 3200.",
"Trace APIs": "APIs de trace",
"Zipkin v2 · Tempo": "Zipkin v2 · Tempo",
@@ -3392,13 +3399,17 @@
"A status other than “ok”, “error” or “unset”.": "Um status diferente de «ok», «error» ou «unset».",
"Span tags are those this store reported for the window being queried, so the list follows the time range.": "As etiquetas de span são as que este repositório reportou para a janela consultada, pelo que a lista acompanha o intervalo de tempo.",
"ok, error or unset": "ok, error ou unset",
+ "Span kind — server, client, producer, consumer, internal, unspecified (OTLP)": "Tipo de span — server, client, producer, consumer, internal, unspecified (OTLP)",
"Service instance (native)": "Instância de serviço (nativa)",
- "Peer service (Zipkin)": "Serviço par (Zipkin)",
+ "Service instance (OTLP)": "Instância do serviço (OTLP)",
+ "Peer service (Zipkin, OTLP)": "Serviço par (Zipkin, OTLP)",
"Trace duration — >, >=, <, <= with us/µs/ms/s/m/h": "Duração do trace — >, >=, <, <= com us/µs/ms/s/m/h",
"Zipkin Traces": "Traces do Zipkin",
"TraceQL - Native": "TraceQL - Native",
"TraceQL - Zipkin": "TraceQL - Zipkin",
+ "TraceQL - OTLP": "TraceQL - OTLP",
"Zipkin spans": "Spans do Zipkin",
+ "Natively stored OTLP spans": "Spans OTLP armazenados nativamente",
"Menu name": "Nome no menu",
"Ignore case": "Ignorar maiúsculas",
"Horizon will not run this expression, so the picker keeps showing every service. It either does not compile, or it can backtrack exponentially — avoid a repeat applied to a group that itself repeats or branches, such as (a+)+.": "O Horizon não executará esta expressão, por isso o seletor continua a mostrar todos os serviços. Ou não compila, ou pode retroceder exponencialmente — evite uma repetição aplicada a um grupo que se repete ou ramifica, como (a+)+.",
@@ -3406,5 +3417,8 @@
"Open the calendar": "Abrir o calendário",
"Previous month": "Mês anterior",
"Next month": "Mês seguinte",
- "Now": "Agora"
+ "Now": "Agora",
+ "edge metrics queried as": "métricas de aresta consultadas como",
+ "No server-side metrics.": "Nenhuma métrica do lado servidor.",
+ "No client-side metrics.": "Nenhuma métrica do lado cliente."
}
diff --git a/apps/ui/src/i18n/locales/zh-CN.json b/apps/ui/src/i18n/locales/zh-CN.json
index b40a9428..6e0a04b9 100644
--- a/apps/ui/src/i18n/locales/zh-CN.json
+++ b/apps/ui/src/i18n/locales/zh-CN.json
@@ -354,7 +354,6 @@
"Client address": "客户端地址",
"Client service…": "客户端服务…",
"Client side": "客户端",
- "Client-side metrics": "客户端指标",
"Close": "关闭",
"Close (Esc)": "关闭(Esc)",
"Close hierarchy": "关闭层级关系",
@@ -749,6 +748,9 @@
"Keep this version": "保留此版本",
"Keep this version to overwrite the other one, or load the other version to discard the turns shown here.": "保留此版本会覆盖另一个版本;载入另一个版本则会丢弃这里显示的对话轮次。",
"Kernel-level CPU / off-CPU profiling via eBPF agents.": "通过 eBPF 探针进行内核级的 CPU / off-CPU 剖析。",
+ "Process-to-process network profiling via the Rover eBPF agent.": "通过 Rover eBPF 探针进行的进程间网络性能分析。",
+ "Go runtime pprof profiles (Go-only).": "Go 运行时 pprof 性能分析(仅 Go)。",
+ "AI agent conversations recorded against this layer’s services.": "记录在该层服务上的 AI 智能体会话。",
"Key": "键",
"Key metrics": "关键指标",
"Keyword": "关键词",
@@ -946,7 +948,6 @@
"No async profile tasks yet.": "暂无异步剖析任务。",
"No browser logs in this window.": "此时间窗口内无浏览器日志。",
"No client metrics configured.": "未配置客户端指标。",
- "No client-side metrics.": "尚无客户端指标。",
"No cluster rule configured — the topology view renders without cluster bounding boxes. Enable a rule for layers whose service names encode a cluster dimension (k8s namespace, fleet, tenant) so topology can cluster nodes accordingly.": "未配置集群规则 —— 拓扑视图不会绘制集群边框。若某个 layer 的服务名中编码了集群维度(k8s 命名空间、集群组、租户),可为其启用规则,让拓扑据此对节点分组。",
"No components enabled — toggle one on the right to populate the menu.": "尚未启用任何组件 —— 在右侧开启一个即可填充菜单。",
"No conditions — every instance matching the name filter is shown.": "没有条件——所有匹配名称筛选的实例都会显示。",
@@ -1029,7 +1030,13 @@
"No schedules captured for this task.": "该任务未捕获到任何调度。",
"No screen or request checks this — granting it has no effect.": "没有任何页面或请求会校验它,授予它不会产生任何效果。",
"No server metrics configured.": "未配置服务端指标。",
- "No server-side metrics.": "尚无服务端指标。",
+ "Edge metrics": "边指标",
+ "Each names the end of the conversation it describes. The eBPF probe watches both, and a metric that describes the exchange itself — the HTTP/1.x families — belongs to neither.": "每个指标都标明它描述的是通信的哪一端。eBPF 探针会同时观测两端,而描述交互本身的指标(HTTP/1.x 系列)不属于任何一端。",
+ "No edge metrics.": "暂无边指标。",
+ "Side": "端",
+ "neither — describes the exchange": "都不是 — 描述交互本身",
+ "Both sides": "双端",
+ "No shared metrics configured.": "未配置双端共享指标。",
"No service matches this": "没有服务符合该条件",
"No services match": "无匹配的服务",
"No services reported.": "没有服务上报。",
@@ -1498,7 +1505,6 @@
"Server side": "服务端",
"Server timezone": "服务器时区",
"Server-side": "服务端",
- "Server-side metrics": "服务端指标",
"Service": "服务",
"Service bind": "Service Bind",
"Service bind DN": "Service Bind DN",
@@ -1895,7 +1901,6 @@
"Your sidebar adapts to your role: anything you can't use is simply not shown. The sections below group SkyWalking's capabilities by feature area. For each area you'll see": "侧边栏会根据你的角色自适应:用不到的条目会直接隐藏。下方按功能区域对 SkyWalking 的能力进行了分组。对于每个区域,你将看到",
"Your sidebar adapts to your role: anything you can't use is simply not shown. The sections below group SkyWalking's capabilities by feature area. For each area you'll see {menuScope} (which sidebar entries fall in the area) and {actions} (the concrete things you can do, marked per role). Multiple roles add up — a user holding more than one role gets every permission any of their roles grant.": "侧边栏会根据你的角色自适应:用不到的条目会直接隐藏。下方按功能区域对 SkyWalking 的能力进行了分组。对于每个区域,你将看到 {menuScope}(哪些侧边栏条目属于该区域 [...]
"Your username is your identity here, and your roles are resolved from it rather than stored against you. A token you hold re-reads them on every request, so removing you from the directory or configuration ends it at once. This browser session keeps the roles it was given until you sign in again.": "在这里,用户名就是你的身份,角色是据此解析的,而非存储在你名下。你持有的令牌会在每次请求时重新读取角色,因此将你从目录或配置中移除会立即生效。而当前浏览器会话会保留登录时获得的角色,直到你重新登录。",
- "Zipkin endpoint unreachable": "Zipkin 端点不可达",
"Zipkin query failed.": "Zipkin 查询失败。",
"Zipkin trace": "Zipkin trace",
"Zoom in": "放大",
@@ -2092,7 +2097,6 @@
"edge": "连线",
"edge MQE for the process-topology detail panel. Queried under ProcessRelation when an operator clicks a process→process call.": "进程拓扑详情面板的连线 MQE。当运维人员点击 进程→进程 的调用时,以 ProcessRelation 范围查询。",
"edge metrics for “{from} → {to}” — queried as": "“{from} → {to}” 的连线指标 —— 查询范围为",
- "edge metrics queried as": "连线指标,查询范围为",
"edges": "连线",
"edit": "编辑",
"editing: {source}": "正在编辑:{source}",
@@ -3355,6 +3359,7 @@
"OAP’s Zipkin v2 API, which also serves the OpenTelemetry spans OAP converts into Zipkin form.": "OAP 的 Zipkin v2 API,它同时提供 OAP 转换为 Zipkin 格式的 OpenTelemetry span。",
"Grafana Tempo’s API over the native spans. Needs OAP’s traceQL module and a configured datasource URL.": "基于原生 span 的 Grafana Tempo API。需要 OAP 的 traceQL 模块,并配置数据源 URL。",
"Grafana Tempo’s API over the Zipkin spans. Needs OAP’s traceQL module and a configured datasource URL.": "基于 Zipkin span 的 Grafana Tempo API。需要 OAP 的 traceQL 模块,并配置数据源 URL。",
+ "Grafana Tempo’s API over the OTLP spans OAP stored as they arrived, converting nothing. Needs OAP’s traceQL module and a configured datasource URL.": "基于 OAP 原样存储、未做任何转换的 OTLP span 的 Grafana Tempo API。需要 OAP 的 traceQL 模块,并配置数据源 URL。",
"Flags": "正则标志",
"The Tempo API has no notion of a layer, so it lists every service of the store. This pattern narrows the service PICKER only — result rows are never filtered, so cross-service traces still appear.": "Tempo API 没有 layer 的概念,会列出该存储中的所有服务。此表达式只收窄服务选择列表,结果行不会被过滤,因此跨服务的 trace 依然会出现。",
"checking {n} statuses…": "正在检查 {n} 条状态…",
@@ -3368,8 +3373,10 @@
"Tempo API": "Tempo API",
"not available": "不可用",
"no URL configured": "未配置 URL",
+ "no context path": "未配置上下文路径",
+ "not exposed": "未开放",
+ "This OAP does not enable the {name} datasource. Switch it on with SW_TRACEQL_ENABLE_DATASOURCE_* and it appears here; until then the rows fed by it stay empty.": "该 OAP 未启用 {name} 数据源。使用 SW_TRACEQL_ENABLE_DATASOURCE_* 开启后即会显示;在此之前,由它提供数据的行保持为空。",
"SkyWalking-native spans": "SkyWalking 原生 span",
- "{name} datasource unreachable": "数据源 {name} 不可达",
"Tried {url}. Enable OAP's traceQL module (SW_TRACEQL=default with the datasource switched on) and confirm oap.traceql points at its host:port and context path — {path} by default on port 3200.": "已尝试 {url}。请启用 OAP 的 traceQL 模块(SW_TRACEQL=default 并打开对应数据源),并确认 oap.traceql 指向其 host:port 与 context path —— 默认是 3200 端口上的 {path}。",
"Trace APIs": "Trace API",
"Zipkin v2 · Tempo": "Zipkin v2 · Tempo",
@@ -3392,13 +3399,17 @@
"A status other than “ok”, “error” or “unset”.": "“ok”“error”“unset”以外的 status。",
"Span tags are those this store reported for the window being queried, so the list follows the time range.": "span 标签是该存储针对所查询时间窗上报的,因此列表会随时间范围变化。",
"ok, error or unset": "ok、error 或 unset",
+ "Span kind — server, client, producer, consumer, internal, unspecified (OTLP)": "Span 类型 — server、client、producer、consumer、internal、unspecified(OTLP)",
"Service instance (native)": "服务实例(原生)",
- "Peer service (Zipkin)": "对端服务(Zipkin)",
+ "Service instance (OTLP)": "服务实例(OTLP)",
+ "Peer service (Zipkin, OTLP)": "对端服务(Zipkin、OTLP)",
"Trace duration — >, >=, <, <= with us/µs/ms/s/m/h": "trace 耗时——可用 >、>=、<、<=,单位 us/µs/ms/s/m/h",
"Zipkin Traces": "Zipkin Trace",
"TraceQL - Native": "TraceQL - Native",
"TraceQL - Zipkin": "TraceQL - Zipkin",
+ "TraceQL - OTLP": "TraceQL - OTLP",
"Zipkin spans": "Zipkin span",
+ "Natively stored OTLP spans": "原生存储的 OTLP span",
"Menu name": "菜单名称",
"Ignore case": "忽略大小写",
"Horizon will not run this expression, so the picker keeps showing every service. It either does not compile, or it can backtrack exponentially — avoid a repeat applied to a group that itself repeats or branches, such as (a+)+.": "Horizon 不会执行该表达式,因此选择器仍会显示全部服务。它要么无法编译,要么可能产生指数级回溯——请避免对本身带有重复或分支的分组再加重复,例如 (a+)+。",
@@ -3406,5 +3417,8 @@
"Open the calendar": "打开日历",
"Previous month": "上一个月",
"Next month": "下一个月",
- "Now": "此刻"
+ "Now": "此刻",
+ "edge metrics queried as": "连线指标,查询范围为",
+ "No server-side metrics.": "尚无服务端指标。",
+ "No client-side metrics.": "尚无客户端指标。"
}
diff --git a/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue b/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
index e84626b7..b33d7fc1 100644
--- a/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
+++ b/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
@@ -305,7 +305,7 @@ const edgeTimeFmt = new Intl.DateTimeFormat(undefined, {
});
const edgeXLabels = computed<string[]>(() => {
const m = relationMetrics.value;
- const n = m?.client[0]?.values.length ?? m?.server[0]?.values.length ?? 0;
+ const n = m?.client[0]?.values.length ?? m?.server[0]?.values.length ?? m?.shared[0]?.values.length ?? 0;
if (n <= 0) return [];
const task = currentTask.value;
const startMs =
@@ -499,12 +499,12 @@ function fmtTime(ms: number): string {
<!-- The metric set is operator-configurable in the admin. -->
<div v-else-if="relationMetrics" class="edge-cols">
<section
- v-for="side in (['client', 'server'] as const)"
+ v-for="side in (['client', 'server', 'shared'] as const)"
:key="side"
class="edge-col"
>
- <h5 class="edge-col-head" :class="side">{{ side === 'client' ? t('Client side') : t('Server side') }}</h5>
- <div v-if="!relationMetrics[side].length" class="muted sm">{{ side === 'client' ? t('No client metrics configured.') : t('No server metrics configured.') }}</div>
+ <h5 class="edge-col-head" :class="side">{{ side === 'client' ? t('Client side') : side === 'server' ? t('Server side') : t('Both sides') }}</h5>
+ <div v-if="!relationMetrics[side].length" class="muted sm">{{ side === 'client' ? t('No client metrics configured.') : side === 'server' ? t('No server metrics configured.') : t('No shared metrics configured.') }}</div>
<div v-else class="edge-col-grid">
<div v-for="m in relationMetrics[side]" :key="m.id" class="edge-widget sw-card">
<div class="ew-head">
@@ -940,6 +940,8 @@ function fmtTime(ms: number): string {
}
.edge-col-head.client { color: var(--sw-accent); border-bottom-color: var(--sw-accent); }
.edge-col-head.server { color: var(--sw-info, #5a9cf8); border-bottom-color: var(--sw-info, #5a9cf8); }
+/* Metrics of the exchange itself — HTTP/1.x — belong to neither column. */
+.edge-col-head.shared { color: var(--sw-fg-2); border-bottom-color: var(--sw-line-2); }
.edge-widget { padding: 8px 10px; }
.ew-head {
display: flex;
diff --git a/apps/ui/src/layer/traceql/LayerTraceQLView.vue b/apps/ui/src/layer/traceql/LayerTraceQLView.vue
index e5da6963..1f29f4b2 100644
--- a/apps/ui/src/layer/traceql/LayerTraceQLView.vue
+++ b/apps/ui/src/layer/traceql/LayerTraceQLView.vue
@@ -63,7 +63,9 @@ const time = useTimeRangeStore();
const layerKey = computed(() => String(route.params.layerKey ?? ''));
/** The row decides the store — each store has its own row. */
const ds = computed<TraceQLDatasource>(() =>
- /traceql-zipkin-trace/.test(route.path) ? 'zipkin' : 'native',
+ /traceql-zipkin-trace/.test(route.path) ? 'zipkin'
+ : /traceql-otlp-trace/.test(route.path) ? 'otlp'
+ : 'native',
);
const { statusOf } = useTraceQLSources();
@@ -439,7 +441,7 @@ const issueText: Record<string, string> = {
{{ t('Trace ID') }}
</button>
</div>
- <span v-if="status && !status.configured" class="hint">{{ t('not configured') }}</span>
+ <span v-if="status && !status.configured" class="hint">{{ status.endpoint ? t('not exposed') : t('not configured') }}</span>
<span v-else-if="sourceDown" class="hint err">{{ t('unreachable') }}</span>
<span v-else-if="busy" class="hint">{{ t('refreshing…') }}</span>
<button
diff --git a/apps/ui/src/layer/traceql/TraceQLSchemaPanel.vue b/apps/ui/src/layer/traceql/TraceQLSchemaPanel.vue
index 07491fc1..ea7a614e 100644
--- a/apps/ui/src/layer/traceql/TraceQLSchemaPanel.vue
+++ b/apps/ui/src/layer/traceql/TraceQLSchemaPanel.vue
@@ -63,7 +63,9 @@ watch(
onBeforeUnmount(() => document.removeEventListener('pointerdown', closeOnOutside));
const { t } = useI18n({ useScope: 'global' });
-const resourceAttrs = computed(() => TRACEQL_RESOURCE_ATTRS.filter((a) => !a.ds || a.ds === props.ds));
+// The panel is the store's schema, so it lists what THIS store can filter.
+const resourceAttrs = computed(() => TRACEQL_RESOURCE_ATTRS.filter((a) => !a.ds || a.ds.includes(props.ds)));
+const intrinsics = computed(() => TRACEQL_INTRINSICS.filter((i) => !i.ds || i.ds.includes(props.ds)));
</script>
<template>
@@ -77,7 +79,7 @@ const resourceAttrs = computed(() => TRACEQL_RESOURCE_ATTRS.filter((a) => !a.ds
<section>
<h5>{{ t('Intrinsics') }}</h5>
<ul>
- <li v-for="i in TRACEQL_INTRINSICS" :key="i.name">
+ <li v-for="i in intrinsics" :key="i.name">
<button type="button" class="tqs-chip mono" @click="emit('insert', i.name)">{{ i.name }}</button>
<span class="ops mono">{{ operatorsFor(i.name).join(' ') }}</span>
<span class="dim">{{ t(i.detail) }}</span>
diff --git a/apps/ui/src/layer/traceql/traceqlLint.test.ts b/apps/ui/src/layer/traceql/traceqlLint.test.ts
index bdd84a4a..cb3731e7 100644
--- a/apps/ui/src/layer/traceql/traceqlLint.test.ts
+++ b/apps/ui/src/layer/traceql/traceqlLint.test.ts
@@ -16,10 +16,11 @@
*/
import { describe, it, expect } from 'vitest';
+import type { TraceQLDatasource } from '@skywalking-horizon-ui/api-client';
import { lintTraceQL, buildTraceQL, serviceInExpression } from '@/layer/traceql/traceqlLint';
/** Nothing to warn about: every field is one the schema has. */
-const clean = (q: string, ds?: 'native' | 'zipkin') => lintTraceQL(q, ds).length === 0;
-const ids = (q: string) => lintTraceQL(q).map((i) => i.id);
+const clean = (q: string, ds?: TraceQLDatasource) => lintTraceQL(q, ds).length === 0;
+const ids = (q: string, ds?: TraceQLDatasource) => lintTraceQL(q, ds).map((i) => i.id);
describe('lint', () => {
it('passes every form the language defines, whatever a backend does with it', () => {
@@ -51,14 +52,31 @@ describe('lint', () => {
});
it('warns about a bare name that is not an intrinsic', () => {
- expect(ids('{kind=server}')).toEqual(['unknown-field']);
expect(ids('{traceDuration>1s}')).toEqual(['unknown-field']);
- // The three intrinsics are bare by grammar.
+ // Intrinsics are bare by grammar.
expect(clean('{duration>100ms}')).toBe(true);
expect(clean('{status="ok"}')).toBe(true);
expect(clean('{name="/checkout"}')).toBe(true);
});
+ it('places an intrinsic only the OTLP store has', () => {
+ // `kind` is a column where the spans were stored as OTLP and nowhere else,
+ // so naming it on another store is a query that cannot match — OAP refuses
+ // it outright. It is the schema's answer, not a guess about the backend.
+ expect(clean('{kind="server"}', 'otlp')).toBe(true);
+ expect(ids('{kind="server"}', 'native')).toEqual(['other-store-attribute']);
+ expect(ids('{kind="server"}', 'zipkin')).toEqual(['other-store-attribute']);
+ });
+
+ it('knows which store each reserved resource attribute belongs to', () => {
+ expect(clean('{resource.service.instance.id="a"}', 'otlp')).toBe(true);
+ expect(ids('{resource.service.instance.id="a"}', 'native')).toEqual(['other-store-attribute']);
+ // The peer attribute belongs to two of the three.
+ expect(clean('{resource.remote.service="a"}', 'zipkin')).toBe(true);
+ expect(clean('{resource.remote.service="a"}', 'otlp')).toBe(true);
+ expect(ids('{resource.remote.service="a"}', 'native')).toEqual(['other-store-attribute']);
+ });
+
it('warns about the other store’s resource attribute', () => {
expect(ids('{resource.instance="aaa"}').length).toBe(0);
expect(lintTraceQL('{resource.instance="aaa"}', 'zipkin').map((i) => i.id)).toEqual(['other-store-attribute']);
diff --git a/apps/ui/src/layer/traceql/traceqlLint.ts b/apps/ui/src/layer/traceql/traceqlLint.ts
index c8d9c62b..1f38e525 100644
--- a/apps/ui/src/layer/traceql/traceqlLint.ts
+++ b/apps/ui/src/layer/traceql/traceqlLint.ts
@@ -29,6 +29,7 @@
* does not have.
*/
+import type { TraceQLDatasource } from '@skywalking-horizon-ui/api-client';
import {
TRACEQL_INTRINSICS,
TRACEQL_RESOURCE_ATTRS,
@@ -96,8 +97,8 @@ function structuralIssue(text: string, syntax: string): TraceQLIssue | null {
}
/** Every issue in `q`. An empty expression is fine — it is the match-all `{}`.
- * `ds` picks the schema, whose resource attributes differ between the two. */
-export function lintTraceQL(q: string, ds: 'native' | 'zipkin' = 'native'): TraceQLIssue[] {
+ * `ds` picks the schema, whose fields differ between the three stores. */
+export function lintTraceQL(q: string, ds: TraceQLDatasource = 'native'): TraceQLIssue[] {
const text = q.trim();
if (!text) return [];
const syntax = withoutLiterals(text);
@@ -114,9 +115,10 @@ export function lintTraceQL(q: string, ds: 'native' | 'zipkin' = 'native'): Trac
if (id) out.push({ id, found: field });
}
- // A resource attribute the OTHER datasource defines: the schema panel lists
- // one of these per store, and the two are not interchangeable.
- const foreign = TRACEQL_RESOURCE_ATTRS.filter((a) => a.ds && a.ds !== ds);
+ // A field ANOTHER datasource defines: the schema panel lists what this store
+ // has, and a name from a different one cannot match here.
+ const foreign = [...TRACEQL_RESOURCE_ATTRS, ...TRACEQL_INTRINSICS]
+ .filter((a) => a.ds && !a.ds.includes(ds));
for (const attr of foreign) {
if (new RegExp(`(?:^|[\\s{(&|!])${attr.name.replace(/\./g, '\\.')}\\s*(?:!=|=~|!~|=)`).test(syntax)) {
out.push({ id: 'other-store-attribute', found: attr.name });
diff --git a/apps/ui/src/monaco/setup.ts b/apps/ui/src/monaco/setup.ts
index 68ad40df..35a4bae7 100644
--- a/apps/ui/src/monaco/setup.ts
+++ b/apps/ui/src/monaco/setup.ts
@@ -27,7 +27,7 @@
import * as monaco from 'monaco-editor';
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
-import type { Catalog } from '@skywalking-horizon-ui/api-client';
+import type { Catalog, TraceQLDatasource } from '@skywalking-horizon-ui/api-client';
import { tokens } from '@skywalking-horizon-ui/design-tokens';
// Local shim mapping the vantage rrDark slot names to horizon's nested
// token tree so the Monaco theme below stays readable. The slot semantics
@@ -130,7 +130,7 @@ export function setupMonaco(): void {
* the backend reported, and a way to ask for one tag's values. Attached to the
* MODEL so one registration serves every editor instance and every datasource. */
export interface TraceQLSchema {
- ds: 'native' | 'zipkin';
+ ds: TraceQLDatasource;
tagKeys: string[];
services: string[];
spanNames: string[];
@@ -278,7 +278,7 @@ function registerTraceQL(): void {
...TRACEQL_INTRINSICS.map((i) =>
item(i.name, i.name === 'duration' ? 'duration>' : `${i.name}="`, i.detail, monaco.languages.CompletionItemKind.Keyword, fieldRange),
),
- ...TRACEQL_RESOURCE_ATTRS.filter((a) => !a.ds || !schema || a.ds === schema.ds).map((a) =>
+ ...TRACEQL_RESOURCE_ATTRS.filter((a) => !a.ds || !schema || a.ds.includes(schema.ds)).map((a) =>
item(a.name, `${a.name}="`, a.detail, monaco.languages.CompletionItemKind.Property, fieldRange),
),
...TRACEQL_SCOPES.map((sc) => item(`${sc.name}.`, `${sc.name}.`, sc.detail, monaco.languages.CompletionItemKind.Module, fieldRange)),
diff --git a/apps/ui/src/monaco/traceql-grammar.ts b/apps/ui/src/monaco/traceql-grammar.ts
index 8b41eaf2..f7c81260 100644
--- a/apps/ui/src/monaco/traceql-grammar.ts
+++ b/apps/ui/src/monaco/traceql-grammar.ts
@@ -25,11 +25,17 @@
* deliberately not modelled: see `layer/traceql/traceqlLint.ts`.
*/
+/** Which datasources a schema entry belongs to. Absent means all of them. */
+export type TraceQLStores = ReadonlyArray<'native' | 'zipkin' | 'otlp'>;
+
/** Intrinsic fields, which are bare by grammar and carry no scope. */
-export const TRACEQL_INTRINSICS: ReadonlyArray<{ name: string; detail: string }> = [
+export const TRACEQL_INTRINSICS: ReadonlyArray<{ name: string; detail: string; ds?: TraceQLStores }> = [
{ name: 'duration', detail: 'Trace duration — >, >=, <, <= with us/µs/ms/s/m/h' },
{ name: 'name', detail: 'Span name' },
{ name: 'status', detail: 'ok, error or unset' },
+ // A span kind is a column only where the spans were stored as OTLP; the
+ // Zipkin and SkyWalking tag indexes have nothing to filter it on.
+ { name: 'kind', detail: 'Span kind — server, client, producer, consumer, internal, unspecified (OTLP)', ds: ['otlp'] },
];
/** Attribute scopes. `span.` and the unscoped `.` reach the same tag lookup;
@@ -40,11 +46,14 @@ export const TRACEQL_SCOPES: ReadonlyArray<{ name: string; detail: string }> = [
];
/** The reserved resource attributes, which become entity filters rather than
- * tag lookups. `instance` is native-only, `remote.service` Zipkin-only. */
-export const TRACEQL_RESOURCE_ATTRS: ReadonlyArray<{ name: string; detail: string; ds?: 'native' | 'zipkin' }> = [
+ * tag lookups. Which store defines which is not a detail: naming one the
+ * store does not have is the commonest way to write a query that cannot
+ * match, so each says where it applies. */
+export const TRACEQL_RESOURCE_ATTRS: ReadonlyArray<{ name: string; detail: string; ds?: TraceQLStores }> = [
{ name: 'resource.service.name', detail: 'Service' },
- { name: 'resource.instance', detail: 'Service instance (native)', ds: 'native' },
- { name: 'resource.remote.service', detail: 'Peer service (Zipkin)', ds: 'zipkin' },
+ { name: 'resource.instance', detail: 'Service instance (native)', ds: ['native'] },
+ { name: 'resource.service.instance.id', detail: 'Service instance (OTLP)', ds: ['otlp'] },
+ { name: 'resource.remote.service', detail: 'Peer service (Zipkin, OTLP)', ds: ['zipkin', 'otlp'] },
];
/** The three values TraceQL's `status` intrinsic takes. */
diff --git a/apps/ui/src/shell/SidebarLayerChildren.vue b/apps/ui/src/shell/SidebarLayerChildren.vue
index 4a6966a5..b7659374 100644
--- a/apps/ui/src/shell/SidebarLayerChildren.vue
+++ b/apps/ui/src/shell/SidebarLayerChildren.vue
@@ -57,6 +57,7 @@ function labelFor(path: string): string {
case 'zipkin-trace': return t('Zipkin Traces');
case 'traceql-native-trace': return t('TraceQL - Native');
case 'traceql-zipkin-trace': return t('TraceQL - Zipkin');
+ case 'traceql-otlp-trace': return t('TraceQL - OTLP');
case 'logs': return t('Logs');
case 'evaluation-record': return t('Evaluation records');
case 'browser-errors': return t('Browser Logs');
diff --git a/apps/ui/src/shell/layerFromTemplate.test.ts b/apps/ui/src/shell/layerFromTemplate.test.ts
index 73cf35a8..df57e756 100644
--- a/apps/ui/src/shell/layerFromTemplate.test.ts
+++ b/apps/ui/src/shell/layerFromTemplate.test.ts
@@ -169,7 +169,7 @@ describe('componentsToCaps — parity with the menu route', () => {
pprofProfiling: true, continuousProfiling: true, aiConversations: true,
},
deployment: { roles: [] },
- traces: { sources: ['native', 'zipkin', 'traceql-native', 'traceql-zipkin'] },
+ traces: { sources: ['native', 'zipkin', 'traceql-native', 'traceql-zipkin', 'traceql-otlp'] },
});
expect(paths(def)).toEqual([...DEFAULT_LAYER_ROW_ORDER]);
});
diff --git a/apps/ui/src/shell/router/index.ts b/apps/ui/src/shell/router/index.ts
index b4ba6cfa..03872c84 100644
--- a/apps/ui/src/shell/router/index.ts
+++ b/apps/ui/src/shell/router/index.ts
@@ -90,6 +90,7 @@ function layerRoute(): RouteRecordRaw {
// than a dispatcher inferring it from the layer's configuration.
{ path: 'traceql-native-trace', component: () => import('@/layer/traceql/LayerTraceQLView.vue') },
{ path: 'traceql-zipkin-trace', component: () => import('@/layer/traceql/LayerTraceQLView.vue') },
+ { path: 'traceql-otlp-trace', component: () => import('@/layer/traceql/LayerTraceQLView.vue') },
{ path: 'logs', component: () => import('@/layer/logs/LayerLogsView.vue') },
// Owns its provider picker: the view draws it from the `logs:read`
// evaluation catalog so a logs-only role can pick a provider, where the
diff --git a/apps/ui/src/shell/useTopbarTimeContext.ts b/apps/ui/src/shell/useTopbarTimeContext.ts
index 865f3d1a..d06313b8 100644
--- a/apps/ui/src/shell/useTopbarTimeContext.ts
+++ b/apps/ui/src/shell/useTopbarTimeContext.ts
@@ -35,6 +35,7 @@ const TIME_RANGE_OPT_OUT = [
/^\/layer\/[^/]+\/zipkin-trace$/,
/^\/layer\/[^/]+\/traceql-native-trace$/,
/^\/layer\/[^/]+\/traceql-zipkin-trace$/,
+ /^\/layer\/[^/]+\/traceql-otlp-trace$/,
// Logs carry their own time picker (the condition bar); the level /
// keyword filters make rolling-window refresh awkward mid-investigation.
/^\/layer\/[^/]+\/logs$/,
diff --git a/docs/changelog/1.1.0.md b/docs/changelog/1.1.0.md
index d3e95cfc..38f91f67 100644
--- a/docs/changelog/1.1.0.md
+++ b/docs/changelog/1.1.0.md
@@ -150,8 +150,6 @@ The **3D Infra Map** opens framed on the tiers that hold services. A deployment
### Fixes
-**The Inspect page could send its MQE expressions to the wrong port.** When `oap.mqe` gave a host or a port but not both, Horizon fills in the missing half from OAP's own configuration — and it looked for the sharing server's listener under a name OAP does not use, so a deployment that had moved the query port onto that listener was still sent to the core one. It reads the right name now. Deployments with no `oap.mqe` override were never affected: there the configured query endpoint is u [...]
-
**The sidebar uses less left padding and makes layer grouping clearer.** AI Agents, Browser and other standalone layers align with group headings. A group with only one visible layer shows that layer directly at the same level; groups with multiple visible layers can be folded and unfolded by clicking their heading. Group headings have the same row height as layer entries. Navigating to a layer opens its group, while a menu refresh preserves groups you folded. Satellite and other direct [...]
**Data retention now reads the same on every storage backend, and says which setting governs each figure.** The page used to mirror OAP's wire shape — a Records / Metrics split, a Minute / Hour / Day trio, and a class called *Normal* — which on ElasticSearch, MySQL, PostgreSQL, H2 and TiDB meant nine numbers that were really two, presented as nine knobs you could turn apart. Those backends now show the five things retention is actually decided for — **Metadata**, **Metrics**, **Logs**, * [...]
@@ -166,7 +164,11 @@ The **3D Infra Map** opens framed on the tiers that hold services. A deployment
**Cluster status reports every trace API in one pane.** **Operate → Cluster**'s Zipkin pane becomes **Trace APIs**, listing every endpoint behind a layer's trace rows in one table — API, source, state, endpoint — with a row for the Zipkin v2 REST endpoint and one per TraceQL datasource, SkyWalking-native spans and Zipkin spans, each showing whether it answered and the Tempo API version it reported, plus the error and the exact URL tried when one is configured but silent. A TraceQL dataso [...]
-**Traces can now be queried in TraceQL, over either trace store.** OAP can answer for its traces in [Grafana Tempo's](https://grafana.com/docs/tempo/latest/traceql/) query language, and a layer can expose that as its own sidebar row — **TraceQL - Native** over the SkyWalking-native spans, **TraceQL - Zipkin** over the Zipkin spans, either or both, renamable per layer. Point Horizon at the datasources you enabled with `oap.traceql.nativeUrl` and `oap.traceql.zipkinUrl`; both are empty by [...]
+**Traces can now be queried in TraceQL, over either trace store.** OAP can answer for its traces in [Grafana Tempo's](https://grafana.com/docs/tempo/latest/traceql/) query language, and a layer can expose that as its own sidebar row — **TraceQL - Native** over the SkyWalking-native spans, **TraceQL - Zipkin** over the Zipkin spans, either or both, renamable per layer. Point Horizon at the service with `oap.traceql.url` and name the context path of each datasource you enabled — `nativePat [...]
+
+**Traces can be queried in TraceQL over OAP's natively stored OTLP spans.** OAP can now keep OTLP traces exactly as they arrive instead of converting them to Zipkin, and serve them on a TraceQL datasource of their own. A layer can expose that as a **TraceQL - OTLP** row: point Horizon at it with `oap.traceql.otlpPath`, tick the store in **Dashboard setup → Layer dashboards → (layer) → Trace stores**, and it queries and renders like the other TraceQL rows. Because nothing is converted, th [...]
+
+**A trace list shows failures without opening every trace.** A TraceQL search response can now carry each span's status, so a result row is marked as failed or healthy the moment the list lands, instead of the page reading every undecided trace one at a time afterwards. Rows an OAP does not answer for are still read that way — the header counts them down as before — so an older OAP behaves exactly as it did.
**Each trace store a layer names gets its own sidebar row, with a name you choose.** A layer exposing native and TraceQL traces shows both rows rather than one tab that switches store under you, and **Dashboard setup → Layer dashboards → (layer) → Trace stores** is where they are ticked, renamed, and — for the TraceQL rows — given the service-picker filter. Renaming a row changes only its label; links and stored menu arrangements name the row itself, so they keep working.
@@ -220,6 +222,12 @@ The **3D Infra Map** opens framed on the tiers that hold services. A deployment
**A Zipkin trace's error state was decided by two different rules, and they disagreed on screen.** The result row counted a span as failed when it carried an `error` tag, an `http.status_code` beginning with `5`, or `otel.status_code=ERROR`, while the waterfall and span detail below it looked only for the `error` tag. So a span returning 503 with no `error` tag made the row read ERR over a waterfall in which nothing was marked, a 404 was a failure on neither although the **TraceQL - Zipk [...]
+**Horizon connects to two OAP endpoints, not three.** The Metrics inspect page used to resolve its own endpoint for MQE expressions — an optional `oap.mqe` host/port override, and, when only half of it was given, a discovery step that read OAP's configuration dump to find which listener served the query port. That endpoint was never a third surface: executing an MQE expression is `execExpression` in the query protocol, which `oap.queryUrl` already serves. `oap.mqe` and `HORIZON_OAP_MQE` [...]
+
+**Network profiling's edge metrics are in the layer template, and a metric says which side it describes.** The process-relation panel under a process→process call was drawn from a built-in set of sixteen metrics that no template named — so **Dashboard setup → Layer dashboards → (layer) → Network profiling** showed an empty configuration for a panel that was rendering. The Mesh and Kubernetes Service layers now carry the full set in their templates: **38 metrics**, the TCP families with t [...]
+
+**Every row in the layer menu opens the configuration that governs it.** In **Layer dashboards**, a row whose name the editor did not know was drawn as its raw path — `traceql-otlp-trace`, `evaluation-record`, `conversations` — and several rows could not be clicked at all. The five trace rows now open the **Trace stores** tab that configures all of them; Pod logs, Continuous profiling, pprof, Evaluation records, Browser logs and Conversations were components with no tab at all and now ha [...]
+
## Languages
### Fixes
diff --git a/docs/compatibility/cluster-status.md b/docs/compatibility/cluster-status.md
index 938e0190..c963f1d9 100644
--- a/docs/compatibility/cluster-status.md
+++ b/docs/compatibility/cluster-status.md
@@ -105,8 +105,9 @@ A third pane probes every endpoint behind a layer's trace rows and lists them in
| API | Source | Endpoint |
|---|---|---|
| **Zipkin v2 REST** | Zipkin spans | `oap.zipkinUrl` — the Zipkin / OTel trace rows. |
-| **TraceQL** | SkyWalking-native spans | `oap.traceql.nativeUrl` — the TraceQL rows over native spans. |
-| **TraceQL** | Zipkin spans | `oap.traceql.zipkinUrl` — the TraceQL rows over Zipkin spans. |
+| **TraceQL** | SkyWalking-native spans | `oap.traceql.url` + `nativePath` — the TraceQL rows over native spans. |
+| **TraceQL** | Zipkin spans | `oap.traceql.url` + `zipkinPath` — the TraceQL rows over Zipkin spans. |
+| **TraceQL** | Natively stored OTLP spans | `oap.traceql.url` + `otlpPath` — the TraceQL rows over the OTLP spans OAP kept as they arrived. |
A TraceQL row also prints the Tempo API version the datasource reported.
diff --git a/docs/customization/layer-templates.md b/docs/customization/layer-templates.md
index 7eab9bf8..39150c2a 100644
--- a/docs/customization/layer-templates.md
+++ b/docs/customization/layer-templates.md
@@ -526,8 +526,9 @@ Which trace stores this layer exposes. It is a **checklist**, not a single choic
|---|---|---|
| `native` | `trace` | OAP's native trace query. |
| `zipkin` | `zipkin-trace` | OAP's Zipkin v2 API at `oap.zipkinUrl`, which also carries the OpenTelemetry spans OAP converts into Zipkin form. |
-| `traceql-native` | `traceql-native-trace` | The TraceQL (Grafana Tempo) API over the native spans, at `oap.traceql.nativeUrl`. |
-| `traceql-zipkin` | `traceql-zipkin-trace` | The same API over the Zipkin spans, at `oap.traceql.zipkinUrl`. |
+| `traceql-native` | `traceql-native-trace` | The TraceQL (Grafana Tempo) API over the native spans, at `oap.traceql.url` + `nativePath`. |
+| `traceql-zipkin` | `traceql-zipkin-trace` | The same API over the Zipkin spans, at `oap.traceql.url` + `zipkinPath`. |
+| `traceql-otlp` | `traceql-otlp-trace` | The same API over the OTLP spans OAP stored as they arrived, at `oap.traceql.url` + `otlpPath`. |
**A template written before the checklist needs no change.** One carrying the old `source` enum is read as the store(s) it named (`both` as native plus Zipkin); one with no `traces` block at all is read as the **native** store, which is the row such a layer has always shown. Saying a layer has *no* trace rows is explicit: `"sources": []`. The editor opens on the resolved set rather than the literal one, so the first change made THERE — ticking a store, renaming a row, editing a filter — [...]
diff --git a/docs/operate/traces.md b/docs/operate/traces.md
index 29b085a8..36c86e0b 100644
--- a/docs/operate/traces.md
+++ b/docs/operate/traces.md
@@ -31,6 +31,7 @@ A layer names the trace stores it exposes, and **each one becomes its own row in
| **Zipkin Traces** | OAP's Zipkin v2 API, which also carries the OpenTelemetry traces OAP converts into Zipkin form. |
| **TraceQL - Native** | The TraceQL API over the native spans. |
| **TraceQL - Zipkin** | The TraceQL API over the Zipkin spans. |
+| **TraceQL - OTLP** | The TraceQL API over the OTLP spans OAP stored as they arrived, converting nothing. |
Each row can be renamed per layer, so what you see in your deployment may differ. Stores are kept as separate rows rather than one row with a toggle because their span models and their query conditions genuinely differ, and a row that silently switched stores would change what a field means under you.
@@ -118,9 +119,11 @@ Each Zipkin result shows its duration and error state, with a duration bar color
## How a span's status is decided
-**Only native traces carry a status from the backend.** OAP reports whether a native span errored, and the Traces tab shows what it reported. The other two stores have nothing to report: a **Zipkin** span has no status field at all, and an **OTLP** span converted from one arrives with its status `unset` — OAP's converter sets none. A Tempo search result carries no status either.
+**Where a span reports its own status, that is what you see.** A native span carries OAP's error flag, and a natively stored **OTLP** span keeps the status its SDK set. A **Zipkin** span has no status field at all, and an OTLP span *converted* to Zipkin arrives `unset` — OAP's converter sets none — so for those there is nothing to report.
-So for those, the status is read from the attributes the span carries, by one table shared across every store — the Zipkin row, the TraceQL rows, and each one's result list. An attribute not listed here is never read as a status.
+**Where it does not, the status is read from the attributes the span carries**, by one table shared across every store — the Zipkin row, the TraceQL rows, and each one's result list. An attribute not listed here is never read as a status.
+
+A TraceQL **search** result is a third case: it carries a `status` of its own per span, so a list can show failures without opening each trace. Where that says `error` or `ok` the row is settled at once; where it says `unset`, the traces still undecided are read afterwards and the markers below settle them.
| Attribute | `error` when | `ok` when | says nothing when |
|---|---|---|---|
@@ -152,7 +155,11 @@ Two markers on one span can disagree, and so can two spans in one trace. **Failu
## TraceQL traces
-The TraceQL rows query the same traces through [Grafana Tempo's query language](https://grafana.com/docs/tempo/latest/traceql/), which OAP answers over Tempo's HTTP API. A TraceQL trace is an **OpenTelemetry** trace and is shown as one: opaque span ids, a span kind, a three-valued status (ok / error / unset), resource attributes kept apart from span attributes, and the instrumentation scope. Nothing is folded into SkyWalking's own span shape, so what you read is what the protocol defines.
+The TraceQL rows query traces through [Grafana Tempo's query language](https://grafana.com/docs/tempo/latest/traceql/), which OAP answers over Tempo's HTTP API. A TraceQL trace is an **OpenTelemetry** trace and is shown as one: opaque span ids, a span kind, a three-valued status (ok / error / unset), resource attributes kept apart from span attributes, and the instrumentation scope. Nothing is folded into SkyWalking's own span shape, so what you read is what the protocol defines.
+
+**Each row is a different store, not a different view of one.** *TraceQL - Native* reads the SkyWalking spans and *TraceQL - Zipkin* the Zipkin ones, both converted to OTLP to answer. *TraceQL - OTLP* reads spans OAP stored exactly as they arrived, converting nothing — so attributes keep their own types, and events, links, status messages and the instrumentation scope are what the SDK exported. Where a deployment's OTLP traces live depends on how its receiver was configured to keep them, [...]
+
+The OTLP row also has fields the other two cannot offer: the `kind` intrinsic, `resource.service.instance.id`, and Tempo's `span:`-prefixed spellings. The **Schema reference** on each row lists what that store can filter, which is the shortest way to see the difference.
The one reading Horizon does add is the span kind. OTLP has no entry and exit — it has client, server, producer and consumer — so the views name them the way every other trace surface does: a **server** or **consumer** span is an **Entry** (a call or a message arriving), a **client** or **producer** span an **Exit** (one leaving), and an **internal** span is **Local**. The protocol's own word is shown beside it on the span detail, in the statistics table and in the waterfall's tooltip, s [...]
diff --git a/docs/setup/container-image.md b/docs/setup/container-image.md
index 560b10e0..844544e5 100644
--- a/docs/setup/container-image.md
+++ b/docs/setup/container-image.md
@@ -68,7 +68,6 @@ Scalar vars take a plain value; **list / object vars take a JSON string** (injec
| `HORIZON_OAP_TRACEQL_NATIVE_URL` | _(empty)_ | url | OAP's TraceQL (Tempo API) datasource over the native spans, context path included — `http://oap:3200/skywalking`. Empty means the row is off. |
| `HORIZON_OAP_TRACEQL_ZIPKIN_URL` | _(empty)_ | url | The same over the Zipkin spans — `http://oap:3200/zipkin`. Empty means the row is off. |
| `HORIZON_OAP_TIMEOUT_MS` | `15000` | int | Outbound OAP request timeout. |
-| `HORIZON_OAP_MQE` | (none) | JSON | MQE endpoint override for the Metrics Inspect page, e.g. `{"host":"mqe.internal","port":12800}` (both fields optional). Defaults to the query host — see [OAP Connection](oap.md#mqe-endpoint-override-oapmqe). |
| `HORIZON_OAP_AUTH` | (none) | JSON | OAP basic-auth, e.g. `{"username":"sw","password":"sw"}`. |
| `HORIZON_AUTH_BACKEND` | `local` | `local` \| `ldap` | Auth backend. |
| `HORIZON_AUTH_LOCAL_USERS` | `[]` | JSON | Local users: `[{"username":"admin","passwordHash":"$argon2id$…","roles":["admin"]}]` (hash via `pnpm --filter bff cli:hash`). |
diff --git a/docs/setup/oap.md b/docs/setup/oap.md
index 3c944e33..5241d185 100644
--- a/docs/setup/oap.md
+++ b/docs/setup/oap.md
@@ -8,8 +8,10 @@ oap:
adminUrl: http://127.0.0.1:17128
zipkinUrl: http://127.0.0.1:9412/zipkin
traceql:
- nativeUrl: http://127.0.0.1:3200/skywalking
- zipkinUrl: http://127.0.0.1:3200/zipkin
+ url: http://127.0.0.1:3200
+ nativePath: /skywalking
+ zipkinPath: /zipkin
+ otlpPath: /otlp
timeoutMs: 15000
auth:
username: skywalking
@@ -23,13 +25,13 @@ oap:
| `queryUrl` | URL string | `http://127.0.0.1:12800` | no | OAP GraphQL query endpoint. Load-balanceable — any OAP node answers. Used by all read pages. Must be a valid URL. |
| `adminUrl` | URL string | `http://127.0.0.1:17128` | no | OAP admin REST endpoint. Hosts runtime-rule, dsl-debugging, inspect, status, debugging/config endpoints. Single URL; OAP handles cluster-internal fan-out. |
| `zipkinUrl` | URL string | `http://127.0.0.1:9412/zipkin` | no | Zipkin v2 REST endpoint. Used when a layer exposes the Zipkin trace store. Defaults assume the standalone Armeria binding; for Docker / shared-port deployments use `<queryUrl>/zipkin`. |
-| `traceql.nativeUrl` | URL string | empty | no | OAP's TraceQL (Grafana Tempo API) service over the SkyWalking-native spans, context path included. **Empty means off** — there is deliberately no default, because OAP ships the module disabled. See [TraceQL trace stores](#traceql-trace-stores-oaptraceql). |
-| `traceql.zipkinUrl` | URL string | empty | no | The same service over the Zipkin spans, context path included. Empty means off. |
+| `traceql.url` | URL string | empty | no | OAP's TraceQL (Grafana Tempo API) service — host and port only, no path. One server answers for every datasource. **Empty means off** — there is deliberately no default, because OAP ships the module disabled. See [TraceQL trace stores](#traceql-trace-stores-oaptraceql). |
+| `traceql.nativePath` | string | empty | no | Context path of the datasource over the SkyWalking-native spans — OAP's `restContextPathSkywalking`, `/skywalking` by default. Empty means this OAP does not serve it. |
+| `traceql.zipkinPath` | string | empty | no | Context path of the datasource over the Zipkin spans — OAP's `restContextPathZipkin`, `/zipkin` by default. Empty means this OAP does not serve it. |
+| `traceql.otlpPath` | string | empty | no | Context path of the datasource over the OTLP spans OAP stored as they arrived — OAP's `restContextPathOTLP`, `/otlp` by default. Needs `receiver-otel.otlpTraceStorage: otlp`. Empty means this OAP does not serve it. |
| `timeoutMs` | number | `15000` | no | Per-request HTTP timeout (milliseconds) for all OAP calls. Applies to query, admin, Zipkin. Must be positive integer. |
| `auth.username` | string | — | required if `auth` block present | Basic-auth username. Sent on every outbound OAP call. |
| `auth.password` | string | — | required if `auth` block present | Basic-auth password. Sent on every outbound OAP call. Use `${VAR}` interpolation, not a literal. |
-| `mqe.host` | string | — | no | Override host for the MQE (`execExpression`) calls the Metrics Inspect page fires. When the whole `mqe` block is unset, those calls go to `queryUrl` like every other GraphQL query. See [MQE endpoint override](#mqe-endpoint-override-oapmqe). |
-| `mqe.port` | number | — | no | Override port for the same calls. Must be positive integer. |
## How the BFF uses each URL
@@ -59,25 +61,17 @@ Its default port is `3200`, with the context paths `/skywalking` and `/zipkin`.
```yaml
oap:
traceql:
- nativeUrl: http://<oap-host>:3200/skywalking
- zipkinUrl: http://<oap-host>:3200/zipkin
+ url: http://<oap-host>:3200
+ nativePath: /skywalking
+ zipkinPath: /zipkin
+ otlpPath: /otlp
```
-Leaving a URL empty turns that source off: its sidebar row still appears for a layer that names it, and the tab states that no URL is configured rather than searching something else. A configured URL that does not answer is reported the same way, on the page rather than in a log.
+**One endpoint, one path per datasource.** OAP's TraceQL service binds a single host and port (`restHost` / `restPort`, `3200` by default) and serves each datasource under its own context path, so Horizon takes the endpoint once and a path for each. Set only the paths your OAP enables: a datasource with no path is one this deployment does not serve.
-Which layers expose these stores is a layer-template decision, not a connection one — see [Layer Dashboard Templates → `traces`](../customization/layer-templates.md#traces) and [Traces](../operate/traces.md).
-
-## MQE endpoint override (`oap.mqe`)
-
-The Metrics Inspect page executes MQE expressions (`execExpression`) against a resolved MQE endpoint. By default that endpoint **is `queryUrl`** — the same GraphQL surface as every other query, with the same scheme and basic-auth — so most deployments never set `oap.mqe`. Set the override only when the MQE surface must be reached at a different address than `queryUrl`:
+Leaving `url` or a path empty turns that source off: its sidebar row still appears for a layer that names it, and the tab states that no URL is configured rather than searching something else. A configured URL that does not answer is reported the same way, on the page rather than in a log.
-- **Both `host` and `port` set** — MQE calls go to `http://<host>:<port>` (plain HTTP), with no discovery.
-- **Only one of the two set** — the missing half is discovered from the OAP admin host's configuration dump: the sharing-server REST bind when present (the OAP 11.x default layout), otherwise core's REST bind. A wildcard bind host (`0.0.0.0`, `::`) is replaced with `adminUrl`'s hostname. The combined result is plain HTTP as well.
-- **Neither set (default)** — MQE calls use `queryUrl` verbatim.
-
-The resolved target is cached for about a minute, so a hot-reloaded `oap.mqe` edit takes effect within a minute.
-
-Env form (JSON, both fields optional): `HORIZON_OAP_MQE='{"host":"mqe.internal","port":12800}'`.
+Which layers expose these stores is a layer-template decision, not a connection one — see [Layer Dashboard Templates → `traces`](../customization/layer-templates.md#traces) and [Traces](../operate/traces.md).
## Basic auth handling
diff --git a/horizon.yaml b/horizon.yaml
index b5eac44f..ea953209 100644
--- a/horizon.yaml
+++ b/horizon.yaml
@@ -96,13 +96,17 @@ oap:
# means that source is off; OAP ships the traceQL module disabled, so there
# is deliberately no default to probe. Enable it with SW_TRACEQL=default and
# SW_TRACEQL_ENABLE_DATASOURCE_{SKYWALKING,ZIPKIN}=true on the OAP side.
+ # OAP's TraceQL (Grafana Tempo API) service. One endpoint serves every
+ # datasource under its own context path, and the paths below are OAP's own
+ # defaults — change one only if you changed `restContextPath*` on OAP. Set
+ # the URL and Horizon probes each: a datasource this OAP does not enable
+ # answers 404 and reads as "not served", which is not the same as down.
traceql:
- nativeUrl: "${HORIZON_OAP_TRACEQL_NATIVE_URL:}"
- zipkinUrl: "${HORIZON_OAP_TRACEQL_ZIPKIN_URL:}"
+ url: "${HORIZON_OAP_TRACEQL_URL:}"
+ nativePath: "${HORIZON_OAP_TRACEQL_NATIVE_PATH:/skywalking}"
+ zipkinPath: "${HORIZON_OAP_TRACEQL_ZIPKIN_PATH:/zipkin}"
+ otlpPath: "${HORIZON_OAP_TRACEQL_OTLP_PATH:/otlp}"
timeoutMs: ${HORIZON_OAP_TIMEOUT_MS:15000}
- # Optional MQE host/port override (defaults to the query host). JSON env, e.g.
- # HORIZON_OAP_MQE='{"host":"mqe.internal","port":12800}'.
- mqe: ${HORIZON_OAP_MQE:null}
# Optional basic-auth for outbound OAP calls. JSON env, e.g.
# HORIZON_OAP_AUTH='{"username":"skywalking","password":"skywalking"}'.
auth: ${HORIZON_OAP_AUTH:null}
diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts
index 98af27ce..fb7de4a4 100644
--- a/packages/api-client/src/index.ts
+++ b/packages/api-client/src/index.ts
@@ -90,6 +90,7 @@ export type {
InstanceTopologyConfig,
EndpointDependencyConfig,
ProcessTopologyConfig,
+ ProcessRelationSide,
ProcessRelationMetric,
ProcessRelationMetricsResponse,
ProcessRelationEndpointRef,
@@ -103,6 +104,7 @@ export type {
EndpointDependencyCall,
EndpointDependencyResponse,
} from './topology.js';
+export { resolveEdgeMetrics } from './topology.js';
export type {
DeploymentMetricDef,
ClusterByRule,
diff --git a/packages/api-client/src/layer-menu.test.ts b/packages/api-client/src/layer-menu.test.ts
index 7f4970ec..dc06823f 100644
--- a/packages/api-client/src/layer-menu.test.ts
+++ b/packages/api-client/src/layer-menu.test.ts
@@ -88,7 +88,7 @@ const EVERY_ROW: LayerMenuInput = {
asyncProfiling: true,
} satisfies LayerCaps,
slots: {},
- traces: { sources: ['native', 'zipkin', 'traceql-native', 'traceql-zipkin'] },
+ traces: { sources: ['native', 'zipkin', 'traceql-native', 'traceql-zipkin', 'traceql-otlp'] },
};
describe('DEFAULT_LAYER_ROW_ORDER', () => {
diff --git a/packages/api-client/src/layer-menu.ts b/packages/api-client/src/layer-menu.ts
index 74965e69..abc92389 100644
--- a/packages/api-client/src/layer-menu.ts
+++ b/packages/api-client/src/layer-menu.ts
@@ -145,6 +145,7 @@ export const DEFAULT_LAYER_ROW_ORDER = [
'zipkin-trace',
'traceql-native-trace',
'traceql-zipkin-trace',
+ 'traceql-otlp-trace',
'logs',
'evaluation-record',
'browser-errors',
@@ -168,6 +169,7 @@ export const TRACE_STORE_ROWS: Readonly<Record<TraceStore, string>> = {
zipkin: 'zipkin-trace',
'traceql-native': 'traceql-native-trace',
'traceql-zipkin': 'traceql-zipkin-trace',
+ 'traceql-otlp': 'traceql-otlp-trace',
};
function hasStore(L: LayerMenuInput, store: TraceStore): boolean {
@@ -203,6 +205,7 @@ const ROW_DEFS: Record<BuiltInLayerRow, { icon: LayerMenuRowIcon; when: (L: Laye
'zipkin-trace': { icon: 'trace', when: (L) => hasStore(L, 'zipkin') },
'traceql-native-trace': { icon: 'trace', when: (L) => hasStore(L, 'traceql-native') },
'traceql-zipkin-trace': { icon: 'trace', when: (L) => hasStore(L, 'traceql-zipkin') },
+ 'traceql-otlp-trace': { icon: 'trace', when: (L) => hasStore(L, 'traceql-otlp') },
logs: { icon: 'log', when: (L) => Boolean(L.caps?.logs) },
'evaluation-record': { icon: 'log', when: (L) => Boolean(L.caps?.evaluationRecord) },
'browser-errors': { icon: 'web', when: (L) => Boolean(L.caps?.browserErrors) },
diff --git a/packages/api-client/src/topology.test.ts b/packages/api-client/src/topology.test.ts
new file mode 100644
index 00000000..c217d95a
--- /dev/null
+++ b/packages/api-client/src/topology.test.ts
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Process-relation edge metrics. A metric SAYS which end of the conversation
+ * it describes, because not every one has an end — the HTTP/1.x families
+ * describe the exchange — and because reading the side off the metric's name
+ * cannot express that, nor survive a rename.
+ */
+
+import { describe, expect, it } from 'vitest';
+import { resolveEdgeMetrics } from './topology.js';
+
+const m = (id: string, mqe: string) => ({ id, label: id, mqe, aggregation: 'avg' as const });
+
+describe('edge metrics', () => {
+ it('reads a template that still spells the sides as two lists', () => {
+ // Stored templates written before the one list, resolved rather than
+ // migrated — the editor writes the new spelling on the first edit.
+ expect(resolveEdgeMetrics({
+ edgeClientMetrics: [m('write', 'process_relation_client_write_cpm')],
+ edgeServerMetrics: [m('read', 'process_relation_server_read_cpm')],
+ })).toEqual([
+ { ...m('write', 'process_relation_client_write_cpm'), side: 'client' },
+ { ...m('read', 'process_relation_server_read_cpm'), side: 'server' },
+ ]);
+ });
+
+ it('keeps a metric that belongs to neither side', () => {
+ const http1 = m('http1_request_cpm', 'process_relation_http1_request_cpm');
+ expect(resolveEdgeMetrics({ edgeMetrics: [http1] })).toEqual([http1]);
+ });
+
+ it('prefers the one list when a template carries both spellings', () => {
+ const one = { ...m('a', 'x'), side: 'client' as const };
+ expect(resolveEdgeMetrics({
+ edgeMetrics: [one],
+ edgeClientMetrics: [m('stale', 'y')],
+ })).toEqual([one]);
+ });
+
+ it('answers empty for a layer that declares none', () => {
+ // There is no built-in set to fall back on: a panel showing metrics no
+ // template named is one the editor cannot show, which is how the two
+ // drifted apart.
+ expect(resolveEdgeMetrics({})).toEqual([]);
+ expect(resolveEdgeMetrics(null)).toEqual([]);
+ });
+});
diff --git a/packages/api-client/src/topology.ts b/packages/api-client/src/topology.ts
index b699f4a9..3bc95d65 100644
--- a/packages/api-client/src/topology.ts
+++ b/packages/api-client/src/topology.ts
@@ -139,13 +139,37 @@ export interface InstanceTopologyConfig {
* OAP exposes a client family and a server family (the conversation is
* observed from both sides of the eBPF probe), so both lists exist —
* mirrors `process_relation_client_*` / `process_relation_server_*`. */
+/** Which end of the conversation a process-relation metric describes. The eBPF
+ * probe observes both, and OAP publishes a family for each — but not every
+ * metric has a side: the HTTP/1.x request and response families describe the
+ * exchange itself. A metric says which it is; nothing is inferred from its
+ * name. */
+export type ProcessRelationSide = 'client' | 'server';
+
export interface ProcessTopologyConfig {
- /** Per-edge MQE under ProcessRelation, client side
- * (`process_relation_client_*`). */
- edgeClientMetrics: TopologyMetricDef[];
- /** Per-edge MQE under ProcessRelation, server side
- * (`process_relation_server_*`). */
- edgeServerMetrics: TopologyMetricDef[];
+ /** Per-edge MQE under ProcessRelation, each carrying its own side (or none).
+ * This replaced two lists split by side, which could not hold a metric that
+ * has none and made the side a property of the metric's NAME. */
+ edgeMetrics?: Array<TopologyMetricDef & { side?: ProcessRelationSide }>;
+ /** @deprecated Read from stored templates written before `edgeMetrics` and
+ * resolved into it; nothing writes these. */
+ edgeClientMetrics?: TopologyMetricDef[];
+ /** @deprecated See {@link ProcessTopologyConfig.edgeClientMetrics}. */
+ edgeServerMetrics?: TopologyMetricDef[];
+}
+
+/** The edge metrics a config exposes, old spelling resolved into the new one.
+ * A template carrying the two side lists reads as one list whose entries say
+ * which side they came from. */
+export function resolveEdgeMetrics(
+ cfg: ProcessTopologyConfig | null | undefined,
+): Array<TopologyMetricDef & { side?: ProcessRelationSide }> {
+ if (!cfg) return [];
+ if (cfg.edgeMetrics?.length) return cfg.edgeMetrics;
+ return [
+ ...(cfg.edgeClientMetrics ?? []).map((m) => ({ ...m, side: 'client' as const })),
+ ...(cfg.edgeServerMetrics ?? []).map((m) => ({ ...m, side: 'server' as const })),
+ ];
}
/** One resolved process-relation metric series for the edge panel. */
@@ -161,6 +185,9 @@ export interface ProcessRelationMetric {
export interface ProcessRelationMetricsResponse {
client: ProcessRelationMetric[];
server: ProcessRelationMetric[];
+ /** Metrics describing the exchange rather than one end of it — the HTTP/1.x
+ * request and response families, which belong to neither side. */
+ shared: ProcessRelationMetric[];
reachable: boolean;
error?: string;
}
diff --git a/packages/api-client/src/trace.ts b/packages/api-client/src/trace.ts
index 0af639ff..a5271eb7 100644
--- a/packages/api-client/src/trace.ts
+++ b/packages/api-client/src/trace.ts
@@ -45,14 +45,20 @@ export type TraceSource = 'native' | 'zipkin' | 'both';
* OpenTelemetry spans OAP converts into Zipkin form
* - `traceql-native` — the Tempo API over native spans
* - `traceql-zipkin` — the Tempo API over Zipkin spans
+ * - `traceql-otlp` — the Tempo API over the OTLP spans OAP stores as they
+ * arrived, converting nothing. It is not the same store
+ * as `traceql-zipkin`: which of the two holds a
+ * deployment's OTLP traces depends on whether its
+ * receiver keeps them natively or converts them.
*/
-export type TraceStore = 'native' | 'zipkin' | 'traceql-native' | 'traceql-zipkin';
+export type TraceStore = 'native' | 'zipkin' | 'traceql-native' | 'traceql-zipkin' | 'traceql-otlp';
export const TRACE_STORES: readonly TraceStore[] = [
'native',
'zipkin',
'traceql-native',
'traceql-zipkin',
+ 'traceql-otlp',
];
/**
diff --git a/packages/api-client/src/traceql.ts b/packages/api-client/src/traceql.ts
index 9f94b1a0..b0a80a7d 100644
--- a/packages/api-client/src/traceql.ts
+++ b/packages/api-client/src/traceql.ts
@@ -17,7 +17,9 @@
/** Which trace store answers a TraceQL query. Each is a separate OAP
* datasource on its own URL; they share this API and this renderer. */
-export type TraceQLDatasource = 'native' | 'zipkin';
+/** Which of OAP's TraceQL datasources answers — one context path each on the
+ * Tempo port. `otlp` serves the spans `receiver-otel` stored natively. */
+export type TraceQLDatasource = 'native' | 'zipkin' | 'otlp';
/** The queried service's own entry into a trace — where it starts relative to
* the trace and how long its first span ran. Only available when the query
@@ -123,8 +125,18 @@ export interface TraceQLTagValuesResponse {
* the socket — can be told apart. */
export interface TraceQLSourceStatus {
ds: TraceQLDatasource;
+ /** The datasource's full URL — empty when this OAP does not serve it. */
url: string;
+ /** The TraceQL SERVICE, which all datasources share. Set whenever the
+ * endpoint is configured, so "the service is off" is told apart from
+ * "the service is on and does not expose this datasource" — different
+ * states needing different fixes, and neither of them unreachable. */
+ endpoint: string;
configured: boolean;
+ /** False when the service ANSWERED but does not serve this datasource — a
+ * 404, which is OAP saying the datasource is not enabled. Absent when the
+ * question never got that far. Distinct from `reachable`: nothing is down. */
+ served?: boolean;
reachable: boolean;
version?: string;
error?: string;