Repository: VictoriaMetrics/VictoriaMetrics
Pull request #3521 (2022-12-21 20:11:12)

vmui: step
- Save step to URL params only when overriding step
- By default, the step is set from the URL, if it is not there, then it is calculated automatically
- Step resets when changing duration (zoom in/out or  change time range)
- Change step rounding: 
  * if `step > 100` then rounds up to an integer multiple of 10
  * if `step between 10 and 100` then rounds up to an integer multiple of 5
  * if `step between 0.01 and 1`  then rounds up to a float to thousandths multiple of 5
  * if `step < 0.01` then rounds up to a float to thousandths

<br/>

- #3510
- #3513


Patch:
diff --git a/app/vmselect/promql/transform.go b/app/vmselect/promql/transform.go
index 80bc96bef..c10e34808 100644
--- a/app/vmselect/promql/transform.go
+++ b/app/vmselect/promql/transform.go
@@ -1873,12 +1873,12 @@ func labelReplace(tss []*timeseries, srcLabel string, r *regexp.Regexp, dstLabel
 	replacementBytes := []byte(replacement)
 	for _, ts := range tss {
 		mn := &ts.MetricName
-		dstValue := getDstValue(mn, dstLabel)
 		srcValue := mn.GetTagValue(srcLabel)
 		if !r.Match(srcValue) {
 			continue
 		}
 		b := r.ReplaceAll(srcValue, replacementBytes)
+		dstValue := getDstValue(mn, dstLabel)
 		*dstValue = append((*dstValue)[:0], b...)
 		if len(b) == 0 {
 			mn.RemoveTag(dstLabel)
diff --git a/app/vmui/packages/vmui/src/components/Configurators/AdditionalSettings/AdditionalSettings.tsx b/app/vmui/packages/vmui/src/components/Configurators/AdditionalSettings/AdditionalSettings.tsx
index 46959aa03..7dbe65f5b 100644
--- a/app/vmui/packages/vmui/src/components/Configurators/AdditionalSettings/AdditionalSettings.tsx
+++ b/app/vmui/packages/vmui/src/components/Configurators/AdditionalSettings/AdditionalSettings.tsx
@@ -1,6 +1,6 @@
-import React, { FC } from "preact/compat";
+import React, { FC, useEffect } from "preact/compat";
 import StepConfigurator from "../StepConfigurator/StepConfigurator";
-import { useGraphDispatch } from "../../../state/graph/GraphStateContext";
+import { useGraphDispatch, useGraphState } from "../../../state/graph/GraphStateContext";
 import { getAppModeParams } from "../../../utils/app-mode";
 import TenantsConfiguration from "../TenantsConfiguration/TenantsConfiguration";
 import { useCustomPanelDispatch, useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
@@ -8,10 +8,13 @@ import { useTimeState } from "../../../state/time/TimeStateContext";
 import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
 import "./style.scss";
 import Switch from "../../Main/Switch/Switch";
+import usePrevious from "../../../hooks/usePrevious";
 
 const AdditionalSettings: FC = () => {
 
+  const { customStep } = useGraphState();
   const graphDispatch = useGraphDispatch();
+
   const { inputTenantID } = getAppModeParams();
 
   const { autocomplete } = useQueryState();
@@ -20,7 +23,8 @@ const AdditionalSettings: FC = () => {
   const { nocache, isTracingEnabled } = useCustomPanelState();
   const customPanelDispatch = useCustomPanelDispatch();
 
-  const { period: { step } } = useTimeState();
+  const { period: { step }, duration } = useTimeState();
+  const prevDuration = usePrevious(duration);
 
   const onChangeCache = () => {
     customPanelDispatch({ type: "TOGGLE_NO_CACHE" });
@@ -38,6 +42,15 @@ const AdditionalSettings: FC = () => {
     graphDispatch({ type: "SET_CUSTOM_STEP", payload: value });
   };
 
+  useEffect(() => {
+    if (!customStep && step) onChangeStep(step);
+  }, [step]);
+
+  useEffect(() => {
+    if (duration === prevDuration || !prevDuration) return;
+    if (step) onChangeStep(step);
+  }, [duration, prevDuration]);
+
   return <div className="vm-additional-settings">
     <Switch
       label={"Autocomplete"}
@@ -58,6 +71,7 @@ const AdditionalSettings: FC = () => {
       <StepConfigurator
         defaultStep={step}
         setStep={onChangeStep}
+        value={customStep}
       />
     </div>
     {!!inputTenantID && (
diff --git a/app/vmui/packages/vmui/src/components/Configurators/StepConfigurator/StepConfigurator.tsx b/app/vmui/packages/vmui/src/components/Configurators/StepConfigurator/StepConfigurator.tsx
index 9c32522ae..97f18d30e 100644
--- a/app/vmui/packages/vmui/src/components/Configurators/StepConfigurator/StepConfigurator.tsx
+++ b/app/vmui/packages/vmui/src/components/Configurators/StepConfigurator/StepConfigurator.tsx
@@ -1,5 +1,4 @@
-import React, { FC, useCallback, useState } from "preact/compat";
-import { useEffect } from "react";
+import React, { FC, useCallback, useEffect, useState } from "preact/compat";
 import debounce from "lodash.debounce";
 import { RestartIcon } from "../../Main/Icons";
 import TextField from "../../Main/TextField/TextField";
@@ -8,16 +7,17 @@ import Tooltip from "../../Main/Tooltip/Tooltip";
 
 interface StepConfiguratorProps {
   defaultStep?: number,
+  value?: number,
   setStep: (step: number) => void,
 }
 
-const StepConfigurator: FC<StepConfiguratorProps> = ({ defaultStep, setStep }) => {
+const StepConfigurator: FC<StepConfiguratorProps> = ({ value, defaultStep, setStep }) => {
 
-  const [customStep, setCustomStep] = useState(defaultStep);
+  const [customStep, setCustomStep] = useState(value || defaultStep);
   const [error, setError] = useState("");
 
   const handleApply = (step: number) => setStep(step || 1);
-  const debouncedHandleApply = useCallback(debounce(handleApply, 700), []);
+  const debouncedHandleApply = useCallback(debounce(handleApply, 500), []);
 
   const onChangeStep = (val: string) => {
     const value = +val;
@@ -40,12 +40,12 @@ const StepConfigurator: FC<StepConfiguratorProps> = ({ defaultStep, setStep }) =
   };
 
   useEffect(() => {
-    if (defaultStep) handleSetStep(defaultStep);
-  }, [defaultStep]);
+    if (value) handleSetStep(value);
+  }, [value]);
 
   return (
     <TextField
-      label="Step value"
+      label="Step value of seconds"
       type="number"
       value={customStep}
       error={error}
diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/hooks/useSetQueryParams.ts b/app/vmui/packages/vmui/src/pages/CustomPanel/hooks/useSetQueryParams.ts
index 8f85222b4..8c51aad28 100644
--- a/app/vmui/packages/vmui/src/pages/CustomPanel/hooks/useSetQueryParams.ts
+++ b/app/vmui/packages/vmui/src/pages/CustomPanel/hooks/useSetQueryParams.ts
@@ -6,12 +6,14 @@ import { useQueryState } from "../../../state/query/QueryStateContext";
 import { displayTypeTabs } from "../DisplayTypeSwitch";
 import { setQueryStringWithoutPageReload } from "../../../utils/query-string";
 import { compactObject } from "../../../utils/object";
+import { useGraphState } from "../../../state/graph/GraphStateContext";
 
 export const useSetQueryParams = () => {
   const { tenantId } = useAppState();
   const { displayType } = useCustomPanelState();
   const { query } = useQueryState();
   const { duration, relativeTime, period: { date, step } } = useTimeState();
+  const { customStep } = useGraphState();
 
   const setSearchParamsFromState = () => {
     const params: Record<string, unknown> = {};
@@ -20,15 +22,16 @@ export const useSetQueryParams = () => {
       params[`${group}.expr`] = q;
       params[`${group}.range_input`] = duration;
       params[`${group}.end_input`] = date;
-      params[`${group}.step_input`] = step;
       params[`${group}.tab`] = displayTypeTabs.find(t => t.value === displayType)?.prometheusCode || 0;
       params[`${group}.relative_time`] = relativeTime;
       params[`${group}.tenantID`] = tenantId;
+
+      if ((step !== customStep) && customStep) params[`${group}.step_input`] = customStep;
     });
 
     setQueryStringWithoutPageReload(compactObject(params));
   };
 
-  useEffect(setSearchParamsFromState, [tenantId, displayType, query, duration, relativeTime, date, step]);
+  useEffect(setSearchParamsFromState, [tenantId, displayType, query, duration, relativeTime, date, step, customStep]);
   useEffect(setSearchParamsFromState, []);
 };
diff --git a/app/vmui/packages/vmui/src/utils/time.ts b/app/vmui/packages/vmui/src/utils/time.ts
index b6c13713a..c6d40ab3c 100644
--- a/app/vmui/packages/vmui/src/utils/time.ts
+++ b/app/vmui/packages/vmui/src/utils/time.ts
@@ -26,6 +26,23 @@ const shortDurations = supportedDurations.map(d => d.short);
 
 export const roundToMilliseconds = (num: number): number => Math.round(num*1000)/1000;
 
+const roundStep = (step: number) => {
+  const integerStep = Math.round(step);
+  if (step >= 100) {
+    return integerStep - (integerStep%10); // integer multiple of 10
+  }
+  if (step < 100 && step >= 10) {
+    return integerStep - (integerStep%5); // integer multiple of 5
+  }
+  if (step < 10 && step >= 1) {
+    return integerStep; // integer
+  }
+  if (step < 1 && step > 0.01) {
+    return Math.round(step * 40) / 40; // float to thousandths multiple of 5
+  }
+  return roundToMilliseconds(step);
+};
+
 export const isSupportedDuration = (str: string): Partial<Record<UnitTypeShort, string>> | undefined => {
 
   const digits = str.match(/\d+/g);
@@ -57,7 +74,8 @@ export const getTimeperiodForDuration = (dur: string, date?: Date): TimeParams =
   }, {});
 
   const delta = dayjs.duration(durObject).asSeconds();
-  const step = roundToMilliseconds(delta / MAX_ITEMS_PER_CHART) || 0.001;
+  const rawStep = delta / MAX_ITEMS_PER_CHART;
+  const step = roundStep(rawStep) || 0.001;
 
   return {
     start: n - delta,
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 49d7c7ab2..1ee010201 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -15,6 +15,8 @@ The following tip changes can be tested by building VictoriaMetrics components f
 
 ## tip
 
+* FEATURE: [relabeling](https://docs.victoriametrics.com/vmagent.html#relabeling): add support for `keepequal` and `dropequal` relabeling actions, which are supported by Prometheus starting from [v2.41.0](https://github.com/prometheus/prometheus/releases/tag/v2.41.0). These relabeling actions are almost identical to `keep_if_equal` and `drop_if_equal` relabeling actions supported by VictoriaMetrics since `v1.38.0` - see [these docs](https://docs.victoriametrics.com/vmagent.html#relabeling-enhancements) - so it is recommended sticking to `keep_if_equal` and `drop_if_equal` actions instead of switching to `keepequal` and `dropequal`.
+
 
 ## [v1.85.3](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.85.3)
 
diff --git a/lib/bytesutil/fast_string_matcher.go b/lib/bytesutil/fast_string_matcher.go
index d2933563b..619bb325d 100644
--- a/lib/bytesutil/fast_string_matcher.go
+++ b/lib/bytesutil/fast_string_matcher.go
@@ -62,10 +62,8 @@ func (fsm *FastStringMatcher) Match(s string) bool {
 	s = strings.Clone(s)
 	fsm.m.Store(s, e)
 
-	if atomic.LoadUint64(&fsm.lastCleanupTime)+61 < ct {
-		// Perform a global cleanup for fsm.m by removing items, which weren't accessed
-		// during the last 5 minutes.
-		atomic.StoreUint64(&fsm.lastCleanupTime, ct)
+	if needCleanup(&fsm.lastCleanupTime, ct) {
+		// Perform a global cleanup for fsm.m by removing items, which weren't accessed during the last 5 minutes.
 		m := &fsm.m
 		m.Range(func(k, v interface{}) bool {
 			e := v.(*fsmEntry)
@@ -78,3 +76,14 @@ func (fsm *FastStringMatcher) Match(s string) bool {
 
 	return b
 }
+
+func needCleanup(lastCleanupTime *uint64, currentTime uint64) bool {
+	lct := atomic.LoadUint64(lastCleanupTime)
+	if lct+61 >= currentTime {
+		return false
+	}
+	// Atomically compare and swap the current time with the lastCleanupTime
+	// in order to guarantee that only a single goroutine out of multiple
+	// concurrently executing goroutines gets true from the call.
+	return atomic.CompareAndSwapUint64(lastCleanupTime, lct, currentTime)
+}
diff --git a/lib/bytesutil/fast_string_transformer.go b/lib/bytesutil/fast_string_transformer.go
index c131d15e8..670f2c3f8 100644
--- a/lib/bytesutil/fast_string_transformer.go
+++ b/lib/bytesutil/fast_string_transformer.go
@@ -67,10 +67,8 @@ func (fst *FastStringTransformer) Transform(s string) string {
 	}
 	fst.m.Store(s, e)
 
-	if atomic.LoadUint64(&fst.lastCleanupTime)+61 < ct {
-		// Perform a global cleanup for fst.m by removing items, which weren't accessed
-		// during the last 5 minutes.
-		atomic.StoreUint64(&fst.lastCleanupTime, ct)
+	if needCleanup(&fst.lastCleanupTime, ct) {
+		// Perform a global cleanup for fst.m by removing items, which weren't accessed during the last 5 minutes.
 		m := &fst.m
 		m.Range(func(k, v interface{}) bool {
 			e := v.(*fstEntry)
diff --git a/lib/bytesutil/internstring.go b/lib/bytesutil/internstring.go
index 45c9a6328..138b00795 100644
--- a/lib/bytesutil/internstring.go
+++ b/lib/bytesutil/internstring.go
@@ -30,10 +30,8 @@ func InternString(s string) string {
 	}
 	internStringsMap.Store(sCopy, e)
 
-	if atomic.LoadUint64(&internStringsMapLastCleanupTime)+61 < ct {
-		// Perform a global cleanup for internStringsMap by removing items, which weren't accessed
-		// during the last 5 minutes.
-		atomic.StoreUint64(&internStringsMapLastCleanupTime, ct)
+	if needCleanup(&internStringsMapLastCleanupTime, ct) {
+		// Perform a global cleanup for internStringsMap by removing items, which weren't accessed during the last 5 minutes.
 		m := &internStringsMap
 		m.Range(func(k, v interface{}) bool {
 			e := v.(*ismEntry)
diff --git a/lib/promrelabel/config.go b/lib/promrelabel/config.go
index fd98dcdb1..58cd4095f 100644
--- a/lib/promrelabel/config.go
+++ b/lib/promrelabel/config.go
@@ -278,7 +278,7 @@ func parseRelabelConfig(rc *RelabelConfig) (*parsedRelabelConfig, error) {
 			return nil, fmt.Errorf("`replacement` cannot be used with `action=graphite`; see https://docs.victoriametrics.com/vmagent.html#graphite-relabeling")
 		}
 		if rc.Regex != nil {
-			return nil, fmt.Errorf("`regex` cannot be used with `action=graphite`; see https://docs.victoriametrics.com/vmagent.html#graphite-relabeling")
+			return nil, fmt.Errorf("`regex` cannot be used for `action=graphite`; see https://docs.victoriametrics.com/vmagent.html#graphite-relabeling")
 		}
 	case "replace":
 		if targetLabel == "" {
@@ -295,10 +295,36 @@ func parseRelabelConfig(rc *RelabelConfig) (*parsedRelabelConfig, error) {
 		if len(sourceLabels) < 2 {
 			return nil, fmt.Errorf("`source_labels` must contain at least two entries for `action=keep_if_equal`; got %q", sourceLabels)
 		}
+		if targetLabel != "" {
+			return nil, fmt.Errorf("`target_label` cannot be used for `action=keep_if_equal`")
+		}
+		if rc.Regex != nil {
+			return nil, fmt.Errorf("`regex` cannot be used for `action=keep_if_equal`")
+		}
 	case "drop_if_equal":
 		if len(sourceLabels) < 2 {
 			return nil, fmt.Errorf("`source_labels` must contain at least two entries for `action=drop_if_equal`; got %q", sourceLabels)
 		}
+		if targetLabel != "" {
+			return nil, fmt.Errorf("`target_label` cannot be used for `action=drop_if_equal`")
+		}
+		if rc.Regex != nil {
+			return nil, fmt.Errorf("`regex` cannot be used for `action=drop_if_equal`")
+		}
+	case "keepequal":
+		if targetLabel == "" {
+			return nil, fmt.Errorf("missing `target_label` for `action=keepequal`")
+		}
+		if rc.Regex != nil {
+			return nil, fmt.Errorf("`regex` cannot be used for `action=keepequal`")
+		}
+	case "dropequal":
+		if targetLabel == "" {
+			return nil, fmt.Errorf("missing `target_label` for `action=dropequal`")
+		}
+		if rc.Regex != nil {
+			return nil, fmt.Errorf("`regex` cannot be used for `action=dropequal`")
+		}
 	case "keep":
 		if len(sourceLabels) == 0 && rc.If == nil {
 			return nil, fmt.Errorf("missing `source_labels` for `action=keep`")
diff --git a/lib/promrelabel/relabel.go b/lib/promrelabel/relabel.go
index 125ddd08b..c1021753f 100644
--- a/lib/promrelabel/relabel.go
+++ b/lib/promrelabel/relabel.go
@@ -276,6 +276,28 @@ func (prc *parsedRelabelConfig) apply(labels []prompbmarshal.Label, labelsOffset
 			return labels[:labelsOffset]
 		}
 		return labels
+	case "keepequal":
+		// Keep the entry if `source_labels` joined with `separator` matches `target_label`
+		bb := relabelBufPool.Get()
+		bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator)
+		targetValue := getLabelValue(labels[labelsOffset:], prc.TargetLabel)
+		keep := string(bb.B) == targetValue
+		relabelBufPool.Put(bb)
+		if keep {
+			return labels
+		}
+		return labels[:labelsOffset]
+	case "dropequal":
+		// Drop the entry if `source_labels` joined with `separator` doesn't match `target_label`
+		bb := relabelBufPool.Get()
+		bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator)
+		targetValue := getLabelValue(labels[labelsOffset:], prc.TargetLabel)
+		drop := string(bb.B) == targetValue
+		relabelBufPool.Put(bb)
+		if !drop {
+			return labels
+		}
+		return labels[:labelsOffset]
 	case "keep":
 		// Keep the target if `source_labels` joined with `separator` match the `regex`.
 		if prc.RegexAnchored == defaultRegexForRelabelConfig {
diff --git a/lib/promscrape/discovery/azure/azure.go b/lib/promscrape/discovery/azure/azure.go
index d8f77e8c6..351b9c6fd 100644
--- a/lib/promscrape/discovery/azure/azure.go
+++ b/lib/promscrape/discovery/azure/azure.go
@@ -43,7 +43,7 @@ type SDConfig struct {
 	ProxyClientConfig promauth.ProxyClientConfig `yaml:",inline"`
 }
 
-// GetLabels returns Consul labels according to sdc.
+// GetLabels returns Azure labels according to sdc.
 func (sdc *SDConfig) GetLabels(baseDir string) ([]*promutils.Labels, error) {
 	ac, err := getAPIConfig(sdc, baseDir)
 	if err != nil {


Test patch:
diff --git a/app/vmselect/promql/exec_test.go b/app/vmselect/promql/exec_test.go
index cfa204225..5d9f11fce 100644
--- a/app/vmselect/promql/exec_test.go
+++ b/app/vmselect/promql/exec_test.go
@@ -2149,6 +2149,34 @@ func TestExecSuccess(t *testing.T) {
 		resultExpected := []netstorage.Result{r}
 		f(q, resultExpected)
 	})
+	t.Run(`label_replace(nonexisting_src_match)`, func(t *testing.T) {
+		t.Parallel()
+		q := `label_replace(time(), "foo", "x", "bar", "")`
+		r := netstorage.Result{
+			MetricName: metricNameExpected,
+			Values:     []float64{1000, 1200, 1400, 1600, 1800, 2000},
+			Timestamps: timestampsExpected,
+		}
+		r.MetricName.Tags = []storage.Tag{
+			{
+				Key:   []byte("foo"),
+				Value: []byte("x"),
+			},
+		}
+		resultExpected := []netstorage.Result{r}
+		f(q, resultExpected)
+	})
+	t.Run(`label_replace(nonexisting_src_mismatch)`, func(t *testing.T) {
+		t.Parallel()
+		q := `label_replace(time(), "foo", "x", "bar", "y")`
+		r := netstorage.Result{
+			MetricName: metricNameExpected,
+			Values:     []float64{1000, 1200, 1400, 1600, 1800, 2000},
+			Timestamps: timestampsExpected,
+		}
+		resultExpected := []netstorage.Result{r}
+		f(q, resultExpected)
+	})
 	t.Run(`label_replace(mismatch)`, func(t *testing.T) {
 		t.Parallel()
 		q := `label_replace(label_set(time(), "foo", "foobar"), "__name__", "x${1}y", "foo", "bar(.+)")`
diff --git a/lib/bytesutil/fast_string_matcher_test.go b/lib/bytesutil/fast_string_matcher_test.go
index 7ea7925ce..f9c7e29f0 100644
--- a/lib/bytesutil/fast_string_matcher_test.go
+++ b/lib/bytesutil/fast_string_matcher_test.go
@@ -23,3 +23,27 @@ func TestFastStringMatcher(t *testing.T) {
 	f("a_b-C", false)
 	f("foobar", true)
 }
+
+func TestNeedCleanup(t *testing.T) {
+	f := func(lastCleanupTime, currentTime uint64, resultExpected bool) {
+		t.Helper()
+		lct := lastCleanupTime
+		result := needCleanup(&lct, currentTime)
+		if result != resultExpected {
+			t.Fatalf("unexpected result for needCleanup(%d, %d); got %v; want %v", lastCleanupTime, currentTime, result, resultExpected)
+		}
+		if result {
+			if lct != currentTime {
+				t.Fatalf("unexpected value for lct; got %d; want currentTime=%d", lct, currentTime)
+			}
+		} else {
+			if lct != lastCleanupTime {
+				t.Fatalf("unexpected value for lct; got %d; want lastCleanupTime=%d", lct, lastCleanupTime)
+			}
+		}
+	}
+	f(0, 0, false)
+	f(0, 61, false)
+	f(0, 62, true)
+	f(10, 100, true)
+}
diff --git a/lib/promrelabel/config_test.go b/lib/promrelabel/config_test.go
index c1d945156..41f432739 100644
--- a/lib/promrelabel/config_test.go
+++ b/lib/promrelabel/config_test.go
@@ -84,7 +84,7 @@ func TestLoadRelabelConfigsSuccess(t *testing.T) {
 	if err != nil {
 		t.Fatalf("cannot load relabel configs from %q: %s", path, err)
 	}
-	nExpected := 16
+	nExpected := 18
 	if n := pcs.Len(); n != nExpected {
 		t.Fatalf("unexpected number of relabel configs loaded from %q; got %d; want %d", path, n, nExpected)
 	}
@@ -266,6 +266,26 @@ func TestParseRelabelConfigsFailure(t *testing.T) {
 			},
 		})
 	})
+	t.Run("keep_if_equal-unused-target-label", func(t *testing.T) {
+		f([]RelabelConfig{
+			{
+				Action:       "keep_if_equal",
+				SourceLabels: []string{"foo", "bar"},
+				TargetLabel:  "foo",
+			},
+		})
+	})
+	t.Run("keep_if_equal-unused-regex", func(t *testing.T) {
+		f([]RelabelConfig{
+			{
+				Action:       "keep_if_equal",
+				SourceLabels: []string{"foo", "bar"},
+				Regex: &MultiLineRegex{
+					S: "bar",
+				},
+			},
+		})
+	})
 	t.Run("drop_if_equal-missing-source-labels", func(t *testing.T) {
 		f([]RelabelConfig{
 			{
@@ -281,6 +301,80 @@ func TestParseRelabelConfigsFailure(t *testing.T) {
 			},
 		})
 	})
+	t.Run("drop_if_equal-unused-target-label", func(t *testing.T) {
+		f([]RelabelConfig{
+			{
+				Action:       "drop_if_equal",
+				SourceLabels: []string{"foo", "bar"},
+				TargetLabel:  "foo",
+			},
+		})
+	})
+	t.Run("drop_if_equal-unused-regex", func(t *testing.T) {
+		f([]RelabelConfig{
+			{
+				Action:       "drop_if_equal",
+				SourceLabels: []string{"foo", "bar"},
+				Regex: &MultiLineRegex{
+					S: "bar",
+				},
+			},
+		})
+	})
+	t.Run("keepequal-missing-source-labels", func(t *testing.T) {
+		f([]RelabelConfig{
+			{
+				Action: "keepequal",
+			},
+		})
+	})
+	t.Run("keepequal-missing-target-label", func(t *testing.T) {
+		f([]RelabelConfig{
+			{
+				Action:       "keepequal",
+				SourceLabels: []string{"foo"},
+			},
+		})
+	})
+	t.Run("keepequal-unused-regex", func(t *testing.T) {
+		f([]RelabelConfig{
+			{
+				Action:       "keepequal",
+				SourceLabels: []string{"foo"},
+				TargetLabel:  "foo",
+				Regex: &MultiLineRegex{
+					S: "bar",
+				},
+			},
+		})
+	})
+	t.Run("dropequal-missing-source-labels", func(t *testing.T) {
+		f([]RelabelConfig{
+			{
+				Action: "dropequal",
+			},
+		})
+	})
+	t.Run("dropequal-missing-target-label", func(t *testing.T) {
+		f([]RelabelConfig{
+			{
+				Action:       "dropequal",
+				SourceLabels: []string{"foo"},
+			},
+		})
+	})
+	t.Run("dropequal-unused-regex", func(t *testing.T) {
+		f([]RelabelConfig{
+			{
+				Action:       "dropequal",
+				SourceLabels: []string{"foo"},
+				TargetLabel:  "foo",
+				Regex: &MultiLineRegex{
+					S: "bar",
+				},
+			},
+		})
+	})
 	t.Run("drop-missing-source-labels", func(t *testing.T) {
 		f([]RelabelConfig{
 			{
diff --git a/lib/promrelabel/relabel_test.go b/lib/promrelabel/relabel_test.go
index 9c34a7ebf..35e60ff41 100644
--- a/lib/promrelabel/relabel_test.go
+++ b/lib/promrelabel/relabel_test.go
@@ -399,6 +399,34 @@ func TestParsedRelabelConfigsApply(t *testing.T) {
 - action: drop_if_equal
   source_labels: [xxx, bar]
 `, `{xxx="yyy",bar="yyy"}`, true, `{}`)
+	})
+	t.Run("keepequal-hit", func(t *testing.T) {
+		f(`
+- action: keepequal
+  source_labels: [foo]
+  target_label: bar
+`, `{foo="a",bar="a"}`, true, `{bar="a",foo="a"}`)
+	})
+	t.Run("keepequal-miss", func(t *testing.T) {
+		f(`
+- action: keepequal
+  source_labels: [foo]
+  target_label: bar
+`, `{foo="a",bar="x"}`, true, `{}`)
+	})
+	t.Run("dropequal-hit", func(t *testing.T) {
+		f(`
+- action: dropequal
+  source_labels: [foo]
+  target_label: bar
+`, `{foo="a",bar="a"}`, true, `{}`)
+	})
+	t.Run("dropequal-miss", func(t *testing.T) {
+		f(`
+- action: dropequal
+  source_labels: [foo]
+  target_label: bar
+`, `{foo="a",bar="x"}`, true, `{bar="x",foo="a"}`)
 	})
 	t.Run("keep-miss", func(t *testing.T) {
 		f(`
diff --git a/lib/promrelabel/testdata/relabel_configs_valid.yml b/lib/promrelabel/testdata/relabel_configs_valid.yml
index 5264d7953..eccd0c782 100644
--- a/lib/promrelabel/testdata/relabel_configs_valid.yml
+++ b/lib/promrelabel/testdata/relabel_configs_valid.yml
@@ -22,6 +22,12 @@
   source_labels: [foo, bar]
 - action: drop_if_equal
   source_labels: [foo, bar]
+- action: keepequal
+  source_labels: [foo]
+  target_label: bar
+- action: dropequal
+  source_labels: [foo]
+  target_label: bar
 - action: keep
   source_labels: [__name__]
   regex: