mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-06 00:09:22 +00:00
fix(logstream): show local timezone in stream detail time range (#13652)
## Problem The **Stream Detail** header shows the stream's document time range with a timezone badge that is **hardcoded to \`UTC\`** (i18n key \`logStream.utc\`), while the timestamps next to it are rendered by date-fns \`format()\` in the **browser's local timezone**. So the badge and the value disagree for any non-UTC user — the range reads as local time but is mislabeled "UTC". ## Fix - Add \`formatTimestampInTimezone(us, format, timezone)\` in \`web/src/utils/date.ts\` — same as \`formatTimestamp\` but renders in a given IANA zone (via \`date-fns-tz\` \`formatInTimeZone\`, already a dependency), matching the pattern already used across dashboards. - In \`schema.vue\`, add a \`displayTimezone\` computed = \`store.state.timezone || <browser zone>\` (never a hardcoded "UTC"), render it in the badge, and format the \`doc_time_min\`/\`doc_time_max\` range in that same timezone. Now the badge label and the displayed range always use the same timezone — the user's selected timezone, falling back to their browser zone. ## Scope note The separate **Extended Retention** date picker further down still uses UTC intentionally (retention day-boundaries are computed in UTC on the backend), so its \`(UTC Timezone)\` label is left unchanged. ## Validation Verified \`formatInTimeZone\` behavior against the installed \`date-fns-tz@3.2.0\`: | timezone | rendered value | |---|---| | UTC | \`2025-08-03T20:03:22:99\` | | Asia/Kolkata | \`2025-08-04T01:33:22:99\` | | America/New_York | \`2025-08-03T16:03:22:99\` | Label and value now agree in every case.
This commit is contained in:
@@ -41,7 +41,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<span
|
||||
class="text-3xs rounded-default text-text-secondary bg-surface-subtle px-1.5 py-0.5 font-medium"
|
||||
>
|
||||
{{ t("logStream.utc") }}
|
||||
{{ displayTimezone }}
|
||||
</span>
|
||||
<div class="text-text-body text-xs font-semibold">
|
||||
{{ indexData.stats.doc_time_min }}
|
||||
@@ -792,7 +792,10 @@ import { computed, defineComponent, onBeforeMount, ref, watch } from "vue";
|
||||
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import { useStore } from "vuex";
|
||||
import useTheme from "@/composables/useTheme";
|
||||
import { convertUnixToDateFormat as convertUnixToFormat, formatTimestamp } from "@/utils/date";
|
||||
import {
|
||||
convertUnixToDateFormat as convertUnixToFormat,
|
||||
formatTimestampInTimezone,
|
||||
} from "@/utils/date";
|
||||
import streamService from "../../services/stream";
|
||||
import segment from "../../services/segment_analytics";
|
||||
import {
|
||||
@@ -917,6 +920,11 @@ export default defineComponent({
|
||||
const { t } = useI18nTyped();
|
||||
const store = useStore();
|
||||
const { isDark } = useTheme();
|
||||
// Timezone used for the stream-stats time range: the user's selected
|
||||
// timezone, falling back to the browser's zone (never a hardcoded "UTC").
|
||||
const displayTimezone = computed(
|
||||
() => store.state.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
);
|
||||
const indexData: any = ref(defaultValue());
|
||||
const updateSettingsForm: any = ref(null);
|
||||
const isCloud = config.isCloud;
|
||||
@@ -1355,13 +1363,15 @@ export default defineComponent({
|
||||
indexData.value.stats.original_doc_time_max = streamResponse.stats.doc_time_max;
|
||||
indexData.value.stats.original_doc_time_min = streamResponse.stats.doc_time_min;
|
||||
|
||||
indexData.value.stats.doc_time_max = formatTimestamp(
|
||||
indexData.value.stats.doc_time_max = formatTimestampInTimezone(
|
||||
parseInt(streamResponse.stats.doc_time_max),
|
||||
"YYYY-MM-DDTHH:mm:ss:SS",
|
||||
displayTimezone.value,
|
||||
);
|
||||
indexData.value.stats.doc_time_min = formatTimestamp(
|
||||
indexData.value.stats.doc_time_min = formatTimestampInTimezone(
|
||||
parseInt(streamResponse.stats.doc_time_min),
|
||||
"YYYY-MM-DDTHH:mm:ss:SS",
|
||||
displayTimezone.value,
|
||||
);
|
||||
|
||||
indexData.value.defined_schema_fields = streamResponse.settings.defined_schema_fields || [];
|
||||
@@ -2439,6 +2449,7 @@ export default defineComponent({
|
||||
t,
|
||||
raw,
|
||||
store,
|
||||
displayTimezone,
|
||||
config,
|
||||
dateChangeValue,
|
||||
isCloud,
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { format as dfFormat, sub } from "date-fns";
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
import { DateTime as _DateTime } from "luxon";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -267,6 +268,15 @@ export function formatTimestamp(us: number, format: string): string {
|
||||
return formatDate(us / 1000, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a microsecond timestamp in a specific IANA timezone.
|
||||
* Mirrors formatTimestamp but renders in the given timezone instead of the
|
||||
* browser's local zone, so the displayed value matches a timezone label.
|
||||
*/
|
||||
export function formatTimestampInTimezone(us: number, format: string, timezone: string): string {
|
||||
return formatInTimeZone(us / 1000, timezone, normalizeFormat(format));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a nanosecond timestamp to a string.
|
||||
* Converts nanoseconds to milliseconds internally.
|
||||
|
||||
Reference in New Issue
Block a user