Compare commits

..

1 Commits

Author SHA1 Message Date
Yury Molodov
2685a3405d app/vmui: show selected time zone offset in header controls
Signed-off-by: Yury Molodov <yurymolodov@gmail.com>
2026-08-03 13:59:23 +02:00
27 changed files with 450 additions and 556 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -37,7 +37,7 @@
<meta property="og:title" content="UI for VictoriaMetrics">
<meta property="og:url" content="https://victoriametrics.com/">
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
<script type="module" crossorigin src="./assets/index-B1dXK3k7.js"></script>
<script type="module" crossorigin src="./assets/index-D5egN2id.js"></script>
<link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-CNC7AqOf.js">
<link rel="modulepreload" crossorigin href="./assets/vendor-DwJYpOdw.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-CnsZ1jie.css">

View File

@@ -1,4 +1,4 @@
import { FC, useRef } from "preact/compat";
import { forwardRef, useImperativeHandle, useRef } from "preact/compat";
import ServerConfigurator from "./ServerConfigurator/ServerConfigurator";
import { ArrowDownIcon, SettingsIcon } from "../../Main/Icons";
import Button from "../../Main/Button/Button";
@@ -19,7 +19,11 @@ export interface ChildComponentHandle {
handleApply: () => void;
}
const GlobalSettings: FC = () => {
export interface GlobalSettingsHandle {
open: () => void;
}
const GlobalSettings = forwardRef<GlobalSettingsHandle>((_, ref) => {
const { isMobile } = useDeviceDetect();
const appModeEnable = getAppModeEnable();
@@ -66,6 +70,10 @@ const GlobalSettings: FC = () => {
}
].filter(control => control.show);
useImperativeHandle(ref, () => ({
open: handleOpen,
}));
return <>
{isMobile ? (
<div
@@ -131,6 +139,6 @@ const GlobalSettings: FC = () => {
</Modal>
)}
</>;
};
});
export default GlobalSettings;

View File

@@ -0,0 +1,52 @@
import { FC } from "preact/compat";
import Button from "../../../Main/Button/Button";
import { useTimeState } from "../../../../state/time/TimeStateContext";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import { getUTCByTimezone } from "../../../../utils/time";
import { useMemo } from "react";
import { ArrowDownIcon, PlanetIcon } from "../../../Main/Icons";
type Props = {
onOpenSettings?: () => void;
}
const TimeZonePreview: FC<Props> = ({ onOpenSettings }) => {
const { isMobile } = useDeviceDetect();
const { timezone } = useTimeState();
const utcOffset = useMemo(() => getUTCByTimezone(timezone), [timezone]);
const handleOpenSettings = () => {
onOpenSettings && onOpenSettings();
};
if (isMobile) {
return (
<button
className="vm-mobile-option"
onClick={handleOpenSettings}
>
<span className="vm-mobile-option__icon"><PlanetIcon/></span>
<div className="vm-mobile-option-text">
<span className="vm-mobile-option-text__label">Time zone</span>
<span className="vm-mobile-option-text__value">{utcOffset}</span>
</div>
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
</button>
);
}
return (
<Button
className="vm-header-button"
onClick={handleOpenSettings}
startIcon={<PlanetIcon/>}
>
{utcOffset}
</Button>
);
};
export default TimeZonePreview;

View File

