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

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


The following commit(s) were added to refs/heads/main by this push:
     new 389e750  fix(traces,logs): custom time range uses absolute epoch-ms 
(tz-correct) (#93)
389e750 is described below

commit 389e7506d217737eee76e0a37f1d72b22a5f33f6
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Fri Jul 3 15:49:37 2026 +0800

    fix(traces,logs): custom time range uses absolute epoch-ms (tz-correct) 
(#93)
    
    The native trace and log custom-range paths sent a browser-local wall-clock
    string that the server re-read in its own timezone — trace via new Date() in
    the BFF process TZ, logs via passthrough to OAP as server-local. When the
    browser TZ differs from the server TZ (any containerized deploy), the query
    window shifted by the UTC offset and returned no results; the rolling 
presets
    worked because they derive from an absolute instant.
    
    Both now send absolute epoch ms (startMs/endMs) and the BFF converts to
    OAP-server-local via fmtSecond(ms, offset) — the same pattern the other
    surfaces already use (browser errors, events, alarms, global picker, 
Zipkin).
    The operate trace-inspect path drops its now-redundant ISO round-trip. One
    consistent, timezone-proof way everywhere; the metric->trace drill (same 
send
    path) is fixed too.
---
 CHANGELOG.md                               |  4 ++++
 apps/bff/src/http/query/explore.ts         |  6 ++---
 apps/bff/src/http/query/log.ts             | 36 +++++++++++++-----------------
 apps/bff/src/http/query/trace.ts           | 26 ++++++++++-----------
 apps/ui/src/api/scopes/trace.ts            |  4 ++--
 apps/ui/src/layer/logs/LayerLogsView.vue   | 24 ++++++++++----------
 apps/ui/src/layer/logs/useLayerLogs.ts     | 24 ++++++++++----------
 apps/ui/src/layer/logs/useLogTimeRange.ts  | 33 +++++++++------------------
 apps/ui/src/layer/traces/useLayerTraces.ts |  7 +++++-
 packages/api-client/src/logs.ts            | 11 +++++----
 10 files changed, 83 insertions(+), 92 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 09632aa..725b700 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ The version line is shared by every package in the monorepo 
(apps + shared packa
 
 ## 1.0.0
 
+### Traces & logs
+
+- **Custom time ranges on the Traces and Logs tabs now return results when 
your browser and the OAP server sit in different timezones.** A custom range 
(and the metric→trace drill's centered window) was sent as a browser-local 
wall-clock string that the server re-read in its own timezone — so on a 
UTC-container deployment the window shifted by your UTC offset and came back 
empty, while the rolling presets kept working. Both paths now send absolute 
timestamps and the server applies the OA [...]
+
 ### Deployment & configuration
 
 - **Run on the bundled templates, read-only — no OAP ui_template API needed.** 
A new `templates.mode` setting (`HORIZON_TEMPLATES_MODE`) adds a `readonly` 
mode: Horizon renders every dashboard / overview / alert-page / 3D-map / 
translation from the **local bundle** and never calls OAP's ui_template admin 
API. The whole config surface goes **read-only** — the admin pages still open 
and show the bundled config, but editing and publishing are disabled (and the 
BFF rejects a write even if it [...]
diff --git a/apps/bff/src/http/query/explore.ts 
b/apps/bff/src/http/query/explore.ts
index 4dbd578..6affa50 100644
--- a/apps/bff/src/http/query/explore.ts
+++ b/apps/bff/src/http/query/explore.ts
@@ -74,10 +74,10 @@ function resolveNativeEntity(e: ExploreEntity): {
 }
 
 /** Explicit epoch-ms window overrides the rolling minutes; trace.ts reads
- *  `start`/`end` as ISO and `windowMinutes` as the fallback. */
-function traceWindowFields(w: ExploreWindow): Pick<TraceListBody, 
'windowMinutes' | 'start' | 'end'> {
+ *  `startMs`/`endMs` and `windowMinutes` as the fallback. */
+function traceWindowFields(w: ExploreWindow): Pick<TraceListBody, 
'windowMinutes' | 'startMs' | 'endMs'> {
   if (typeof w.startMs === 'number' && typeof w.endMs === 'number' && w.endMs 
> w.startMs) {
-    return { start: new Date(w.startMs).toISOString(), end: new 
Date(w.endMs).toISOString() };
+    return { startMs: w.startMs, endMs: w.endMs };
   }
   return { windowMinutes: w.windowMinutes };
 }
diff --git a/apps/bff/src/http/query/log.ts b/apps/bff/src/http/query/log.ts
index 42dbbd6..adf0842 100644
--- a/apps/bff/src/http/query/log.ts
+++ b/apps/bff/src/http/query/log.ts
@@ -67,20 +67,22 @@ function clampPageSize(requested: number | undefined, 
fallback: number, max: num
  *  round off the most recent log lines for up to a minute, which is
  *  exactly when an operator is triaging.
  *
- *  Three input shapes:
- *    - explicit MINUTE form `YYYY-MM-DD HHmm` (legacy UI custom-range) —
- *      padded to seconds with `00`. The UI emits these in its current TZ;
- *      OAP reads them in OAP-TZ. (Same convention as booster-ui.)
- *    - explicit SECOND form `YYYY-MM-DD HHmmss` — forwarded verbatim.
- *    - no explicit form → rolling fallback, formatted OAP-local at
- *      SECOND precision using the cached server offset. */
+ *  Two shapes: an explicit absolute window (epoch ms) formatted OAP-local
+ *  via `fmtSecond`, or a rolling fallback in minutes ending at "now". */
 function defaultWindow(
   offsetMinutes: number,
   minutes?: number,
-  explicit?: { startTime?: string; endTime?: string },
+  explicit?: { startMs?: number; endMs?: number },
 ): { start: string; end: string } {
-  if (explicit?.startTime && explicit.endTime) {
-    return { start: toSecond(explicit.startTime), end: 
toSecond(explicit.endTime) };
+  if (
+    typeof explicit?.startMs === 'number' &&
+    typeof explicit.endMs === 'number' &&
+    explicit.startMs < explicit.endMs
+  ) {
+    return {
+      start: fmtSecond(explicit.startMs, offsetMinutes),
+      end: fmtSecond(explicit.endMs, offsetMinutes),
+    };
   }
   const m = Number.isFinite(minutes) && (minutes as number) > 0
     ? Math.min(60 * 24 * 7, Math.round(minutes as number))
@@ -90,12 +92,6 @@ function defaultWindow(
   return { start: fmtSecond(startMs, offsetMinutes), end: fmtSecond(endMs, 
offsetMinutes) };
 }
 
-function toSecond(s: string): string {
-  // Pad MINUTE-precision strings ("YYYY-MM-DD HHmm") to seconds. Pass
-  // SECOND-precision strings through unchanged.
-  return /\s\d{4}$/.test(s) ? `${s}00` : s;
-}
-
 const LIST_SERVICES_FOR_RESOLVE = /* GraphQL */ `
   query ListServicesForLogs($layer: String!) {
     services: listServices(layer: $layer) {
@@ -263,8 +259,8 @@ export function registerLogRoute(app: FastifyInstance, 
deps: LogRouteDeps): void
       const opts = buildOapOpts(deps.config.current, deps.fetch);
       const offset = await getServerOffsetMinutes(deps.config, deps.fetch);
       const window = defaultWindow(offset, body.windowMinutes, {
-        startTime: body.startTime,
-        endTime: body.endTime,
+        startMs: body.startMs,
+        endMs: body.endMs,
       });
 
       // Resolve a service NAME to an id if the caller used one.
@@ -328,8 +324,8 @@ export function registerLogRoute(app: FastifyInstance, 
deps: LogRouteDeps): void
       const opts = buildOapOpts(deps.config.current, deps.fetch);
       const offset = await getServerOffsetMinutes(deps.config, deps.fetch);
       const window = defaultWindow(offset, body.windowMinutes, {
-        startTime: body.startTime,
-        endTime: body.endTime,
+        startMs: body.startMs,
+        endMs: body.endMs,
       });
       let serviceId = body.serviceId ?? null;
       if (!serviceId && body.service) {
diff --git a/apps/bff/src/http/query/trace.ts b/apps/bff/src/http/query/trace.ts
index 42125a7..e9e1ddd 100644
--- a/apps/bff/src/http/query/trace.ts
+++ b/apps/bff/src/http/query/trace.ts
@@ -93,16 +93,12 @@ function rollingWindow(minutes: number, offsetMinutes: 
number): { start: string;
   return { start: fmtSecond(startMs, offsetMinutes), end: fmtSecond(endMs, 
offsetMinutes) };
 }
 function explicitWindow(
-  startIso: string,
-  endIso: string,
+  startMs: number,
+  endMs: number,
   offsetMinutes: number,
 ): { start: string; end: string } | null {
-  const s = new Date(startIso);
-  const e = new Date(endIso);
-  if (!Number.isFinite(s.getTime()) || !Number.isFinite(e.getTime()) || 
e.getTime() < s.getTime()) {
-    return null;
-  }
-  return { start: fmtSecond(s.getTime(), offsetMinutes), end: 
fmtSecond(e.getTime(), offsetMinutes) };
+  if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs) 
return null;
+  return { start: fmtSecond(startMs, offsetMinutes), end: fmtSecond(endMs, 
offsetMinutes) };
 }
 
 export interface TraceListBody {
@@ -123,11 +119,10 @@ export interface TraceListBody {
   tags?: Array<{ key: string; value: string }>;
   /** Rolling window in minutes. Default 30; clamped to [1, 10080]. */
   windowMinutes?: number;
-  /** Explicit ISO start (UTC). When both `start` and `end` are
-   *  provided they override `windowMinutes`. */
-  start?: string;
-  /** Explicit ISO end (UTC). Pair with `start`. */
-  end?: string;
+  /** Explicit absolute epoch ms. When both are set they override
+   *  `windowMinutes`; the BFF applies the OAP offset. */
+  startMs?: number;
+  endMs?: number;
   /** Admin Preview: the operator's draft `traces` block (JSON string).
    *  When present + valid, it picks the source instead of the remote
    *  template — same preview path as topology / endpoint-dependency. */
@@ -300,7 +295,10 @@ export async function fetchNativeList(
   const api = await detectTraceQueryApi(opts);
   // Explicit start+end takes precedence over windowMinutes; falling
   // back to the rolling default when the explicit range is invalid.
-  const explicit = body.start && body.end ? explicitWindow(body.start, 
body.end, offsetMinutes) : null;
+  const explicit =
+    typeof body.startMs === 'number' && typeof body.endMs === 'number'
+      ? explicitWindow(body.startMs, body.endMs, offsetMinutes)
+      : null;
   const window = explicit ?? rollingWindow(body.windowMinutes ?? 
DEFAULT_WINDOW_MIN, offsetMinutes);
   let serviceId: string | null = null;
   try {
diff --git a/apps/ui/src/api/scopes/trace.ts b/apps/ui/src/api/scopes/trace.ts
index 09c2486..13a2cf5 100644
--- a/apps/ui/src/api/scopes/trace.ts
+++ b/apps/ui/src/api/scopes/trace.ts
@@ -45,8 +45,8 @@ export class TraceApi {
       pageSize?: number;
       tags?: Array<{ key: string; value: string }>;
       windowMinutes?: number;
-      start?: string;
-      end?: string;
+      startMs?: number;
+      endMs?: number;
       /** Admin preview: the operator's draft `traces` block (JSON string). */
       previewConfig?: string;
     } = {},
diff --git a/apps/ui/src/layer/logs/LayerLogsView.vue 
b/apps/ui/src/layer/logs/LayerLogsView.vue
index 3308f01..51ba07a 100644
--- a/apps/ui/src/layer/logs/LayerLogsView.vue
+++ b/apps/ui/src/layer/logs/LayerLogsView.vue
@@ -182,8 +182,8 @@ const {
   customStart,
   customEnd,
   isCustomRange,
-  startTime: startTimeRef,
-  endTime: endTimeRef,
+  startMs: startMsRef,
+  endMs: endMsRef,
   windowMinutesEffective,
 } = useLogTimeRange(30);
 
@@ -205,8 +205,8 @@ interface AppliedLogConditions {
   keywords: string[];
   tags: LogTagFilter[];
   windowMinutes: number;
-  startTime: string | null;
-  endTime: string | null;
+  startMs: number | null;
+  endMs: number | null;
 }
 function snapshotConditions(): AppliedLogConditions {
   return {
@@ -217,8 +217,8 @@ function snapshotConditions(): AppliedLogConditions {
     keywords: keywordsRef.value,
     tags: allTags.value,
     windowMinutes: windowMinutesEffective.value,
-    startTime: startTimeRef.value,
-    endTime: endTimeRef.value,
+    startMs: startMsRef.value,
+    endMs: endMsRef.value,
   };
 }
 const applied = ref<AppliedLogConditions>(snapshotConditions());
@@ -246,8 +246,8 @@ const aTraceId = computed(() => applied.value.traceId);
 const aKeywords = computed(() => applied.value.keywords);
 const aTags = computed(() => applied.value.tags);
 const aWindowMinutes = computed(() => applied.value.windowMinutes);
-const aStartTime = computed(() => applied.value.startTime);
-const aEndTime = computed(() => applied.value.endTime);
+const aStartMs = computed(() => applied.value.startMs);
+const aEndMs = computed(() => applied.value.endMs);
 
 const { logs, total, isFetching, error, refetch } = useLayerLogs(layerKey, {
   service: aService,
@@ -259,8 +259,8 @@ const { logs, total, isFetching, error, refetch } = 
useLayerLogs(layerKey, {
   page,
   pageSize,
   windowMinutes: aWindowMinutes,
-  startTime: aStartTime,
-  endTime: aEndTime,
+  startMs: aStartMs,
+  endMs: aEndMs,
   enabled: hasQueried,
 });
 
@@ -271,8 +271,8 @@ const { facets, refetch: refetchFacets } = 
useLayerLogFacets(layerKey, {
   traceId: aTraceId,
   keywords: aKeywords,
   windowMinutes: aWindowMinutes,
-  startTime: aStartTime,
-  endTime: aEndTime,
+  startMs: aStartMs,
+  endMs: aEndMs,
   enabled: hasQueried,
 });
 
diff --git a/apps/ui/src/layer/logs/useLayerLogs.ts 
b/apps/ui/src/layer/logs/useLayerLogs.ts
index 43eb5b7..3cd98bd 100644
--- a/apps/ui/src/layer/logs/useLayerLogs.ts
+++ b/apps/ui/src/layer/logs/useLayerLogs.ts
@@ -30,8 +30,8 @@ export interface LogListParams {
   page: Ref<number>;
   pageSize: Ref<number>;
   windowMinutes?: Ref<number>;
-  startTime?: Ref<string | null>;
-  endTime?: Ref<string | null>;
+  startMs?: Ref<number | null>;
+  endMs?: Ref<number | null>;
   /** Gate the query — when false it never runs. Manual-fire pages hold it
    *  until the operator presses Run query. Defaults to always-on. */
   enabled?: Ref<boolean>;
@@ -51,8 +51,8 @@ export function useLayerLogs(layerKey: Ref<string>, params: 
LogListParams) {
       params.page,
       params.pageSize,
       params.windowMinutes ?? computed(() => 0),
-      params.startTime ?? computed(() => null),
-      params.endTime ?? computed(() => null),
+      params.startMs ?? computed(() => null),
+      params.endMs ?? computed(() => null),
     ],
     queryFn: () =>
       bffClient.log.list(layerKey.value, {
@@ -63,8 +63,8 @@ export function useLayerLogs(layerKey: Ref<string>, params: 
LogListParams) {
         ...(params.keywords.value.length > 0 ? { keywordsOfContent: 
params.keywords.value } : {}),
         ...(params.tags.value.length > 0 ? { tags: params.tags.value } : {}),
         ...(params.windowMinutes?.value ? { windowMinutes: 
params.windowMinutes.value } : {}),
-        ...(params.startTime?.value && params.endTime?.value
-          ? { startTime: params.startTime.value, endTime: params.endTime.value 
}
+        ...(params.startMs?.value && params.endMs?.value
+          ? { startMs: params.startMs.value, endMs: params.endMs.value }
           : {}),
         page: params.page.value,
         pageSize: params.pageSize.value,
@@ -97,8 +97,8 @@ export interface LogFacetParams {
   traceId: Ref<string | null>;
   keywords: Ref<string[]>;
   windowMinutes?: Ref<number>;
-  startTime?: Ref<string | null>;
-  endTime?: Ref<string | null>;
+  startMs?: Ref<number | null>;
+  endMs?: Ref<number | null>;
   enabled?: Ref<boolean>;
 }
 
@@ -113,8 +113,8 @@ export function useLayerLogFacets(layerKey: Ref<string>, 
params: LogFacetParams)
       params.traceId,
       params.keywords,
       params.windowMinutes ?? computed(() => 0),
-      params.startTime ?? computed(() => null),
-      params.endTime ?? computed(() => null),
+      params.startMs ?? computed(() => null),
+      params.endMs ?? computed(() => null),
     ],
     queryFn: () =>
       bffClient.log.facets(layerKey.value, {
@@ -124,8 +124,8 @@ export function useLayerLogFacets(layerKey: Ref<string>, 
params: LogFacetParams)
         ...(params.traceId.value ? { traceId: params.traceId.value } : {}),
         ...(params.keywords.value.length > 0 ? { keywordsOfContent: 
params.keywords.value } : {}),
         ...(params.windowMinutes?.value ? { windowMinutes: 
params.windowMinutes.value } : {}),
-        ...(params.startTime?.value && params.endTime?.value
-          ? { startTime: params.startTime.value, endTime: params.endTime.value 
}
+        ...(params.startMs?.value && params.endMs?.value
+          ? { startMs: params.startMs.value, endMs: params.endMs.value }
           : {}),
         sampleSize: 200,
       }),
diff --git a/apps/ui/src/layer/logs/useLogTimeRange.ts 
b/apps/ui/src/layer/logs/useLogTimeRange.ts
index 399605f..0e6f0ff 100644
--- a/apps/ui/src/layer/logs/useLogTimeRange.ts
+++ b/apps/ui/src/layer/logs/useLogTimeRange.ts
@@ -23,11 +23,10 @@
  * dropdown for two `datetime-local` inputs (cap is 7 days, enforced
  * server-side too). Mirrors the trace tab's Custom… escape hatch.
  *
- * Exposes the OAP-shaped query refs the log/facet composables consume:
+ * Exposes the query refs the log/facet composables consume:
  *   - `windowMinutesEffective` — preset minutes, or 0 in custom mode.
- *   - `startTime`/`endTime` — `YYYY-MM-DD HHmm` (OAP minute) in custom
- *     mode, else null. The BFF forwards these to the same
- *     `queryDuration.start/end` slot the trace tab uses.
+ *   - `startMs`/`endMs` — absolute epoch ms in custom mode, else null.
+ *     The BFF applies the OAP offset (same as the trace tab).
  */
 
 import { computed, ref, watch, type ComputedRef, type Ref } from 'vue';
@@ -48,23 +47,13 @@ function fmtDateTimeLocal(d: Date): string {
   return `${d.getFullYear()}-${pad(d.getMonth() + 
1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
 }
 
-/** OAP wants `YYYY-MM-DD HHmm`; the native `datetime-local` input
- *  emits `YYYY-MM-DDTHH:MM`. Convert so the BFF can forward to the
- *  same `queryDuration.start/end` slot the trace tab uses. */
-function toOapMinute(local: string | null): string | null {
-  if (!local) return null;
-  const [d, t] = local.split('T');
-  if (!d || !t) return null;
-  return `${d} ${t.replace(':', '')}`;
-}
-
 export interface LogTimeRange {
   windowMinutes: Ref<number>;
   customStart: Ref<string | null>;
   customEnd: Ref<string | null>;
   isCustomRange: ComputedRef<boolean>;
-  startTime: ComputedRef<string | null>;
-  endTime: ComputedRef<string | null>;
+  startMs: ComputedRef<number | null>;
+  endMs: ComputedRef<number | null>;
   windowMinutesEffective: ComputedRef<number>;
 }
 
@@ -88,11 +77,11 @@ export function useLogTimeRange(initialMinutes = 30): 
LogTimeRange {
     }
   });
 
-  const startTime = computed<string | null>(() =>
-    isCustomRange.value ? toOapMinute(customStart.value) : null,
+  const startMs = computed<number | null>(() =>
+    isCustomRange.value && customStart.value ? new 
Date(customStart.value).getTime() : null,
   );
-  const endTime = computed<string | null>(() =>
-    isCustomRange.value ? toOapMinute(customEnd.value) : null,
+  const endMs = computed<number | null>(() =>
+    isCustomRange.value && customEnd.value ? new 
Date(customEnd.value).getTime() : null,
   );
   const windowMinutesEffective = computed<number>(() =>
     isCustomRange.value ? 0 : windowMinutes.value,
@@ -103,8 +92,8 @@ export function useLogTimeRange(initialMinutes = 30): 
LogTimeRange {
     customStart,
     customEnd,
     isCustomRange,
-    startTime,
-    endTime,
+    startMs,
+    endMs,
     windowMinutesEffective,
   };
 }
diff --git a/apps/ui/src/layer/traces/useLayerTraces.ts 
b/apps/ui/src/layer/traces/useLayerTraces.ts
index b10997a..723ba53 100644
--- a/apps/ui/src/layer/traces/useLayerTraces.ts
+++ b/apps/ui/src/layer/traces/useLayerTraces.ts
@@ -98,7 +98,12 @@ export function useLayerTraces(layerKey: Ref<string>, 
params: TraceListParams) {
         pageSize: params.pageSize.value,
         ...(params.tags.value.length > 0 ? { tags: params.tags.value } : {}),
         ...(params.customStart.value && params.customEnd.value
-          ? { start: params.customStart.value, end: params.customEnd.value }
+          ? // datetime-local is browser-local; send absolute epoch ms so the 
BFF
+            // applies the OAP offset — same as every other query surface.
+            {
+              startMs: new Date(params.customStart.value).getTime(),
+              endMs: new Date(params.customEnd.value).getTime(),
+            }
           : { windowMinutes: params.windowMinutes.value }),
         ...(previewCfg.value ? { previewConfig: previewCfg.value } : {}),
       }),
diff --git a/packages/api-client/src/logs.ts b/packages/api-client/src/logs.ts
index 6784e38..919ba11 100644
--- a/packages/api-client/src/logs.ts
+++ b/packages/api-client/src/logs.ts
@@ -66,13 +66,12 @@ export interface LogQueryRequest {
   pageSize?: number;
   /** Rolling time-window for the query in minutes, ending at "now".
    *  Falls back to the BFF default (~60min) when omitted. Ignored when
-   *  an explicit `startTime` / `endTime` pair is supplied. */
+   *  an explicit `startMs` / `endMs` pair is supplied. */
   windowMinutes?: number;
-  /** Explicit absolute window — `YYYY-MM-DD HHmm` (OAP minute-step
-   *  format). When both `startTime` + `endTime` are set, the rolling
-   *  `windowMinutes` is ignored. */
-  startTime?: string;
-  endTime?: string;
+  /** Explicit absolute window in epoch ms. When both are set the rolling
+   *  `windowMinutes` is ignored; the BFF applies the OAP offset. */
+  startMs?: number;
+  endMs?: number;
 }
 
 export interface LogsResponse {

Reply via email to