@@ -117,6 +117,8 @@ const StepConfigurator: FC = () => {
if (step === value || step === defaultStep) handleApply(defaultStep);
}, [isHistogram, displayType]);
const textValue = isAutoStep ? `auto (${customStep})` : customStep;
return (
<div
className="vm-step-control"
@@ -130,7 +132,7 @@ const StepConfigurator: FC = () => {
<span className="vm-mobile-option__icon"><TimelineIcon/></span>
<div className="vm-mobile-option-text">
<span className="vm-mobile-option-text__label">Step</span>
<span className="vm-mobile-option-text__value">{customStep}</span>
<span className="vm-mobile-option-text__value">{textValue}</span>
</div>
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
</div>
@@ -142,7 +144,7 @@ const StepConfigurator: FC = () => {
startIcon={<TimelineIcon/>}
onClick={toggleOpenOptions}
>
Step: {isAutoStep ? `auto (${customStep})` : customStep}
Step: {textValue}
</Button>
)}
<Popper

View File

@@ -19,7 +19,11 @@ import useBoolean from "../../../../hooks/useBoolean";
import useWindowSize from "../../../../hooks/useWindowSize";
import usePrevious from "../../../../hooks/usePrevious";
export const TimeSelector: FC = () => {
type Props = {
onOpenSettings?: () => void;
}
export const TimeSelector: FC<Props> = ({ onOpenSettings }) => {
const { isMobile } = useDeviceDetect();
const { isDarkTheme } = useAppState();
const wrapperRef = useRef<HTMLDivElement>(null);
@@ -53,7 +57,7 @@ export const TimeSelector: FC = () => {
setFrom(formatDateForNativeInput(dateFromSeconds(start)));
}, [timezone, start]);
const setDuration = ({ duration, until, id }: {duration: string, until: Date, id: string}) => {
const setDuration = ({ duration, until, id }: { duration: string, until: Date, id: string }) => {
dispatch({ type: "SET_RELATIVE_TIME", payload: { duration, until, id } });
handleCloseOptions();
};
@@ -75,16 +79,23 @@ export const TimeSelector: FC = () => {
const setTimeAndClosePicker = () => {
if (from && until) {
dispatch({ type: "SET_PERIOD", payload: {
from: dayjs.tz(from).toDate(),
to: dayjs.tz(until).toDate()
} });
dispatch({
type: "SET_PERIOD", payload: {
from: dayjs.tz(from).toDate(),
to: dayjs.tz(until).toDate()
}
});
}
handleCloseOptions();
};
const onSwitchToNow = () => dispatch({ type: "RUN_QUERY_TO_NOW" });
const handleOpenSettings = () => {
onOpenSettings && onOpenSettings();
handleCloseOptions();
};
const onCancelClick = () => {
setUntil(formatDateForNativeInput(dateFromSeconds(end)));
setFrom(formatDateForNativeInput(dateFromSeconds(start)));
@@ -140,6 +151,7 @@ export const TimeSelector: FC = () => {
</Tooltip>
)}
</div>
<Popper
open={openOptions}
buttonRef={buttonRef}
@@ -179,13 +191,17 @@ export const TimeSelector: FC = () => {
onEnter={setTimeAndClosePicker}
/>
</div>
<div className="vm-time-selector-left-timezone">
<div className="vm-time-selector-left-timezone__title">{activeTimezone.region}</div>
<div className="vm-time-selector-left-timezone__utc">{activeTimezone.utc}</div>
</div>
<button
type="button"
className="vm-time-selector-left-timezone"
onClick={handleOpenSettings}
>
<span className="vm-time-selector-left-timezone__title">{activeTimezone.region}</span>
<span className="vm-time-selector-left-timezone__utc">{activeTimezone.utc}</span>
</button>
<Button
variant="text"
startIcon={<AlarmIcon />}
startIcon={<AlarmIcon/>}
onClick={onSwitchToNow}
>
switch to now

View File

@@ -40,8 +40,13 @@
gap: $padding-small;
font-size: $font-size-small;
margin-bottom: $padding-small;
color: $color-text;
cursor: pointer;
&__title {}
&:hover {
color: $color-primary;
text-decoration: underline;
}
&__utc {
display: inline-flex;

View File

@@ -633,3 +633,14 @@ export const DebugIcon = () => (
/>
</svg>
);
export const PlanetIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2M4 12c0-.61.08-1.21.21-1.78L8.99 15v1c0 1.1.9 2 2 2v1.93C7.06 19.43 4 16.07 4 12m13.89 5.4c-.26-.81-1-1.4-1.9-1.4h-1v-3c0-.55-.45-1-1-1h-6v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41C17.92 5.77 20 8.65 20 12c0 2.08-.81 3.98-2.11 5.4"
></path>
</svg>
);

View File

@@ -11,6 +11,7 @@
&_mobile {
display: grid;
grid-template-columns: 1fr;
gap: 0;
padding: 0;
flex-grow: initial;

View File

@@ -6,9 +6,11 @@ import StepConfigurator from "../../components/Configurators/StepConfigurator/St
import { TimeSelector } from "../../components/Configurators/TimeRangeSettings/TimeSelector/TimeSelector";
import CardinalityDatePicker from "../../components/Configurators/CardinalityDatePicker/CardinalityDatePicker";
import { ExecutionControls } from "../../components/Configurators/TimeRangeSettings/ExecutionControls/ExecutionControls";
import GlobalSettings from "../../components/Configurators/GlobalSettings/GlobalSettings";
import GlobalSettings, { GlobalSettingsHandle } from "../../components/Configurators/GlobalSettings/GlobalSettings";
import ShortcutKeys from "../../components/Main/ShortcutKeys/ShortcutKeys";
import { ControlsProps } from "../Header/HeaderControls/HeaderControls";
import { useRef } from "react";
import TimeZonePreview from "../../components/Configurators/GlobalSettings/TimeZonePreview/TimeZonePreview";
const ControlsMainLayout: FC<ControlsProps> = ({
displaySidebar,
@@ -17,6 +19,7 @@ const ControlsMainLayout: FC<ControlsProps> = ({
accountIds,
closeModal,
}) => {
const settingsRef = useRef<GlobalSettingsHandle>(null);
return (
<div
@@ -27,14 +30,15 @@ const ControlsMainLayout: FC<ControlsProps> = ({
>
{headerSetup?.tenant && <TenantsConfiguration accountIds={accountIds || []}/>}
{headerSetup?.stepControl && <StepConfigurator/>}
{headerSetup?.timeSelector && <TimeSelector/>}
{headerSetup?.timeSelector && <TimeSelector onOpenSettings={() => settingsRef.current?.open()}/>}
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
<TimeZonePreview onOpenSettings={() => settingsRef.current?.open()}/>
{headerSetup?.executionControls && <ExecutionControls
tooltip={headerSetup?.executionControls?.tooltip}
useAutorefresh={headerSetup?.executionControls?.useAutorefresh}
closeModal={closeModal}
/>}
<GlobalSettings/>
<GlobalSettings ref={settingsRef}/>
{!displaySidebar && <ShortcutKeys/>}
</div>
);

View File

@@ -1,11 +1,12 @@
@use "src/styles/variables" as *;
.vm-mobile-option {
display: flex;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
justify-content: flex-start;
gap: $padding-small;
padding: calc($padding-medium/2) 0;
gap: $padding-global;
padding: $padding-global $padding-small;
width: 100%;
user-select: none;
@@ -17,14 +18,33 @@
}
&__icon {
width: 22px;
height: 22px;
position: relative;
display: flex;
width: 40px;
height: 40px;
color: $color-primary;
&:after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.1;
background-color: currentColor;
border-radius: $border-radius-medium;
}
svg {
width: 21px;
height: auto;
}
}
&__arrow {
width: 14px;
height: 14px;
width: 20px;
height: 20px;
transform: rotate(-90deg);
color: $color-primary;
}
@@ -32,11 +52,13 @@
&-text {
display: grid;
align-items: center;
gap: 2px;
height: 100%;
gap: calc($padding-small / 2);
flex-grow: 1;
text-align: left;
&__label {
font-weight: bold;
font-weight: 600;
}
&__value {

View File

@@ -26,20 +26,33 @@ func TestClusterSearchWithDisabledPerDayIndex(t *testing.T) {
defer tc.Stop()
testSearchWithDisabledPerDayIndex(tc, func(name string, disablePerDayIndex bool) apptest.PrometheusWriteQuerier {
vmstorage := tc.MustStartVmstorage("vmstorage-"+name, []string{
"-storageDataPath=" + tc.Dir() + "/vmstorage",
// Using static ports for vmstorage because random ports may cause
// changes in how data is sharded.
vmstorage1 := tc.MustStartVmstorage("vmstorage1-"+name, []string{
"-storageDataPath=" + tc.Dir() + "/vmstorage1",
"-retentionPeriod=100y",
"-httpListenAddr=127.0.0.1:61001",
"-vminsertAddr=127.0.0.1:61002",
"-vmselectAddr=127.0.0.1:61003",
fmt.Sprintf("-disablePerDayIndex=%t", disablePerDayIndex),
})
vmstorage2 := tc.MustStartVmstorage("vmstorage2-"+name, []string{
"-storageDataPath=" + tc.Dir() + "/vmstorage2",
"-retentionPeriod=100y",
"-httpListenAddr=127.0.0.1:62001",
"-vminsertAddr=127.0.0.1:62002",
"-vmselectAddr=127.0.0.1:62003",
fmt.Sprintf("-disablePerDayIndex=%t", disablePerDayIndex),
})
vminsert := tc.MustStartVminsert("vminsert-"+name, []string{
"-storageNode=" + vmstorage.VminsertAddr(),
"-storageNode=" + vmstorage1.VminsertAddr() + "," + vmstorage2.VminsertAddr(),
})
vmselect := tc.MustStartVmselect("vmselect"+name, []string{
"-storageNode=" + vmstorage.VmselectAddr(),
"-storageNode=" + vmstorage1.VmselectAddr() + "," + vmstorage2.VmselectAddr(),
"-search.maxStalenessInterval=1m",
})
return &apptest.Vmcluster{
Vmstorages: []*apptest.Vmstorage{vmstorage},
Vmstorages: []*apptest.Vmstorage{vmstorage1, vmstorage2},
Vminsert: vminsert,
Vmselect: vmselect,
}

View File

@@ -829,7 +829,7 @@ See also [minimum downtime strategy](#minimum-downtime-strategy).
## Slowness-based re-routing
By default{{% available_from "v1.149.0" %}}, `vminsert` automatically re-routes writes away from the slowest `vmstorage` node
By default{{% available_from "#" %}}, `vminsert` automatically re-routes writes away from the slowest `vmstorage` node
to preserve maximum ingestion throughput. This prevents a single slow `vmstorage` node
from throttling the entire cluster.
@@ -843,7 +843,7 @@ Disable slowness-based re-routing with `-disableRerouting=true` when keeping met
perfectly balanced across nodes or minimizing the number of [active time series](https://docs.victoriametrics.com/victoriametrics/faq/#what-is-an-active-time-series)
matters more than peak write throughput.
Slowness-based re-routing is automatically disabled{{% available_from "v1.149.0" %}} when `-replicationFactor` is greater than `1`,
Slowness-based re-routing is automatically disabled{{% available_from "#" %}} when `-replicationFactor` is greater than `1`,
because rerouting does not guarantee that replicated copies land on distinct storage nodes,
which violates the replication contract.

View File

@@ -39,8 +39,6 @@ VictoriaMetrics has the following prominent features:
* Easy and fast backups from [instant snapshots](https://medium.com/@valyala/how-victoriametrics-makes-instant-snapshots-for-multi-terabyte-time-series-data-e1f3fb0e0282)
can be done with [vmbackup](https://docs.victoriametrics.com/victoriametrics/vmbackup/) / [vmrestore](https://docs.victoriametrics.com/victoriametrics/vmrestore/) tools.
See [this article](https://medium.com/@valyala/speeding-up-backups-for-big-time-series-databases-533c1a927883) for more details.
* It supports storage and retrieval of samples with timestamps that fall within the `[1970-01-02T00:00:00.000Z, 2262-03-31T23:59:59.999Z]` time range with millisecond precision.
See [Retention](#retention) for details.
* It implements a PromQL-like query language - [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/), which provides improved functionality on top of PromQL.
* It provides a global query view. Multiple Prometheus instances or any other data sources may ingest data into VictoriaMetrics. Later this data may be queried via a single query.
* It provides high performance and good vertical and horizontal scalability for both
@@ -1542,9 +1540,6 @@ It is safe to extend `-retentionPeriod` on existing data. If `-retentionPeriod`
value than before, then data outside the configured period will be eventually deleted.
VictoriaMetrics does not support indefinite retention, but you can specify an arbitrarily high duration, e.g. `-retentionPeriod=100y`.
Just keep in mind that VictoriaMetrics does not support samples with negative timestamps. Timestamps at `1970-01-01` are also not
supported because this date has a special meaning internally. It therefore rejects samples with timestamps before
`1970-01-02T00:00:00.000Z`.
By default, VictoriaMetrics doesn't accept samples with timestamps bigger than `now+2d`, e.g. 2 days in the future.
If you need accepting samples with bigger timestamps, then specify the desired "future retention" via `-futureRetention` command-line flag.
@@ -1556,9 +1551,6 @@ For example, the following command starts VictoriaMetrics, which accepts samples
/path/to/victoria-metrics -futureRetention=1y
```
VictoriaMetrics does not support stamples after `2262-03-31T23:59:59.999Z`. If the future retention includes dates after this timestamp,
the samples for those dates will be rejected.
By default, VictoriaMetrics accepts samples with timestamps as old as the configured `-retentionPeriod` allows, e.g. it accepts backfilled
historical data as long as it fits into the retention. If you need rejecting samples with historical timestamps older than the specified
duration, then specify the desired duration via the `-maxBackfillAge` command-line flag. This can be useful for limiting ingestion of

View File

@@ -26,10 +26,6 @@ See also [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-rel
## tip
## [v1.149.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.149.0)
Release candidate
**Update Note 1:** `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): the default value of `-disableRerouting` flag has changed from `true` to `false`, enabling [slowness-based re-routing](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#slowness-based-re-routing) by default. Slowness re-routing is automatically disabled when `-replicationFactor` is greater than 1. If you rely on the old behavior, pass `-disableRerouting` command-line flag to `vminsert`. See [#11287](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11287).
**Update Note 2:** [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): the `/api/v1/admin/tsdb/delete_series`, `/tags/delSeries` endpoints now require `POST` method. Previously, it also accepted `GET` requests. If you use `GET` requests for this endpoint, update your scripts or tooling to use `POST` instead. See [#5552](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5552).
@@ -40,18 +36,18 @@ Release candidate
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): add `name` label identifying the corresponding `-remoteWrite.url` target to the `vm_persistentqueue_*` metrics exposed by persistent queue. See [#7944](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7944). Thanks to @tIGO for contribution.
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): add `-remoteWrite.obfuscateLabels` flag for hashing values of the specified labels before sending metrics to the corresponding `-remoteWrite.url`. This allows sharing metrics with external systems while keeping sensitive label values hidden. See [#10599](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10599).
* FEATURE: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): add `-replay.continueWithExecutionErr` flag to allow continuing to replay other rules when a rule execution fails with a 422 response code, which can happen due to an expression syntax error or a resource limit being hit. See [11313](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11313).
* FEATURE: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): add template variable `$interval` to expose the alerting rule group's evaluation interval. This allows generating dashboard links with a lookback window relative to the rule's interval, for example `&from={{ ($activeAt.Add (parseDurationTime (printf "-%s" .Interval))).UnixMilli }}&to={{ $activeAt.UnixMilli }}`. See [#11232](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11232). Thanks to @1solomonwakhungu for contribution.
* FEATURE: [vmbackupmanager](https://docs.victoriametrics.com/victoriametrics/vmbackupmanager/) and [alerts](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/rules): introduce `vm_backup_last_success_at` metric to track the last successful backup by type. Add [alerting rules](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/rules/alerts-vmbackupmanager.yml) `NoLatestBackupWithinLastDay`, `NoHourlyBackupWithinLastDay`, `NoDailyBackupWithinLast3Days`, `NoWeeklyBackupWithinLast14Days` and `NoMonthlyBackupWithinLast62Days` to remind users about the missing backups. See [#11217](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11217).
* FEATURE: [vmctl](https://docs.victoriametrics.com/victoriametrics/vmctl/): support [Prometheus native histograms](https://prometheus.io/docs/specs/native_histograms/) migration in [remote read mode](https://docs.victoriametrics.com/victoriametrics/vmctl/remoteread/). Native histograms are converted into `_count`, `_sum` and `_bucket` series with `vmrange` labels in the same way as VictoriaMetrics [converts native histograms received via Prometheus remote write protocol](https://docs.victoriametrics.com/victoriametrics/integrations/prometheus/#native-histograms), except that for native histograms with custom buckets the original bucket bounds are preserved instead of being estimated with the exponential formula. Previously native histograms were silently ignored in `SAMPLES` mode, while in stream mode the migration failed with `EOF` error. See [#11292](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11292). Thanks to @liuxu623 for contribution.
* FEATURE: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): add template variable `$interval` to expose the alerting rule group's evaluation interval. This allows generating dashboard links with a lookback window relative to the rule's interval, for example `&from={{ ($activeAt.Add (parseDurationTime (printf "-%s" .Interval))).UnixMilli }}&to={{ $activeAt.UnixMilli }}`. See this issue [#11232](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11232) for more details. Thanks to @1solomonwakhungu for contribution.
* FEATURE: [vmbackupmanager](https://docs.victoriametrics.com/victoriametrics/vmbackupmanager/) and [alerts](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/rules): introduce `vm_backup_last_success_at` metric to track the last successful backup by type. Add [alerting rules](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/rules/alerts-vmbackupmanager.yml) `NoLatestBackupWithinLastDay`, `NoHourlyBackupWithinLastDay`,
`NoDailyBackupWithinLast3Days`, `NoWeeklyBackupWithinLast14Days` and `NoMonthlyBackupWithinLast62Days` to remind users about the missing backups. See [#11217](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11217).
* FEATURE: [vmctl](https://docs.victoriametrics.com/victoriametrics/vmctl/): support [Prometheus native histograms](https://prometheus.io/docs/specs/native_histograms/) migration in [remote read mode](https://docs.victoriametrics.com/victoriametrics/vmctl/remoteread/). Native histograms are converted into `_count`, `_sum` and `_bucket` series with `vmrange` labels in the same way as VictoriaMetrics converts native histograms received via Prometheus remote write protocol, except that for native histograms with custom buckets the original bucket bounds are preserved instead of being estimated with the exponential formula. Previously native histograms were silently ignored in `SAMPLES` mode, while in stream mode the migration failed with `EOF` error. See [#11292](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11292). Thanks to @liuxu623 for contribution.
* FEATURE: `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): enable [slowness-based re-routing](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#slowness-based-re-routing) by default. Previously, `-disableRerouting` defaulted to `true`, which limited ingestion throughput to the slowest `vmstorage` node. Now `-disableRerouting` defaults to `false`, so `vminsert` automatically routes data away from the slowest `vmstorage` node, improving overall ingestion performance. Slowness re-routing is automatically disabled when `-replicationFactor` is greater than 1. See [#11287](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11287).
* FEATURE: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): persist the selected auto-refresh interval in the URL. See [VictoriaLogs#1310](https://github.com/VictoriaMetrics/VictoriaLogs/issues/1310).
* FEATURE: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): show the selected time zone UTC offset next to the date/time controls and allow opening time zone settings from it.
* BUGFIX: [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/): properly drop data points filtered out by an inner [comparison operation](https://prometheus.io/docs/prometheus/latest/querying/operators/#comparison-binary-operators) when its result is used on the right side of another comparison. Previously, queries like `foo != (bar > 100)` could return unexpected results because filtered-out data points are represented internally as `NaN`, and `value != NaN` evaluates to `true`. Comparisons against explicitly present `NaN` values keep the previous behavior. See [#10018](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10018). Thanks to @zasdaym for contribution.
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/) and [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/): ignore HTTP proxy environment variables when scraping targets over Unix domain sockets. See [#11318](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11318). Thanks to @lwmacct for contribution.
* BUGFIX: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): fixed the display of rule state badges on the `Groups` page in the web UI. See [#11160](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11160).
* BUGFIX: [vmbackupmanager](https://docs.victoriametrics.com/victoriametrics/vmbackupmanager/): previously, `vmbackupmanager` was crashing on startup when it failed to restore backup state from remote storage, causing a crash loop. Now it logs the error and continues running, retrying the state restore before each scheduled backup. Added `vm_backup_errors_total{type="restoreState"}` metric to track backup state restore failures. See [#11217](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11217).
* BUGFIX: [stream aggregation](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/): fix incorrect [sum_samples_total](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#sum_samples_total) results when `enable_windows: true` is set. See [#11261](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11261). Thanks to @beyond-infra for contribution.
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/), `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) and [vmctl](https://docs.victoriametrics.com/victoriametrics/vmctl/): accept scientific notation with sub-second precision (e.g. `1.784144612388E9`) for timestamp args such as `start` and `end` in `/api/v1/query_range` and `--vm-native-filter-time-start` and `--vm-native-filter-time-end` in `vmctl`. Previously, values with this pattern were rejected, which is incompatible with Prometheus. See [#11268](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11268). Thanks to @STiFLeR7 for contribution.
## [v1.148.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.148.0)

View File

@@ -688,7 +688,7 @@ Extra labels can be added to metrics collected by `vmagent` via the following me
## Obfuscating label values
`vmagent` can obfuscate the values of specified labels before sending metrics to `-remoteWrite.url`
via `-remoteWrite.obfuscateLabels`{{% available_from "v1.149.0" %}}.
via `-remoteWrite.obfuscateLabels`{{% available_from "#" %}}.
This is useful when one or more `-remoteWrite.url` endpoints point to external monitoring services
outside the organization, and sensitive label values such as `ip`, `host`, `instance`, or `datacenter`

View File

@@ -480,7 +480,7 @@ Clusters here are referred to as `source` and `destination`.
To verify that `vmbackupmanager` is executing backup tasks normally, the following metrics can help:
* `vm_backup_last_success_at{type="<backup_type>"}` - unix timestamp of the last successful backup{{% available_from "v1.149.0" %}}. Remains `0` if no backup has completed successfully since startup. Check error logs and verify remote storage accessibility if this persists.
* `vm_backup_last_success_at{type="<backup_type>"}` - unix timestamp of the last successful backup{{% available_from "#" %}}. Remains `0` if no backup has completed successfully since startup. Check error logs and verify remote storage accessibility if this persists.
* `vm_backup_last_run_failed{type="<backup_type>"}` - whether the last backup task for the given backup type failed. The value `1` means the last task failed. Check the error logs of `vmbackupmanager` for the root cause
* `vm_backup_errors_total{type="<backup_type>"}` - total number of backup errors for the given backup type.

View File

@@ -1290,9 +1290,6 @@ func (pt *partition) mergeParts(pws []*partWrapper, stopCh <-chan struct{}, isFi
putBlockStreamReader(bsr)
}
if err != nil {
if mpNew != nil {
putInmemoryPart(mpNew)
}
return err
}
if mpNew != nil {
@@ -1447,8 +1444,6 @@ func (pt *partition) openCreatedPart(ph *partHeader, pws []*partWrapper, mpNew *
// The created part is empty. Remove it
if mpNew == nil {
fs.MustRemoveDir(dstPartPath)
} else {
putInmemoryPart(mpNew)
}
return nil
}

View File

@@ -1118,14 +1118,6 @@ func searchAndMerge[T any](qt *querytracer.Tracer, s *Storage, tr TimeRange, sea
qt = qt.NewChild("search indexDBs: timeRange=%v", &tr)
defer qt.Done()
var zeroValue T
if tr.MinTimestamp < minUnixMilli {
tr.MinTimestamp = minUnixMilli
}
if tr.MaxTimestamp < tr.MinTimestamp {
return zeroValue, nil
}
var idbts []indexDBWithType
ptws := s.tb.GetPartitions(tr)

View File

@@ -402,10 +402,6 @@ func TestStorageDeletePendingSeries(t *testing.T) {
defer testRemoveAll(t)
const numMonths = 10
start := time.Date(1971, 1, 1, 0, 0, 0, 0, time.UTC)
middle := start.AddDate(0, (numMonths-1)/2, 0)
end := start.AddDate(0, numMonths-1, 0)
s := MustOpenStorage(t.Name(), OpenOptions{})
var metricGroupName = []byte("metric")
@@ -460,7 +456,7 @@ func TestStorageDeletePendingSeries(t *testing.T) {
assertCountMonthsWithLabels := func(count int) {
t.Helper()
ts := start
ts := time.Unix(0, 0)
n := 0
for range numMonths {
lns, err := s.SearchLabelNames(nil, nil, TimeRange{ts.UnixMilli(), ts.UnixMilli()}, 1e5, 1e9, noDeadline)
@@ -485,7 +481,7 @@ func TestStorageDeletePendingSeries(t *testing.T) {
var search Search
defer search.MustClose()
search.Init(nil, s, []*TagFilters{tfs}, TimeRange{start.UnixMilli(), math.MaxInt64}, 1e5, noDeadline)
search.Init(nil, s, []*TagFilters{tfs}, TimeRange{0, math.MaxInt64}, 1e5, noDeadline)
n := 0
for search.NextMetricBlock() {
var b Block
@@ -502,6 +498,10 @@ func TestStorageDeletePendingSeries(t *testing.T) {
// Verify no metrics exist
assertCountRows(0)
start := time.Unix(0, 0)
middle := start.AddDate(0, (numMonths-1)/2, 0)
end := start.AddDate(0, numMonths-1, 0)
// Add some rows and flush, so next DeleteSeries() can delete them
addRows(start, middle, false)
s.DebugFlush()
@@ -3385,190 +3385,53 @@ func TestStorageQueryWithoutIndex(t *testing.T) {
testStorageSearchWithoutIndex(t, &opts)
}
func TestStorageAddRowsWithZeroDate(t *testing.T) {
func TestStorageAddRows_SamplesWithZeroDate(t *testing.T) {
defer testRemoveAll(t)
f := func(t *testing.T, disablePerDayIndex bool) {
t.Helper()
s := MustOpenStorage(t.Name(), OpenOptions{
DisablePerDayIndex: disablePerDayIndex,
})
defer s.MustClose()
mn := MetricName{MetricGroup: []byte("metric")}
mr := MetricRow{MetricNameRaw: mn.marshalRaw(nil)}
for range 10 {
mr.Timestamp = rand.Int63n(msecPerDay)
mr.Value = float64(rand.Intn(1000))
s.AddRows([]MetricRow{mr}, defaultPrecisionBits)
s.DebugFlush()
// Reset TSID cache so that insertion takes the path that involves
// checking whether the index contains metricName->TSID mapping.
s.resetAndSaveTSIDCache()
}
want := 1
firstUnixDay := TimeRange{
MinTimestamp: 0,
MaxTimestamp: msecPerDay - 1,
}
if got := s.newTimeseriesCreated.Load(); got != uint64(want) {
t.Errorf("unexpected new timeseries count: got %d, want %d", got, want)
}
if got := testCountAllMetricNames(s, firstUnixDay); got != want {
t.Errorf("unexpected metric name count: got %d, want %d", got, want)
}
if got := testCountAllMetricIDs(s, firstUnixDay); got != want {
t.Errorf("unexpected metric id count: got %d, want %d", got, want)
}
}
for _, disablePerDayIndex := range []bool{false, true} {
name := fmt.Sprintf("disablePerDayIndex=%t", disablePerDayIndex)
t.Run(name, func(t *testing.T) {
testStorageAddRowsWithZeroDate(t, disablePerDayIndex)
f(t, disablePerDayIndex)
})
}
}
func testStorageAddRowsWithZeroDate(t *testing.T, disablePerDayIndex bool) {
s := MustOpenStorage(t.Name(), OpenOptions{
DisablePerDayIndex: disablePerDayIndex,
})
defer s.MustClose()
const numDays = 4
var metricNamesAll []string
labelNamesAll := []string{"__name__", "label"}
var labelValuesAll []string
mrs := make([]MetricRow, numDays)
for day := range numDays {
metricName := fmt.Sprintf("metric_%02d", day)
labelName := fmt.Sprintf("label_%02d", day)
labelValue := fmt.Sprintf("value_%02d", day)
if day != 0 {
metricNamesAll = append(metricNamesAll, metricName)
labelNamesAll = append(labelNamesAll, labelName)
labelValuesAll = append(labelValuesAll, labelValue)
}
mn := MetricName{
MetricGroup: []byte(metricName),
Tags: []Tag{
{Key: []byte(labelName), Value: []byte("value")},
{Key: []byte("label"), Value: []byte(labelValue)},
},
}
mn.sortTags()
mrs[day].MetricNameRaw = mn.marshalRaw(nil)
mrs[day].Timestamp = int64(day * msecPerDay)
}
s.AddRows(mrs, defaultPrecisionBits)
s.DebugFlush()
if got, want := s.newTimeseriesCreated.Load(), uint64(numDays-1); got != want {
t.Fatalf("unexpected new timeseries count: got %d, want %d", got, want)
}
if got, want := s.tooSmallTimestampRows.Load(), uint64(1); got != want {
t.Fatalf("unexpected rows with too small timestamp: got %d, want %d", got, want)
}
assertMetricNames := func(tr TimeRange, want []string) {
t.Helper()
tfs := NewTagFilters()
if err := tfs.Add(nil, []byte("metric_.*"), false, true); err != nil {
t.Fatalf("unexpected error in TagFilters.Add: %v", err)
}
got, err := s.SearchMetricNames(nil, []*TagFilters{tfs}, tr, 1e9, noDeadline)
if err != nil {
t.Fatalf("SearchMetricNames(%v, %v) failed unexpectedly: %v", tfs, &tr, err)
}
for i, name := range got {
var mn MetricName
if err := mn.UnmarshalString(name); err != nil {
t.Fatalf("Could not unmarshal metric name %q: %v", name, err)
}
got[i] = string(mn.MetricGroup)
}
slices.Sort(got)
slices.Sort(want)
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("unexpected metric names (-want, +got):\n%s", diff)
}
}
assertLabelNames := func(tr TimeRange, want []string) {
t.Helper()
tfs := NewTagFilters()
if err := tfs.Add(nil, []byte("metric_.*"), false, true); err != nil {
t.Fatalf("unexpected error in TagFilters.Add: %v", err)
}
got, err := s.SearchLabelNames(nil, []*TagFilters{tfs}, tr, 1e9, 1e9, noDeadline)
if err != nil {
t.Fatalf("SearchLabelNames(%v, %v) failed unexpectedly: %s", tfs, &tr, err)
}
slices.Sort(got)
slices.Sort(want)
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("unexpected label names (-want, +got):\n%s", diff)
}
}
assertLabelValues := func(tr TimeRange, want []string) {
t.Helper()
tfs := NewTagFilters()
if err := tfs.Add([]byte("label"), []byte("value_.*"), false, true); err != nil {
t.Fatalf("unexpected error in TagFilters.Add: %v", err)
}
got, err := s.SearchLabelValues(nil, "label", []*TagFilters{tfs}, tr, 1e9, 1e9, noDeadline)
if err != nil {
t.Fatalf("SearchLabelValues(%v, %v) failed unexpectedly: %s", tfs, tr, err)
}
slices.Sort(got)
slices.Sort(want)
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("unexpected label values (-want, +got):\n%s", diff)
}
}
assertData := func(tr TimeRange, want []MetricRow) {
t.Helper()
tfs := NewTagFilters()
if err := tfs.Add(nil, []byte("metric_.*"), false, true); err != nil {
t.Fatalf("TagFilters.Add() failed unexpectedly: %v", err)
}
if err := testAssertSearchResult(s, tr, tfs, want); err != nil {
t.Fatalf("Search(%v, %v) failed unexpectedly: %v", tfs, tr, err)
}
}
var tr TimeRange
// Empty time range.
// Expect empty search results
tr = TimeRange{}
assertMetricNames(tr, nil)
assertLabelNames(tr, []string{})
assertLabelValues(tr, []string{})
assertData(tr, nil)
// First day time range.
// Expect empty search results
tr = TimeRange{
MinTimestamp: 0,
MaxTimestamp: msecPerDay - 1,
}
assertMetricNames(tr, nil)
assertLabelNames(tr, []string{})
assertLabelValues(tr, []string{})
assertData(tr, nil)
// Second day time range.
tr = TimeRange{
MinTimestamp: msecPerDay,
MaxTimestamp: 2*msecPerDay - 1,
}
if disablePerDayIndex {
// Expect index search results for all days if per-day index is
// disabled.
assertMetricNames(tr, metricNamesAll)
assertLabelNames(tr, labelNamesAll)
assertLabelValues(tr, labelValuesAll)
} else {
// Expect index search results on second day only if per-day index is
// enabled.
assertMetricNames(tr, []string{"metric_01"})
assertLabelNames(tr, []string{"__name__", "label", "label_01"})
assertLabelValues(tr, []string{"value_01"})
}
assertData(tr, mrs[1:2])
// First two days time range.
// Expect results on second day only.
tr = TimeRange{
MinTimestamp: 0,
MaxTimestamp: 2*msecPerDay - 1,
}
if disablePerDayIndex {
// Expect index search results for all days if per-day index is
// disabled.
assertMetricNames(tr, metricNamesAll)
assertLabelNames(tr, labelNamesAll)
assertLabelValues(tr, labelValuesAll)
} else {
// Expect index search results on second day only if per-day index is
// enabled.
assertMetricNames(tr, []string{"metric_01"})
assertLabelNames(tr, []string{"__name__", "label", "label_01"})
assertLabelValues(tr, []string{"value_01"})
}
assertData(tr, mrs[1:2])
}
// testSearchMetricIDs returns metricIDs for the given tfss and tr.
//
// The returned metricIDs are sorted. The function panics in in case of error.

View File

@@ -429,8 +429,9 @@ func (tb *table) getMinMaxIngestionTimestamps() (int64, int64) {
func (tb *table) getMinMaxTimestampsForAge(minAgeMsecs int64) (int64, int64) {
now := int64(fasttime.UnixTimestamp() * 1000)
minTimestamp := now - minAgeMsecs
if minTimestamp < minUnixMilli {
minTimestamp = minUnixMilli
if minTimestamp < 0 {
// Negative timestamps aren't supported by the storage.
minTimestamp = 0
}
maxTimestamp := int64(maxUnixMilli)
if maxUnixMilli-now > tb.s.futureRetentionMsecs {

View File

@@ -40,6 +40,12 @@ type TimeRange struct {
MaxTimestamp int64
}
// Zero time range and zero date are used to force global index search.
var (
globalIndexTimeRange = TimeRange{}
globalIndexDate = uint64(0)
)
// DateRange returns the date range for the given time range.
func (tr *TimeRange) DateRange() (uint64, uint64) {
minDate := uint64(tr.MinTimestamp) / msecPerDay
@@ -111,29 +117,10 @@ func (tr *TimeRange) contains(timestamp int64) bool {
return tr.MinTimestamp <= timestamp && timestamp <= tr.MaxTimestamp
}
// Zero time range and zero date are used to force global index search.
var (
globalIndexDate = uint64(0)
globalIndexTimeRange = TimeRange{}
)
const (
msecPerDay = 24 * 3600 * 1000
msecPerHour = 3600 * 1000
// minUnixMilli is the min millisecond that is allowed to be used as the
// sample timestamp.
//
// It corresponds to the first millisecond of the second day of the Unix
// Epoch, i.e. 1970-01-02T00:00:00.000Z.
//
// The first day of the Unix Epoch is reserved: zero date and zero time
// range are used for indicating that the the global index search is
// required. See globalIndexDate and globalIndexTimeRange above.
//
// Negative timestamps aren't supported.
minUnixMilli = msecPerDay
// maxUnixMilli is the max millisecond that is allowed to be used as the
// sample timestamp.
//
@@ -143,6 +130,6 @@ const (
// time.UnixMicro(math.MaxInt64/1000) == 2262-04-11 23:47:16.854775 UTC.
//
// Round it to the last millisecond of the last complete partition:
// 2262-03-31T23:59:59.999Z.
// 2262-03-31 23:59:59.999 UTC.
maxUnixMilli = 9222422399999
)

View File

@@ -935,25 +935,4 @@ foo:1m_sum_samples{baz="qwe"} 10
dedup_interval: 30s
outputs: [sum_samples]
`, "11111111")
// Reproduce issue #11261: sum_samples_total must be monotonic with enable_windows: true
// See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11262
f([]string{`
test_delta 1
`, `
test_delta 1
`, `
test_delta 1
`, `
test_delta 1
`}, time.Minute, `test_delta 1
test_delta 2
test_delta 3
test_delta 4
`, `
- interval: 1m
keep_metric_names: true
outputs: [sum_samples_total]
enable_windows: true
`, "1111")
}

View File

@@ -4,39 +4,30 @@ import (
"math"
)
type sumSamplesAggrValueShared struct {
total float64
}
type sumSamplesAggrValue struct {
delta float64
shared *sumSamplesAggrValueShared
sum float64
}
func (av *sumSamplesAggrValue) pushSample(_ aggrConfig, sample *pushSample, _ string, _ int64) {
av.delta += sample.value
if math.Abs(av.sum) >= (1 << 53) {
// It is time to reset the entry, since it starts losing float64 precision
av.sum = 0
}
av.sum += sample.value
}
func (av *sumSamplesAggrValue) flush(c aggrConfig, ctx *flushCtx, key string, _ bool) {
ac := c.(*sumSamplesAggrConfig)
if ac.resetTotalOnFlush {
ctx.appendSeries(key, "sum_samples", av.delta)
av.delta = 0
ctx.appendSeries(key, "sum_samples", av.sum)
av.sum = 0
return
}
total := av.shared.total + av.delta
av.delta = 0
if math.Abs(total) >= (1 << 53) {
// It is time to reset the entry, since it starts losing float64 precision
av.shared.total = 0
} else {
av.shared.total = total
}
ctx.appendSeries(key, "sum_samples_total", total)
ctx.appendSeries(key, "sum_samples_total", av.sum)
}
func (av *sumSamplesAggrValue) state() any {
return av.shared
func (*sumSamplesAggrValue) state() any {
return nil
}
func newSumSamplesAggrConfig(resetTotalOnFlush bool) aggrConfig {
@@ -49,14 +40,6 @@ type sumSamplesAggrConfig struct {
resetTotalOnFlush bool
}
func (*sumSamplesAggrConfig) getValue(s any) aggrValue {
var shared *sumSamplesAggrValueShared
if s == nil {
shared = &sumSamplesAggrValueShared{}
} else {
shared = s.(*sumSamplesAggrValueShared)
}
return &sumSamplesAggrValue{
shared: shared,
}
func (*sumSamplesAggrConfig) getValue(_ any) aggrValue {
return &sumSamplesAggrValue{}
}

View File

@@ -206,36 +206,17 @@ func tryParseScientificNumberForUnixTimestamp(s string, decimalExp int64) (int64
return multiplyByDecimalExp(n, decimalExp)
}
if decimalExp < 0 {
// Negative exponents on a fractional mantissa are intentionally not
// supported. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11268
return 0, false
}
intStr := s[:dotIdx]
fracStr := s[dotIdx+1:]
if decimalExp < int64(len(fracStr)) {
return 0, false
}
n, ok := tryParseFractionalNumberForUnixTimestamp(intStr, fracStr)
if !ok {
return 0, false
}
if decimalExp >= int64(len(fracStr)) {
// The exponent shifts the decimal point past every fractional digit,
// so the value is an integer number of seconds (or coarser).
decimalExp -= int64(len(fracStr))
return multiplyByDecimalExp(n, decimalExp)
}
// The exponent leaves fractional digits, e.g. 1.784144612388E9 == 1784144612.388
// Pad n as plain fractional timestamps do.
fracDigits := int64(len(fracStr)) - decimalExp
for fracDigits%3 != 0 {
if n >= 0 && n > math.MaxInt64/10 || n < 0 && n < math.MinInt64/10 {
return 0, false
}
n *= 10
fracDigits++
}
return n, true
decimalExp -= int64(len(fracStr))
return multiplyByDecimalExp(n, decimalExp)
}
func tryParseFractionalNumberForUnixTimestamp(intStr, fracStr string) (int64, bool) {

View File

@@ -69,17 +69,6 @@ func TestTryParseUnixTimestamp_Success(t *testing.T) {
f("1.23e2", 123000000000)
f("1.2e1", 12000000000)
f("1123.456789123456789E15", 1123456789123456789)
// scientific notation with sub-second precision, i.e. more fractional digits
// than the exponent shifts (https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11268).
// These must match the equivalent plain fractional form.
f("1.784144612388E9", 1784144612388000000) // == 1784144612.388
f("1.784144612388e9", 1784144612388000000)
f("-1.784144612388e9", -1784144612388000000)
f("1.5000000005e9", 1500000000500000000) // == 1500000000.5
f("1.23456789e9", 1234567890000000000) // exponent consumes all frac digits (integer result)
f("1.23e1", 12300000000000) // == 12.3
f("1.234e0", 1234000000000) // == 1.234
}
func TestTryParseUnixTimestamp_Failure(t *testing.T) {
@@ -126,7 +115,9 @@ func TestTryParseUnixTimestamp_Failure(t *testing.T) {
f("1e19")
f("1.3e123456789090123")
// negative decimal exponent
// too small decimal exponent
f("1.23e1")
f("1.234e0")
f("1E-1")
f("1.3e-123456789090123")
}