You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by sd...@apache.org on 2022/10/06 08:32:03 UTC

[ignite-3] branch main updated: IGNITE-17355 CLI management of metrics (#1081)

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

sdanilov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new d8ead0ab84 IGNITE-17355 CLI management of metrics (#1081)
d8ead0ab84 is described below

commit d8ead0ab84c0daa68e666495314e4c1df3d0c809
Author: Vadim Pakhnushev <86...@users.noreply.github.com>
AuthorDate: Thu Oct 6 11:31:57 2022 +0300

    IGNITE-17355 CLI management of metrics (#1081)
---
 .../cli/call/metric/ItMetricCallsTest.java         |  91 ++++++++++++++++++
 .../ItClusterConfigCommandNotInitializedTest.java  |   2 +-
 .../commands/metric/ItNodeMetricCommandTest.java   |  70 ++++++++++++++
 .../internal/rest/ItGeneratedRestClientTest.java   |  26 ++++-
 .../call/cluster/topology/LogicalTopologyCall.java |   5 +-
 .../cluster/topology/PhysicalTopologyCall.java     |   5 +-
 .../call/cluster/topology/TopologyCallOutput.java  |  70 --------------
 .../metric/NodeMetricEnableCall.java}              |  34 +++----
 .../node/metric/NodeMetricEnableCallInput.java     | 105 +++++++++++++++++++++
 .../metric/NodeMetricListCall.java}                |  28 +++---
 .../cli/commands/metric/MetricSourceMixin.java     |  44 +++++++++
 .../internal/cli/commands/node/NodeCommand.java    |   7 +-
 .../cli/commands/node/NodeReplCommand.java         |   7 +-
 .../commands/node/metric/NodeMetricCommand.java}   |  21 ++---
 .../node/metric/NodeMetricDisableCommand.java      |  53 +++++++++++
 .../node/metric/NodeMetricDisableReplCommand.java  |  54 +++++++++++
 .../node/metric/NodeMetricEnableCommand.java       |  53 +++++++++++
 .../node/metric/NodeMetricEnableReplCommand.java   |  54 +++++++++++
 .../node/metric/NodeMetricListCommand.java         |  52 ++++++++++
 .../node/metric/NodeMetricListReplCommand.java     |  53 +++++++++++
 .../node/metric/NodeMetricReplCommand.java}        |  21 ++---
 .../cli/decorators/MetricListDecorator.java        |  45 +++++++++
 .../cli/commands/UrlOptionsNegativeTest.java       |  12 +++
 .../cli/deprecated/IgniteCliInterfaceTest.java     |  61 ++++++++++++
 modules/metrics/build.gradle                       |   5 +
 modules/metrics/pom.xml                            |  10 ++
 .../ignite/internal/metrics/MetricManager.java     |  10 ++
 .../ignite/internal/metrics/MetricRegistry.java    |  18 +++-
 .../internal/metrics/rest/MetricRestFactory.java}  |  34 ++++---
 .../metrics/rest/NodeMetricController.java         |  61 ++++++++++++
 .../rest/exception/MetricNotFoundException.java}   |  22 ++---
 .../handler/MetricNotFoundExceptionHandler.java    |  45 +++++++++
 .../internal/rest/api/metric/MetricSourceDto.java  |  69 ++++++++++++++
 .../internal/rest/api/metric/NodeMetricApi.java    |  72 ++++++++++++++
 modules/rest/openapi/openapi.yaml                  |  85 +++++++++++++++++
 .../apache/ignite/internal/rest/RestComponent.java |   2 +
 .../org/apache/ignite/internal/app/IgniteImpl.java |  25 +++--
 37 files changed, 1252 insertions(+), 179 deletions(-)

diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/call/metric/ItMetricCallsTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/call/metric/ItMetricCallsTest.java
new file mode 100644
index 0000000000..1037927cff
--- /dev/null
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/call/metric/ItMetricCallsTest.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.cli.call.metric;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import jakarta.inject.Inject;
+import java.util.List;
+import org.apache.ignite.internal.cli.call.CallInitializedIntegrationTestBase;
+import org.apache.ignite.internal.cli.call.node.metric.NodeMetricEnableCall;
+import org.apache.ignite.internal.cli.call.node.metric.NodeMetricEnableCallInput;
+import org.apache.ignite.internal.cli.call.node.metric.NodeMetricListCall;
+import org.apache.ignite.internal.cli.core.call.CallOutput;
+import org.apache.ignite.internal.cli.core.call.StringCallInput;
+import org.apache.ignite.rest.client.model.MetricSource;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link NodeMetricListCall} and {@link NodeMetricEnableCall}. */
+class ItMetricCallsTest extends CallInitializedIntegrationTestBase {
+
+    @Inject
+    NodeMetricListCall nodeMetricListCall;
+
+    @Inject
+    NodeMetricEnableCall nodeMetricEnableCall;
+
+    @Test
+    @DisplayName("Should display empty node metric list when cluster is up and running")
+    void nodeMetricList() {
+        // Given
+        var input = new StringCallInput(NODE_URL);
+
+        // When
+        CallOutput<List<MetricSource>> output = nodeMetricListCall.execute(input);
+
+        // Then
+        assertThat(output.hasError()).isFalse();
+        // And
+        assertThat(output.body()).isEmpty();
+    }
+
+    @Test
+    @DisplayName("Should display error message when enabling nonexistent metric source and is cluster up and running")
+    void nodeMetricEnable() {
+        // Given
+        var input = NodeMetricEnableCallInput.builder()
+                .endpointUrl(NODE_URL)
+                .srcName("no.such.metric")
+                .enable(true)
+                .build();
+
+        // When
+        CallOutput<String> output = nodeMetricEnableCall.execute(input);
+
+        // Then
+        assertThat(output.hasError()).isTrue();
+    }
+
+    @Test
+    @DisplayName("Should display error message when disabling nonexistent metric source and is cluster up and running")
+    void nodeMetricDisable() {
+        // Given
+        var input = NodeMetricEnableCallInput.builder()
+                .endpointUrl(NODE_URL)
+                .srcName("no.such.metric")
+                .enable(false)
+                .build();
+
+        // When
+        CallOutput<String> output = nodeMetricEnableCall.execute(input);
+
+        // Then
+        assertThat(output.hasError()).isTrue();
+    }
+}
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/cluster/config/ItClusterConfigCommandNotInitializedTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/cluster/config/ItClusterConfigCommandNotInitializedTest.java
index 5e0e44cd7e..132666134d 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/cluster/config/ItClusterConfigCommandNotInitializedTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/cluster/config/ItClusterConfigCommandNotInitializedTest.java
@@ -24,7 +24,7 @@ import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
 /**
- * Tests for {@link ClusterConfigSubCommand} for the cluster that is not initialized.
+ * Tests for {@link ClusterConfigCommand} for the cluster that is not initialized.
  */
 class ItClusterConfigCommandNotInitializedTest extends CliCommandTestNotInitializedIntegrationBase {
     @Test
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/metric/ItNodeMetricCommandTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/metric/ItNodeMetricCommandTest.java
new file mode 100644
index 0000000000..18d6aa8251
--- /dev/null
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/metric/ItNodeMetricCommandTest.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.cli.commands.metric;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import org.apache.ignite.internal.cli.commands.CliCommandTestInitializedIntegrationBase;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+/** Tests for node metric commands. */
+class ItNodeMetricCommandTest extends CliCommandTestInitializedIntegrationBase {
+    @Test
+    @DisplayName("Should display empty node metric list when valid node-url is given")
+    void nodeMetricList() {
+        // When list node metric with valid url
+        execute("node", "metric", "list", "--node-url", NODE_URL);
+
+        // Then
+        assertAll(
+                this::assertExitCodeIsZero,
+                this::assertErrOutputIsEmpty,
+                () -> assertOutputIs("Enabled metric sources:" + System.lineSeparator()
+                        + "Disabled metric sources:" + System.lineSeparator())
+        );
+    }
+
+    @Test
+    @DisplayName("Should display error message when enabling nonexistent metric source and valid node-url is given")
+    void nodeMetricEnableNonexistent() {
+        // When list node metric with valid url
+        execute("node", "metric", "enable", "no.such.metric", "--node-url", NODE_URL);
+
+        // Then
+        assertAll(
+                () -> assertExitCodeIs(1),
+                () -> assertErrOutputContains("Metrics source with given name doesn't exist: no.such.metric"),
+                this::assertOutputIsEmpty
+        );
+    }
+
+    @Test
+    @DisplayName("Should display error message when disabling nonexistent metric source and valid node-url is given")
+    void nodeMetricDisableNonexistent() {
+        // When list node metric with valid url
+        execute("node", "metric", "disable", "no.such.metric", "--node-url", NODE_URL);
+
+        // Then
+        assertAll(
+                () -> assertExitCodeIs(1),
+                () -> assertErrOutputContains("Metrics source with given name doesn't exist: no.such.metric"),
+                this::assertOutputIsEmpty
+        );
+    }
+}
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/rest/ItGeneratedRestClientTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/rest/ItGeneratedRestClientTest.java
index ba4961a06e..72a18a67e4 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/rest/ItGeneratedRestClientTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/rest/ItGeneratedRestClientTest.java
@@ -23,6 +23,7 @@ import static org.apache.ignite.internal.testframework.matchers.CompletableFutur
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.is;
@@ -49,6 +50,7 @@ import org.apache.ignite.rest.client.api.ClusterConfigurationApi;
 import org.apache.ignite.rest.client.api.ClusterManagementApi;
 import org.apache.ignite.rest.client.api.NodeConfigurationApi;
 import org.apache.ignite.rest.client.api.NodeManagementApi;
+import org.apache.ignite.rest.client.api.NodeMetricApi;
 import org.apache.ignite.rest.client.api.TopologyApi;
 import org.apache.ignite.rest.client.invoker.ApiClient;
 import org.apache.ignite.rest.client.invoker.ApiException;
@@ -81,8 +83,6 @@ public class ItGeneratedRestClientTest {
     @WorkDirectory
     private Path workDir;
 
-    private CompletableFuture<Ignite> ignite;
-
     private ClusterConfigurationApi clusterConfigurationApi;
 
     private NodeConfigurationApi nodeConfigurationApi;
@@ -93,6 +93,8 @@ public class ItGeneratedRestClientTest {
 
     private TopologyApi topologyApi;
 
+    private NodeMetricApi nodeMetricApi;
+
     private ObjectMapper objectMapper;
 
     private String firstNodeName;
@@ -135,6 +137,7 @@ public class ItGeneratedRestClientTest {
         clusterManagementApi = new ClusterManagementApi(client);
         nodeManagementApi = new NodeManagementApi(client);
         topologyApi = new TopologyApi(client);
+        nodeMetricApi = new NodeMetricApi(client);
 
         objectMapper = new ObjectMapper();
     }
@@ -322,6 +325,25 @@ public class ItGeneratedRestClientTest {
         assertThat(nodeManagementApi.nodeVersion(), is(notNullValue()));
     }
 
+    @Test
+    void nodeMetricList() throws ApiException {
+        assertThat(nodeMetricApi.listNodeMetrics(), empty());
+    }
+
+    @Test
+    void enableInvalidNodeMetric() throws JsonProcessingException {
+        var thrown = assertThrows(
+                ApiException.class,
+                () -> nodeMetricApi.enableNodeMetric("no.such.metric")
+        );
+
+        assertThat(thrown.getCode(), equalTo(404));
+
+        Problem problem = objectMapper.readValue(thrown.getResponseBody(), Problem.class);
+        assertThat(problem.getStatus(), equalTo(404));
+        assertThat(problem.getDetail(), containsString("Metrics source with given name doesn't exist: no.such.metric"));
+    }
+
     private CompletableFuture<Ignite> startNodeAsync(TestInfo testInfo, int index) {
         String nodeName = testNodeName(testInfo, BASE_PORT + index);
 
diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/topology/LogicalTopologyCall.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/topology/LogicalTopologyCall.java
index 1daefe6b05..2d36557cfe 100644
--- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/topology/LogicalTopologyCall.java
+++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/topology/LogicalTopologyCall.java
@@ -21,6 +21,7 @@ import jakarta.inject.Singleton;
 import java.util.List;
 import org.apache.ignite.internal.cli.core.call.Call;
 import org.apache.ignite.internal.cli.core.call.CallOutput;
+import org.apache.ignite.internal.cli.core.call.DefaultCallOutput;
 import org.apache.ignite.internal.cli.core.call.UrlCallInput;
 import org.apache.ignite.internal.cli.core.exception.IgniteCliApiException;
 import org.apache.ignite.rest.client.api.TopologyApi;
@@ -39,9 +40,9 @@ public class LogicalTopologyCall implements Call<UrlCallInput, List<ClusterNode>
     public CallOutput<List<ClusterNode>> execute(UrlCallInput input) {
         String clusterUrl = input.getUrl();
         try {
-            return TopologyCallOutput.success(fetchLogicalTopology(clusterUrl));
+            return DefaultCallOutput.success(fetchLogicalTopology(clusterUrl));
         } catch (ApiException | IllegalArgumentException e) {
-            return TopologyCallOutput.failure(new IgniteCliApiException(e, clusterUrl));
+            return DefaultCallOutput.failure(new IgniteCliApiException(e, clusterUrl));
         }
     }
 
diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/topology/PhysicalTopologyCall.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/topology/PhysicalTopologyCall.java
index cd6529e55b..a5efa7c555 100644
--- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/topology/PhysicalTopologyCall.java
+++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/topology/PhysicalTopologyCall.java
@@ -21,6 +21,7 @@ import jakarta.inject.Singleton;
 import java.util.List;
 import org.apache.ignite.internal.cli.core.call.Call;
 import org.apache.ignite.internal.cli.core.call.CallOutput;
+import org.apache.ignite.internal.cli.core.call.DefaultCallOutput;
 import org.apache.ignite.internal.cli.core.call.UrlCallInput;
 import org.apache.ignite.internal.cli.core.exception.IgniteCliApiException;
 import org.apache.ignite.rest.client.api.TopologyApi;
@@ -39,9 +40,9 @@ public class PhysicalTopologyCall implements Call<UrlCallInput, List<ClusterNode
     public CallOutput<List<ClusterNode>> execute(UrlCallInput input) {
         String clusterUrl = input.getUrl();
         try {
-            return TopologyCallOutput.success(fetchPhysicalTopology(clusterUrl));
+            return DefaultCallOutput.success(fetchPhysicalTopology(clusterUrl));
         } catch (ApiException | IllegalArgumentException e) {
-            return TopologyCallOutput.failure(new IgniteCliApiException(e, clusterUrl));
+            return DefaultCallOutput.failure(new IgniteCliApiException(e, clusterUrl));
         }
     }
 
diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/topology/TopologyCallOutput.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/topology/TopologyCallOutput.java
deleted file mode 100644
index 40d1b4ff37..0000000000
--- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/topology/TopologyCallOutput.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.internal.cli.call.cluster.topology;
-
-import java.util.List;
-import org.apache.ignite.internal.cli.core.call.CallOutput;
-import org.apache.ignite.internal.cli.core.exception.IgniteCliApiException;
-import org.apache.ignite.rest.client.model.ClusterNode;
-
-/**
- * Output fot the topology calls.
- */
-public class TopologyCallOutput implements CallOutput<List<ClusterNode>> {
-    private final Throwable error;
-
-    private final List<ClusterNode> topology;
-
-    private TopologyCallOutput(List<ClusterNode> topology) {
-        this.topology = topology;
-        error = null;
-    }
-
-    private TopologyCallOutput(Exception error) {
-        this.error = error;
-        this.topology = null;
-    }
-
-    public static TopologyCallOutput success(List<ClusterNode> topology) {
-        return new TopologyCallOutput(topology);
-    }
-
-    public static TopologyCallOutput failure(IgniteCliApiException e) {
-        return new TopologyCallOutput(e);
-    }
-
-    @Override
-    public List<ClusterNode> body() {
-        return topology;
-    }
-
-    @Override
-    public boolean hasError() {
-        return error != null;
-    }
-
-    @Override
-    public boolean isEmpty() {
-        return topology != null && topology.isEmpty();
-    }
-
-    @Override
-    public Throwable errorCause() {
-        return error;
-    }
-}
diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/topology/LogicalTopologyCall.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/node/metric/NodeMetricEnableCall.java
similarity index 54%
copy from modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/topology/LogicalTopologyCall.java
copy to modules/cli/src/main/java/org/apache/ignite/internal/cli/call/node/metric/NodeMetricEnableCall.java
index 1daefe6b05..cd22775217 100644
--- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/topology/LogicalTopologyCall.java
+++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/node/metric/NodeMetricEnableCall.java
@@ -15,37 +15,39 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.cli.call.cluster.topology;
+package org.apache.ignite.internal.cli.call.node.metric;
 
 import jakarta.inject.Singleton;
-import java.util.List;
 import org.apache.ignite.internal.cli.core.call.Call;
 import org.apache.ignite.internal.cli.core.call.CallOutput;
-import org.apache.ignite.internal.cli.core.call.UrlCallInput;
+import org.apache.ignite.internal.cli.core.call.DefaultCallOutput;
 import org.apache.ignite.internal.cli.core.exception.IgniteCliApiException;
-import org.apache.ignite.rest.client.api.TopologyApi;
+import org.apache.ignite.rest.client.api.NodeMetricApi;
 import org.apache.ignite.rest.client.invoker.ApiException;
 import org.apache.ignite.rest.client.invoker.Configuration;
-import org.apache.ignite.rest.client.model.ClusterNode;
 
-/**
- * Shows logical cluster topology.
- */
+/** Enables or disables metric source. */
 @Singleton
-public class LogicalTopologyCall implements Call<UrlCallInput, List<ClusterNode>> {
-
+public class NodeMetricEnableCall implements Call<NodeMetricEnableCallInput, String> {
     /** {@inheritDoc} */
     @Override
-    public CallOutput<List<ClusterNode>> execute(UrlCallInput input) {
-        String clusterUrl = input.getUrl();
+    public CallOutput<String> execute(NodeMetricEnableCallInput input) {
+        NodeMetricApi api = createApiClient(input);
+
         try {
-            return TopologyCallOutput.success(fetchLogicalTopology(clusterUrl));
+            if (input.getEnable()) {
+                api.enableNodeMetric(input.getSrcName());
+            } else {
+                api.disableNodeMetric(input.getSrcName());
+            }
+            String message = input.getEnable() ? "enabled" : "disabled";
+            return DefaultCallOutput.success("Metric source was " + message + " successfully");
         } catch (ApiException | IllegalArgumentException e) {
-            return TopologyCallOutput.failure(new IgniteCliApiException(e, clusterUrl));
+            return DefaultCallOutput.failure(new IgniteCliApiException(e, input.getEndpointUrl()));
         }
     }
 
-    private List<ClusterNode> fetchLogicalTopology(String url) throws ApiException {
-        return new TopologyApi(Configuration.getDefaultApiClient().setBasePath(url)).logical();
+    private static NodeMetricApi createApiClient(NodeMetricEnableCallInput input) {
+        return new NodeMetricApi(Configuration.getDefaultApiClient().setBasePath(input.getEndpointUrl()));
     }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/node/metric/NodeMetricEnableCallInput.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/node/metric/NodeMetricEnableCallInput.java
new file mode 100644
index 0000000000..b31562415d
--- /dev/null
+++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/node/metric/NodeMetricEnableCallInput.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.cli.call.node.metric;
+
+import org.apache.ignite.internal.cli.core.call.CallInput;
+
+/** Input for {@link NodeMetricEnableCall}. */
+public class NodeMetricEnableCallInput implements CallInput {
+    /** Metric source name. */
+    private final String srcName;
+
+    /** Enable or disable metric source. */
+    private final boolean enable;
+
+    /** endpoint URL. */
+    private final String endpointUrl;
+
+    private NodeMetricEnableCallInput(String srcName, boolean enable, String endpointUrl) {
+        this.srcName = srcName;
+        this.enable = enable;
+        this.endpointUrl = endpointUrl;
+    }
+
+    /**
+     * Builder method.
+     *
+     * @return Builder for {@link NodeMetricEnableCallInput}.
+     */
+    public static NodeMetricEnableCallInputBuilder builder() {
+        return new NodeMetricEnableCallInputBuilder();
+    }
+
+    /**
+     * Get configuration.
+     *
+     * @return Configuration to update.
+     */
+    public String getSrcName() {
+        return srcName;
+    }
+
+    /**
+     * Get enable flag.
+     *
+     * @return {@code true} if metric source needs to be enabled, {@code false} if it needs to be disabled.
+     */
+    public boolean getEnable() {
+        return enable;
+    }
+
+    /**
+     * Get endpoint URL.
+     *
+     * @return endpoint URL.
+     */
+    public String getEndpointUrl() {
+        return endpointUrl;
+    }
+
+    /**
+     * Builder for {@link NodeMetricEnableCallInput}.
+     */
+    public static class NodeMetricEnableCallInputBuilder {
+
+        private String srcName;
+
+        private boolean enable;
+
+        private String endpointUrl;
+
+        public NodeMetricEnableCallInputBuilder srcName(String srcName) {
+            this.srcName = srcName;
+            return this;
+        }
+
+        public NodeMetricEnableCallInputBuilder enable(boolean enable) {
+            this.enable = enable;
+            return this;
+        }
+
+        public NodeMetricEnableCallInputBuilder endpointUrl(String endpointUrl) {
+            this.endpointUrl = endpointUrl;
+            return this;
+        }
+
+        public NodeMetricEnableCallInput build() {
+            return new NodeMetricEnableCallInput(srcName, enable, endpointUrl);
+        }
+    }
+}
diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/topology/LogicalTopologyCall.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/node/metric/NodeMetricListCall.java
similarity index 59%
copy from modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/topology/LogicalTopologyCall.java
copy to modules/cli/src/main/java/org/apache/ignite/internal/cli/call/node/metric/NodeMetricListCall.java
index 1daefe6b05..20cba1ce34 100644
--- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/topology/LogicalTopologyCall.java
+++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/node/metric/NodeMetricListCall.java
@@ -15,37 +15,35 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.cli.call.cluster.topology;
+package org.apache.ignite.internal.cli.call.node.metric;
 
 import jakarta.inject.Singleton;
 import java.util.List;
 import org.apache.ignite.internal.cli.core.call.Call;
 import org.apache.ignite.internal.cli.core.call.CallOutput;
-import org.apache.ignite.internal.cli.core.call.UrlCallInput;
+import org.apache.ignite.internal.cli.core.call.DefaultCallOutput;
+import org.apache.ignite.internal.cli.core.call.StringCallInput;
 import org.apache.ignite.internal.cli.core.exception.IgniteCliApiException;
-import org.apache.ignite.rest.client.api.TopologyApi;
+import org.apache.ignite.rest.client.api.NodeMetricApi;
 import org.apache.ignite.rest.client.invoker.ApiException;
 import org.apache.ignite.rest.client.invoker.Configuration;
-import org.apache.ignite.rest.client.model.ClusterNode;
+import org.apache.ignite.rest.client.model.MetricSource;
 
-/**
- * Shows logical cluster topology.
- */
+/** Lists node metric sources. */
 @Singleton
-public class LogicalTopologyCall implements Call<UrlCallInput, List<ClusterNode>> {
-
+public class NodeMetricListCall implements Call<StringCallInput, List<MetricSource>> {
     /** {@inheritDoc} */
     @Override
-    public CallOutput<List<ClusterNode>> execute(UrlCallInput input) {
-        String clusterUrl = input.getUrl();
+    public CallOutput<List<MetricSource>> execute(StringCallInput input) {
+
         try {
-            return TopologyCallOutput.success(fetchLogicalTopology(clusterUrl));
+            return DefaultCallOutput.success(listNodeMetrics(input));
         } catch (ApiException | IllegalArgumentException e) {
-            return TopologyCallOutput.failure(new IgniteCliApiException(e, clusterUrl));
+            return DefaultCallOutput.failure(new IgniteCliApiException(e, input.getString()));
         }
     }
 
-    private List<ClusterNode> fetchLogicalTopology(String url) throws ApiException {
-        return new TopologyApi(Configuration.getDefaultApiClient().setBasePath(url)).logical();
+    private static List<MetricSource> listNodeMetrics(StringCallInput input) throws ApiException {
+        return new NodeMetricApi(Configuration.getDefaultApiClient().setBasePath(input.getString())).listNodeMetrics();
     }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/metric/MetricSourceMixin.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/metric/MetricSourceMixin.java
new file mode 100644
index 0000000000..b4a367a414
--- /dev/null
+++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/metric/MetricSourceMixin.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.cli.commands.metric;
+
+import org.apache.ignite.internal.cli.call.node.metric.NodeMetricEnableCallInput;
+import picocli.CommandLine.Parameters;
+
+/** Mixin class for metric source name, provides source name parameter and constructs call input. */
+public class MetricSourceMixin {
+    /** Name of the metric source name. */
+    @Parameters(index = "0", description = "Metric source name")
+    private String srcName;
+
+    public NodeMetricEnableCallInput buildEnableCallInput(String endpointUrl) {
+        return buildCallInput(endpointUrl, true);
+    }
+
+    public NodeMetricEnableCallInput buildDisableCallInput(String endpointUrl) {
+        return buildCallInput(endpointUrl, false);
+    }
+
+    private NodeMetricEnableCallInput buildCallInput(String endpointUrl, boolean enable) {
+        return NodeMetricEnableCallInput.builder()
+                .endpointUrl(endpointUrl)
+                .srcName(srcName)
+                .enable(enable)
+                .build();
+    }
+}
diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/NodeCommand.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/NodeCommand.java
index abbe1811c6..6aa9719bbf 100644
--- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/NodeCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/NodeCommand.java
@@ -19,16 +19,15 @@ package org.apache.ignite.internal.cli.commands.node;
 
 import org.apache.ignite.cli.commands.node.version.NodeVersionCommand;
 import org.apache.ignite.internal.cli.commands.node.config.NodeConfigCommand;
+import org.apache.ignite.internal.cli.commands.node.metric.NodeMetricCommand;
 import org.apache.ignite.internal.cli.commands.node.status.NodeStatusCommand;
 import org.apache.ignite.internal.cli.deprecated.spec.NodeCommandSpec;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.Mixin;
 
-/**
- * Node command.
- */
+/** Node command. */
 @Command(name = "node",
-        subcommands = {NodeConfigCommand.class, NodeStatusCommand.class, NodeVersionCommand.class},
+        subcommands = {NodeConfigCommand.class, NodeStatusCommand.class, NodeVersionCommand.class, NodeMetricCommand.class},
         description = "Node operations")
 public class NodeCommand {
     @Mixin
diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/NodeReplCommand.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/NodeReplCommand.java
index 70f82aa242..0aa7d751ce 100644
--- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/NodeReplCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/NodeReplCommand.java
@@ -19,16 +19,15 @@ package org.apache.ignite.internal.cli.commands.node;
 
 import org.apache.ignite.cli.commands.node.version.NodeVersionReplCommand;
 import org.apache.ignite.internal.cli.commands.node.config.NodeConfigReplCommand;
+import org.apache.ignite.internal.cli.commands.node.metric.NodeMetricReplCommand;
 import org.apache.ignite.internal.cli.commands.node.status.NodeStatusReplCommand;
 import org.apache.ignite.internal.cli.deprecated.spec.NodeCommandSpec;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.Mixin;
 
-/**
- * Node command in REPL mode.
- */
+/** Node command in REPL mode. */
 @Command(name = "node",
-        subcommands = {NodeConfigReplCommand.class, NodeStatusReplCommand.class, NodeVersionReplCommand.class},
+        subcommands = {NodeConfigReplCommand.class, NodeStatusReplCommand.class, NodeVersionReplCommand.class, NodeMetricReplCommand.class},
         description = "Node operations")
 public class NodeReplCommand {
     @Mixin
diff --git a/modules/metrics/build.gradle b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricCommand.java
similarity index 59%
copy from modules/metrics/build.gradle
copy to modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricCommand.java
index 7f5a8fa822..752dad0028 100644
--- a/modules/metrics/build.gradle
+++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricCommand.java
@@ -15,19 +15,14 @@
  * limitations under the License.
  */
 
-apply from: "$rootDir/buildscripts/java-core.gradle"
-apply from: "$rootDir/buildscripts/java-junit5.gradle"
-apply from: "$rootDir/buildscripts/java-integration-test.gradle"
+package org.apache.ignite.internal.cli.commands.node.metric;
 
-dependencies {
-    implementation project(':ignite-core')
-    implementation project(':ignite-configuration')
-    implementation project(':ignite-configuration-api')
-    implementation libs.jetbrains.annotations
+import org.apache.ignite.internal.cli.commands.BaseCommand;
+import picocli.CommandLine.Command;
 
-    testImplementation libs.hamcrest.core
-    testImplementation libs.mockito.core
-    testImplementation project(':ignite-core')
+/** Node metric command. */
+@Command(name = "metric",
+        subcommands = {NodeMetricEnableCommand.class, NodeMetricDisableCommand.class, NodeMetricListCommand.class},
+        description = "Node metric operations")
+public class NodeMetricCommand extends BaseCommand {
 }
-
-description = 'ignite-metrics'
diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricDisableCommand.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricDisableCommand.java
new file mode 100644
index 0000000000..7b69bad6a6
--- /dev/null
+++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricDisableCommand.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.cli.commands.node.metric;
+
+import jakarta.inject.Inject;
+import java.util.concurrent.Callable;
+import org.apache.ignite.internal.cli.call.node.metric.NodeMetricEnableCall;
+import org.apache.ignite.internal.cli.commands.BaseCommand;
+import org.apache.ignite.internal.cli.commands.metric.MetricSourceMixin;
+import org.apache.ignite.internal.cli.commands.node.NodeUrlProfileMixin;
+import org.apache.ignite.internal.cli.core.call.CallExecutionPipeline;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Mixin;
+
+/** Command that disables node metric source. */
+@Command(name = "disable", description = "Disables node metric source")
+public class NodeMetricDisableCommand extends BaseCommand implements Callable<Integer> {
+    /** Node URL option. */
+    @Mixin
+    private NodeUrlProfileMixin nodeUrl;
+
+    @Mixin
+    private MetricSourceMixin metricSource;
+
+    @Inject
+    private NodeMetricEnableCall call;
+
+    /** {@inheritDoc} */
+    @Override
+    public Integer call() {
+        return CallExecutionPipeline.builder(call)
+                .inputProvider(() -> metricSource.buildDisableCallInput(nodeUrl.getNodeUrl()))
+                .output(spec.commandLine().getOut())
+                .errOutput(spec.commandLine().getErr())
+                .build()
+                .runPipeline();
+    }
+}
diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricDisableReplCommand.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricDisableReplCommand.java
new file mode 100644
index 0000000000..3a4c97735f
--- /dev/null
+++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricDisableReplCommand.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.cli.commands.node.metric;
+
+import jakarta.inject.Inject;
+import org.apache.ignite.internal.cli.call.node.metric.NodeMetricEnableCall;
+import org.apache.ignite.internal.cli.commands.BaseCommand;
+import org.apache.ignite.internal.cli.commands.metric.MetricSourceMixin;
+import org.apache.ignite.internal.cli.commands.node.NodeUrlMixin;
+import org.apache.ignite.internal.cli.commands.questions.ConnectToClusterQuestion;
+import org.apache.ignite.internal.cli.core.flow.builder.Flows;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Mixin;
+
+/** Command that disables node metric source in REPL mode. */
+@Command(name = "disable", description = "Disables node metric source")
+public class NodeMetricDisableReplCommand extends BaseCommand implements Runnable {
+    /** Node URL option. */
+    @Mixin
+    private NodeUrlMixin nodeUrl;
+
+    @Mixin
+    private MetricSourceMixin metricSource;
+
+    @Inject
+    private NodeMetricEnableCall call;
+
+    @Inject
+    private ConnectToClusterQuestion question;
+
+    @Override
+    public void run() {
+        question.askQuestionIfNotConnected(nodeUrl.getNodeUrl())
+                .map(metricSource::buildDisableCallInput)
+                .then(Flows.fromCall(call))
+                .print()
+                .start();
+    }
+}
diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricEnableCommand.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricEnableCommand.java
new file mode 100644
index 0000000000..5ab69a5bed
--- /dev/null
+++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricEnableCommand.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.cli.commands.node.metric;
+
+import jakarta.inject.Inject;
+import java.util.concurrent.Callable;
+import org.apache.ignite.internal.cli.call.node.metric.NodeMetricEnableCall;
+import org.apache.ignite.internal.cli.commands.BaseCommand;
+import org.apache.ignite.internal.cli.commands.metric.MetricSourceMixin;
+import org.apache.ignite.internal.cli.commands.node.NodeUrlProfileMixin;
+import org.apache.ignite.internal.cli.core.call.CallExecutionPipeline;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Mixin;
+
+/** Command that enables node metric source. */
+@Command(name = "enable", description = "Enables node metric source")
+public class NodeMetricEnableCommand extends BaseCommand implements Callable<Integer> {
+    /** Node URL option. */
+    @Mixin
+    private NodeUrlProfileMixin nodeUrl;
+
+    @Mixin
+    private MetricSourceMixin metricSource;
+
+    @Inject
+    private NodeMetricEnableCall call;
+
+    /** {@inheritDoc} */
+    @Override
+    public Integer call() {
+        return CallExecutionPipeline.builder(call)
+                .inputProvider(() -> metricSource.buildEnableCallInput(nodeUrl.getNodeUrl()))
+                .output(spec.commandLine().getOut())
+                .errOutput(spec.commandLine().getErr())
+                .build()
+                .runPipeline();
+    }
+}
diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricEnableReplCommand.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricEnableReplCommand.java
new file mode 100644
index 0000000000..09fd277774
--- /dev/null
+++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricEnableReplCommand.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.cli.commands.node.metric;
+
+import jakarta.inject.Inject;
+import org.apache.ignite.internal.cli.call.node.metric.NodeMetricEnableCall;
+import org.apache.ignite.internal.cli.commands.BaseCommand;
+import org.apache.ignite.internal.cli.commands.metric.MetricSourceMixin;
+import org.apache.ignite.internal.cli.commands.node.NodeUrlMixin;
+import org.apache.ignite.internal.cli.commands.questions.ConnectToClusterQuestion;
+import org.apache.ignite.internal.cli.core.flow.builder.Flows;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Mixin;
+
+/** Command that enables node metric source in REPL mode. */
+@Command(name = "enable", description = "Enables node metric source")
+public class NodeMetricEnableReplCommand extends BaseCommand implements Runnable {
+    /** Node URL option. */
+    @Mixin
+    private NodeUrlMixin nodeUrl;
+
+    @Mixin
+    private MetricSourceMixin metricSource;
+
+    @Inject
+    private NodeMetricEnableCall call;
+
+    @Inject
+    private ConnectToClusterQuestion question;
+
+    @Override
+    public void run() {
+        question.askQuestionIfNotConnected(nodeUrl.getNodeUrl())
+                .map(metricSource::buildEnableCallInput)
+                .then(Flows.fromCall(call))
+                .print()
+                .start();
+    }
+}
diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricListCommand.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricListCommand.java
new file mode 100644
index 0000000000..278c26a781
--- /dev/null
+++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricListCommand.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.cli.commands.node.metric;
+
+import jakarta.inject.Inject;
+import java.util.concurrent.Callable;
+import org.apache.ignite.internal.cli.call.node.metric.NodeMetricListCall;
+import org.apache.ignite.internal.cli.commands.BaseCommand;
+import org.apache.ignite.internal.cli.commands.node.NodeUrlProfileMixin;
+import org.apache.ignite.internal.cli.core.call.CallExecutionPipeline;
+import org.apache.ignite.internal.cli.core.call.StringCallInput;
+import org.apache.ignite.internal.cli.decorators.MetricListDecorator;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Mixin;
+
+/** Command that lists node metric sources. */
+@Command(name = "list", description = "Lists node metric sources")
+public class NodeMetricListCommand extends BaseCommand implements Callable<Integer> {
+    /** Node URL option. */
+    @Mixin
+    private NodeUrlProfileMixin nodeUrl;
+
+    @Inject
+    private NodeMetricListCall call;
+
+    /** {@inheritDoc} */
+    @Override
+    public Integer call() {
+        return CallExecutionPipeline.builder(call)
+                .inputProvider(() -> new StringCallInput(nodeUrl.getNodeUrl()))
+                .output(spec.commandLine().getOut())
+                .errOutput(spec.commandLine().getErr())
+                .decorator(new MetricListDecorator())
+                .build()
+                .runPipeline();
+    }
+}
diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricListReplCommand.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricListReplCommand.java
new file mode 100644
index 0000000000..0ee87407d2
--- /dev/null
+++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricListReplCommand.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.cli.commands.node.metric;
+
+import jakarta.inject.Inject;
+import org.apache.ignite.internal.cli.call.node.metric.NodeMetricListCall;
+import org.apache.ignite.internal.cli.commands.BaseCommand;
+import org.apache.ignite.internal.cli.commands.node.NodeUrlMixin;
+import org.apache.ignite.internal.cli.commands.questions.ConnectToClusterQuestion;
+import org.apache.ignite.internal.cli.core.call.StringCallInput;
+import org.apache.ignite.internal.cli.core.flow.builder.Flows;
+import org.apache.ignite.internal.cli.decorators.MetricListDecorator;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Mixin;
+
+/** Command that lists node metric sources in REPL mode. */
+@Command(name = "list", description = "Lists node metric sources")
+public class NodeMetricListReplCommand extends BaseCommand implements Runnable {
+    /** Node URL option. */
+    @Mixin
+    private NodeUrlMixin nodeUrl;
+
+    @Inject
+    private NodeMetricListCall call;
+
+    @Inject
+    private ConnectToClusterQuestion question;
+
+    /** {@inheritDoc} */
+    @Override
+    public void run() {
+        question.askQuestionIfNotConnected(nodeUrl.getNodeUrl())
+                .map(StringCallInput::new)
+                .then(Flows.fromCall(call))
+                .print(new MetricListDecorator())
+                .start();
+    }
+}
diff --git a/modules/metrics/build.gradle b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricReplCommand.java
similarity index 59%
copy from modules/metrics/build.gradle
copy to modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricReplCommand.java
index 7f5a8fa822..2a2154b7b4 100644
--- a/modules/metrics/build.gradle
+++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/metric/NodeMetricReplCommand.java
@@ -15,19 +15,14 @@
  * limitations under the License.
  */
 
-apply from: "$rootDir/buildscripts/java-core.gradle"
-apply from: "$rootDir/buildscripts/java-junit5.gradle"
-apply from: "$rootDir/buildscripts/java-integration-test.gradle"
+package org.apache.ignite.internal.cli.commands.node.metric;
 
-dependencies {
-    implementation project(':ignite-core')
-    implementation project(':ignite-configuration')
-    implementation project(':ignite-configuration-api')
-    implementation libs.jetbrains.annotations
+import org.apache.ignite.internal.cli.commands.BaseCommand;
+import picocli.CommandLine.Command;
 
-    testImplementation libs.hamcrest.core
-    testImplementation libs.mockito.core
-    testImplementation project(':ignite-core')
+/** Node metric command in REPL. */
+@Command(name = "metric",
+        subcommands = {NodeMetricEnableReplCommand.class, NodeMetricDisableReplCommand.class, NodeMetricListReplCommand.class},
+        description = "Node metric operations")
+public class NodeMetricReplCommand extends BaseCommand {
 }
-
-description = 'ignite-metrics'
diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/MetricListDecorator.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/MetricListDecorator.java
new file mode 100644
index 0000000000..581022f38c
--- /dev/null
+++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/MetricListDecorator.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.cli.decorators;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cli.core.decorator.Decorator;
+import org.apache.ignite.internal.cli.core.decorator.TerminalOutput;
+import org.apache.ignite.rest.client.model.MetricSource;
+
+/** Decorator for printing list of {@link MetricSource}. */
+public class MetricListDecorator implements Decorator<List<MetricSource>, TerminalOutput> {
+    @Override
+    public TerminalOutput decorate(List<MetricSource> data) {
+        return () -> {
+            String enabled = data.stream()
+                    .filter(MetricSource::getEnabled)
+                    .map(MetricSource::getName)
+                    .collect(Collectors.joining(System.lineSeparator()));
+            String disabled = data.stream()
+                    .filter(metricSource -> !metricSource.getEnabled())
+                    .map(MetricSource::getName)
+                    .collect(Collectors.joining(System.lineSeparator()));
+            return Stream.of("Enabled metric sources:", enabled, "Disabled metric sources:", disabled)
+                    .filter(s -> !s.isEmpty())
+                    .collect(Collectors.joining(System.lineSeparator()));
+        };
+    }
+}
diff --git a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/UrlOptionsNegativeTest.java b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/UrlOptionsNegativeTest.java
index 215687b84a..d06340e546 100644
--- a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/UrlOptionsNegativeTest.java
+++ b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/UrlOptionsNegativeTest.java
@@ -44,6 +44,12 @@ import org.apache.ignite.internal.cli.commands.node.config.NodeConfigShowCommand
 import org.apache.ignite.internal.cli.commands.node.config.NodeConfigShowReplCommand;
 import org.apache.ignite.internal.cli.commands.node.config.NodeConfigUpdateCommand;
 import org.apache.ignite.internal.cli.commands.node.config.NodeConfigUpdateReplCommand;
+import org.apache.ignite.internal.cli.commands.node.metric.NodeMetricDisableCommand;
+import org.apache.ignite.internal.cli.commands.node.metric.NodeMetricDisableReplCommand;
+import org.apache.ignite.internal.cli.commands.node.metric.NodeMetricEnableCommand;
+import org.apache.ignite.internal.cli.commands.node.metric.NodeMetricEnableReplCommand;
+import org.apache.ignite.internal.cli.commands.node.metric.NodeMetricListCommand;
+import org.apache.ignite.internal.cli.commands.node.metric.NodeMetricListReplCommand;
 import org.apache.ignite.internal.cli.commands.node.status.NodeStatusCommand;
 import org.apache.ignite.internal.cli.commands.node.status.NodeStatusReplCommand;
 import org.apache.ignite.internal.cli.commands.topology.LogicalTopologyCommand;
@@ -110,6 +116,9 @@ public class UrlOptionsNegativeTest {
                 arguments(ClusterConfigShowCommand.class, CLUSTER_URL_OPTION, List.of()),
                 arguments(ClusterConfigUpdateCommand.class, CLUSTER_URL_OPTION, List.of("{key: value}")),
                 arguments(ClusterStatusCommand.class, CLUSTER_URL_OPTION, List.of()),
+                arguments(NodeMetricEnableCommand.class, NODE_URL_OPTION, List.of("srcName")),
+                arguments(NodeMetricDisableCommand.class, NODE_URL_OPTION, List.of("srcName")),
+                arguments(NodeMetricListCommand.class, NODE_URL_OPTION, List.of()),
                 arguments(LogicalTopologyCommand.class, CLUSTER_URL_OPTION, List.of()),
                 arguments(PhysicalTopologyCommand.class, CLUSTER_URL_OPTION, List.of()),
                 arguments(ClusterInitCommand.class, CLUSTER_URL_OPTION, List.of("--cluster-name=cluster", "--meta-storage-node=test"))
@@ -126,6 +135,9 @@ public class UrlOptionsNegativeTest {
                 arguments(ClusterConfigShowReplCommand.class, CLUSTER_URL_OPTION, List.of()),
                 arguments(ClusterConfigUpdateReplCommand.class, CLUSTER_URL_OPTION, List.of("{key: value}")),
                 arguments(ClusterStatusReplCommand.class, CLUSTER_URL_OPTION, List.of()),
+                arguments(NodeMetricEnableReplCommand.class, NODE_URL_OPTION, List.of("srcName")),
+                arguments(NodeMetricDisableReplCommand.class, NODE_URL_OPTION, List.of("srcName")),
+                arguments(NodeMetricListReplCommand.class, NODE_URL_OPTION, List.of()),
                 arguments(LogicalTopologyReplCommand.class, CLUSTER_URL_OPTION, List.of()),
                 arguments(PhysicalTopologyReplCommand.class, CLUSTER_URL_OPTION, List.of()),
                 arguments(ClusterInitReplCommand.class, CLUSTER_URL_OPTION, List.of("--cluster-name=cluster", "--meta-storage-node=test")),
diff --git a/modules/cli/src/test/java/org/apache/ignite/internal/cli/deprecated/IgniteCliInterfaceTest.java b/modules/cli/src/test/java/org/apache/ignite/internal/cli/deprecated/IgniteCliInterfaceTest.java
index aa86b22bcb..76cec8d5b0 100644
--- a/modules/cli/src/test/java/org/apache/ignite/internal/cli/deprecated/IgniteCliInterfaceTest.java
+++ b/modules/cli/src/test/java/org/apache/ignite/internal/cli/deprecated/IgniteCliInterfaceTest.java
@@ -508,6 +508,67 @@ public class IgniteCliInterfaceTest extends AbstractCliTest {
                 assertThatStderrIsEmpty();
             }
         }
+
+        @Nested
+        @DisplayName("metric")
+        class Metric {
+            @Test
+            @DisplayName("metric enable srcName")
+            void enable() {
+                clientAndServer
+                        .when(request()
+                                .withMethod("POST")
+                                .withPath("/management/v1/metric/node/enable")
+                                .withBody("srcName")
+                        )
+                        .respond(response(null));
+
+                int exitCode = execute("node metric enable --node-url " + mockUrl + " srcName");
+
+                assertThatExitCodeMeansSuccess(exitCode);
+
+                assertOutputEqual("Metric source was enabled successfully");
+                assertThatStderrIsEmpty();
+            }
+
+            @Test
+            @DisplayName("metric disable srcName")
+            void disable() {
+                clientAndServer
+                        .when(request()
+                                .withMethod("POST")
+                                .withPath("/management/v1/metric/node/disable")
+                                .withBody("srcName")
+                        )
+                        .respond(response(null));
+
+                int exitCode = execute("node metric disable --node-url " + mockUrl + " srcName");
+
+                assertThatExitCodeMeansSuccess(exitCode);
+
+                assertOutputEqual("Metric source was disabled successfully");
+                assertThatStderrIsEmpty();
+            }
+
+            @Test
+            @DisplayName("metric list")
+            void list() {
+                String responseBody = "[{\"name\":\"enabledMetric\",\"enabled\":true},{\"name\":\"disabledMetric\",\"enabled\":false}]";
+                clientAndServer
+                        .when(request()
+                                .withMethod("GET")
+                                .withPath("/management/v1/metric/node")
+                        )
+                        .respond(response(responseBody));
+
+                int exitCode = execute("node metric list --node-url " + mockUrl);
+
+                assertThatExitCodeMeansSuccess(exitCode);
+
+                assertOutputEqual("Enabled metric sources:\nenabledMetric\nDisabled metric sources:\ndisabledMetric\n");
+                assertThatStderrIsEmpty();
+            }
+        }
     }
 
     /**
diff --git a/modules/metrics/build.gradle b/modules/metrics/build.gradle
index 7f5a8fa822..b48e1594de 100644
--- a/modules/metrics/build.gradle
+++ b/modules/metrics/build.gradle
@@ -20,10 +20,15 @@ apply from: "$rootDir/buildscripts/java-junit5.gradle"
 apply from: "$rootDir/buildscripts/java-integration-test.gradle"
 
 dependencies {
+    annotationProcessor project(":ignite-configuration-annotation-processor")
+    annotationProcessor libs.micronaut.inject.annotation.processor
+
     implementation project(':ignite-core')
     implementation project(':ignite-configuration')
     implementation project(':ignite-configuration-api')
+    implementation project(':ignite-rest-api')
     implementation libs.jetbrains.annotations
+    implementation libs.micronaut.http.core
 
     testImplementation libs.hamcrest.core
     testImplementation libs.mockito.core
diff --git a/modules/metrics/pom.xml b/modules/metrics/pom.xml
index dfc7327191..ad35a6c216 100644
--- a/modules/metrics/pom.xml
+++ b/modules/metrics/pom.xml
@@ -48,6 +48,11 @@
             <artifactId>ignite-configuration-api</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-rest-api</artifactId>
+        </dependency>
+
         <!-- 3rd party dependencies -->
         <dependency>
             <groupId>org.jetbrains</groupId>
@@ -113,6 +118,11 @@
                             <artifactId>ignite-configuration-annotation-processor</artifactId>
                             <version>${project.version}</version>
                         </path>
+                        <path>
+                            <groupId>io.micronaut</groupId>
+                            <artifactId>micronaut-inject-java</artifactId>
+                            <version>${micronaut.version}</version>
+                        </path>
                     </annotationProcessorPaths>
                 </configuration>
             </plugin>
diff --git a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/MetricManager.java b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/MetricManager.java
index b4f2ca0e0e..01b603b283 100644
--- a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/MetricManager.java
+++ b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/MetricManager.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.metrics;
 
+import java.util.Collection;
 import java.util.Map;
 import java.util.ServiceLoader;
 import java.util.ServiceLoader.Provider;
@@ -199,6 +200,15 @@ public class MetricManager implements IgniteComponent {
         return registry.metricSnapshot();
     }
 
+    /**
+     * Gets a collection of metric sources.
+     *
+     * @return collection of metric sources
+     */
+    public Collection<MetricSource> metricSources() {
+        return registry.metricSources();
+    }
+
     private <T extends ExporterView> void checkAndStartExporter(
             String exporterName,
             T exporterConfiguration) {
diff --git a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/MetricRegistry.java b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/MetricRegistry.java
index 3acbf3d0db..3d6679fc2d 100644
--- a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/MetricRegistry.java
+++ b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/MetricRegistry.java
@@ -21,7 +21,9 @@ import static java.util.Collections.emptyMap;
 import static java.util.Collections.unmodifiableMap;
 import static java.util.Objects.requireNonNull;
 
+import java.util.Collection;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
 import java.util.concurrent.locks.ReentrantLock;
@@ -193,7 +195,7 @@ public class MetricRegistry {
             MetricSource src = sources.get(srcName);
 
             if (src == null) {
-                throw new IllegalStateException("Metrics source with given name doesn't exists: " + srcName);
+                throw new IllegalStateException("Metrics source with given name doesn't exist: " + srcName);
             }
 
             if (!src.enabled()) {
@@ -290,4 +292,18 @@ public class MetricRegistry {
     public IgniteBiTuple<Map<String, MetricSet>, Long> metricSnapshot() {
         return metricSnapshot;
     }
+
+    /**
+     * Gets a collection of registered metric sources.
+     *
+     * @return Metric sources.
+     */
+    public Collection<MetricSource> metricSources() {
+        lock.lock();
+        try {
+            return List.copyOf(sources.values());
+        } finally {
+            lock.unlock();
+        }
+    }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/NodeCommand.java b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/rest/MetricRestFactory.java
similarity index 54%
copy from modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/NodeCommand.java
copy to modules/metrics/src/main/java/org/apache/ignite/internal/metrics/rest/MetricRestFactory.java
index abbe1811c6..9710de23b7 100644
--- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/node/NodeCommand.java
+++ b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/rest/MetricRestFactory.java
@@ -15,22 +15,28 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.cli.commands.node;
+package org.apache.ignite.internal.metrics.rest;
 
-import org.apache.ignite.cli.commands.node.version.NodeVersionCommand;
-import org.apache.ignite.internal.cli.commands.node.config.NodeConfigCommand;
-import org.apache.ignite.internal.cli.commands.node.status.NodeStatusCommand;
-import org.apache.ignite.internal.cli.deprecated.spec.NodeCommandSpec;
-import picocli.CommandLine.Command;
-import picocli.CommandLine.Mixin;
+import io.micronaut.context.annotation.Bean;
+import io.micronaut.context.annotation.Factory;
+import jakarta.inject.Singleton;
+import org.apache.ignite.internal.metrics.MetricManager;
+import org.apache.ignite.internal.rest.RestFactory;
 
 /**
- * Node command.
+ * Factory that creates beans that are needed for {@link NodeMetricController}.
  */
-@Command(name = "node",
-        subcommands = {NodeConfigCommand.class, NodeStatusCommand.class, NodeVersionCommand.class},
-        description = "Node operations")
-public class NodeCommand {
-    @Mixin
-    NodeCommandSpec nodeCommandSpec;
+@Factory
+public class MetricRestFactory implements RestFactory {
+    private final MetricManager metricManager;
+
+    public MetricRestFactory(MetricManager metricManager) {
+        this.metricManager = metricManager;
+    }
+
+    @Bean
+    @Singleton
+    public MetricManager metricManager() {
+        return metricManager;
+    }
 }
diff --git a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/rest/NodeMetricController.java b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/rest/NodeMetricController.java
new file mode 100644
index 0000000000..30271d9256
--- /dev/null
+++ b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/rest/NodeMetricController.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.metrics.rest;
+
+import io.micronaut.http.annotation.Controller;
+import java.util.Collection;
+import java.util.stream.Collectors;
+import org.apache.ignite.internal.metrics.MetricManager;
+import org.apache.ignite.internal.metrics.rest.exception.MetricNotFoundException;
+import org.apache.ignite.internal.rest.api.metric.MetricSourceDto;
+import org.apache.ignite.internal.rest.api.metric.NodeMetricApi;
+
+/** Node metric controller. */
+@Controller("/management/v1/metric/node")
+public class NodeMetricController implements NodeMetricApi {
+    private final MetricManager metricManager;
+
+    public NodeMetricController(MetricManager metricManager) {
+        this.metricManager = metricManager;
+    }
+
+    @Override
+    public void enable(String srcName) {
+        try {
+            metricManager.enable(srcName);
+        } catch (IllegalStateException e) {
+            throw new MetricNotFoundException(e);
+        }
+    }
+
+    @Override
+    public void disable(String srcName) {
+        try {
+            metricManager.disable(srcName);
+        } catch (IllegalStateException e) {
+            throw new MetricNotFoundException(e);
+        }
+    }
+
+    @Override
+    public Collection<MetricSourceDto> list() {
+        return metricManager.metricSources().stream()
+                .map(source -> new MetricSourceDto(source.name(), source.enabled()))
+                .collect(Collectors.toList());
+    }
+}
diff --git a/modules/metrics/build.gradle b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/rest/exception/MetricNotFoundException.java
similarity index 59%
copy from modules/metrics/build.gradle
copy to modules/metrics/src/main/java/org/apache/ignite/internal/metrics/rest/exception/MetricNotFoundException.java
index 7f5a8fa822..e6e2300514 100644
--- a/modules/metrics/build.gradle
+++ b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/rest/exception/MetricNotFoundException.java
@@ -15,19 +15,13 @@
  * limitations under the License.
  */
 
-apply from: "$rootDir/buildscripts/java-core.gradle"
-apply from: "$rootDir/buildscripts/java-junit5.gradle"
-apply from: "$rootDir/buildscripts/java-integration-test.gradle"
+package org.apache.ignite.internal.metrics.rest.exception;
 
-dependencies {
-    implementation project(':ignite-core')
-    implementation project(':ignite-configuration')
-    implementation project(':ignite-configuration-api')
-    implementation libs.jetbrains.annotations
-
-    testImplementation libs.hamcrest.core
-    testImplementation libs.mockito.core
-    testImplementation project(':ignite-core')
+/**
+ * Exception that is thrown when requested metric is not found in the registry.
+ */
+public class MetricNotFoundException extends RuntimeException {
+    public MetricNotFoundException(Throwable cause) {
+        super(cause);
+    }
 }
-
-description = 'ignite-metrics'
diff --git a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/rest/exception/handler/MetricNotFoundExceptionHandler.java b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/rest/exception/handler/MetricNotFoundExceptionHandler.java
new file mode 100644
index 0000000000..d37e00edd8
--- /dev/null
+++ b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/rest/exception/handler/MetricNotFoundExceptionHandler.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.metrics.rest.exception.handler;
+
+import io.micronaut.context.annotation.Requires;
+import io.micronaut.http.HttpRequest;
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.server.exceptions.ExceptionHandler;
+import jakarta.inject.Singleton;
+import org.apache.ignite.internal.metrics.rest.exception.MetricNotFoundException;
+import org.apache.ignite.internal.rest.api.Problem;
+import org.apache.ignite.internal.rest.constants.HttpCode;
+import org.apache.ignite.internal.rest.problem.HttpProblemResponse;
+
+/**
+ * Handles {@link MetricNotFoundException} and represents it as a rest response.
+ */
+@Singleton
+@Requires(classes = {MetricNotFoundException.class, ExceptionHandler.class})
+public class MetricNotFoundExceptionHandler implements
+        ExceptionHandler<MetricNotFoundException, HttpResponse<? extends Problem>> {
+
+    @Override
+    public HttpResponse<? extends Problem> handle(HttpRequest request, MetricNotFoundException exception) {
+        return HttpProblemResponse.from(
+                Problem.fromHttpCode(HttpCode.NOT_FOUND)
+                        .detail(exception.getCause().getMessage())
+        );
+    }
+}
diff --git a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/metric/MetricSourceDto.java b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/metric/MetricSourceDto.java
new file mode 100644
index 0000000000..1cda8844b4
--- /dev/null
+++ b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/metric/MetricSourceDto.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.rest.api.metric;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+/**
+ * REST representation of MetricSource.
+ */
+@Schema(name = "MetricSource")
+public class MetricSourceDto {
+    /** Name of the metric source. */
+    private final String name;
+
+    /** Enabled. */
+    private final boolean enabled;
+
+    /**
+     * Constructor.
+     *
+     * @param name metric source name
+     * @param enabled flags showing whether this metric source is enabled or not
+     */
+    @JsonCreator
+    public MetricSourceDto(
+            @JsonProperty("name") String name,
+            @JsonProperty("enabled") boolean enabled) {
+        this.name = name;
+        this.enabled = enabled;
+    }
+
+    /**
+     * Returns the metric source name.
+     *
+     * @return metric source name
+     */
+    @JsonGetter("name")
+    public String name() {
+        return name;
+    }
+
+    /**
+     * Returns the status of the metric source.
+     *
+     * @return {@code true} if metrics are enabled, otherwise - {@code false}
+     */
+    @JsonGetter("enabled")
+    public boolean enabled() {
+        return enabled;
+    }
+}
diff --git a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/metric/NodeMetricApi.java b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/metric/NodeMetricApi.java
new file mode 100644
index 0000000000..900affadaa
--- /dev/null
+++ b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/metric/NodeMetricApi.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.rest.api.metric;
+
+import io.micronaut.http.annotation.Body;
+import io.micronaut.http.annotation.Consumes;
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Get;
+import io.micronaut.http.annotation.Post;
+import io.micronaut.http.annotation.Produces;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import java.util.Collection;
+import org.apache.ignite.internal.rest.api.Problem;
+import org.apache.ignite.internal.rest.constants.MediaType;
+
+/** Node metric endpoint. */
+@Controller("/management/v1/metric/node")
+@Tag(name = "nodeMetric")
+public interface NodeMetricApi {
+
+    /** Enable metric source. */
+    @Operation(operationId = "enableNodeMetric")
+    @ApiResponse(responseCode = "200", description = "Metric source enabled")
+    @ApiResponse(responseCode = "500", description = "Internal error",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
+    @ApiResponse(responseCode = "404", description = "Metric source not found",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
+    @Consumes(MediaType.TEXT_PLAIN)
+    @Produces(MediaType.PROBLEM_JSON)
+    @Post("enable")
+    void enable(@Body String srcName);
+
+    /** Disable metric source. */
+    @Operation(operationId = "disableNodeMetric")
+    @ApiResponse(responseCode = "200", description = "Metric source disabled")
+    @ApiResponse(responseCode = "500", description = "Internal error",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
+    @ApiResponse(responseCode = "404", description = "Metric source not found",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
+    @Consumes(MediaType.TEXT_PLAIN)
+    @Produces(MediaType.PROBLEM_JSON)
+    @Post("disable")
+    void disable(@Body String srcName);
+
+    /** List metric sources. */
+    @Operation(operationId = "listNodeMetrics")
+    @ApiResponse(responseCode = "200", description = "Metric sources returned")
+    @ApiResponse(responseCode = "500", description = "Internal error",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
+    @Produces(MediaType.APPLICATION_JSON)
+    @Get()
+    Collection<MetricSourceDto> list();
+}
diff --git a/modules/rest/openapi/openapi.yaml b/modules/rest/openapi/openapi.yaml
index 70848c2896..f2f496b777 100644
--- a/modules/rest/openapi/openapi.yaml
+++ b/modules/rest/openapi/openapi.yaml
@@ -305,6 +305,81 @@ paths:
             application/problem+json:
               schema:
                 $ref: '#/components/schemas/Problem'
+  /management/v1/metric/node:
+    get:
+      tags:
+      - nodeMetric
+      operationId: listNodeMetrics
+      parameters: []
+      responses:
+        "200":
+          description: Metric sources returned
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/MetricSource'
+        "500":
+          description: Internal error
+          content:
+            application/problem+json:
+              schema:
+                $ref: '#/components/schemas/Problem'
+  /management/v1/metric/node/disable:
+    post:
+      tags:
+      - nodeMetric
+      operationId: disableNodeMetric
+      parameters: []
+      requestBody:
+        content:
+          text/plain:
+            schema:
+              type: string
+        required: true
+      responses:
+        "200":
+          description: Metric source disabled
+        "500":
+          description: Internal error
+          content:
+            application/problem+json:
+              schema:
+                $ref: '#/components/schemas/Problem'
+        "404":
+          description: Metric source not found
+          content:
+            application/problem+json:
+              schema:
+                $ref: '#/components/schemas/Problem'
+  /management/v1/metric/node/enable:
+    post:
+      tags:
+      - nodeMetric
+      operationId: enableNodeMetric
+      parameters: []
+      requestBody:
+        content:
+          text/plain:
+            schema:
+              type: string
+        required: true
+      responses:
+        "200":
+          description: Metric source enabled
+        "500":
+          description: Internal error
+          content:
+            application/problem+json:
+              schema:
+                $ref: '#/components/schemas/Problem'
+        "404":
+          description: Metric source not found
+          content:
+            application/problem+json:
+              schema:
+                $ref: '#/components/schemas/Problem'
   /management/v1/node/state:
     get:
       tags:
@@ -415,6 +490,16 @@ components:
           type: string
         reason:
           type: string
+    MetricSource:
+      required:
+      - enabled
+      - name
+      type: object
+      properties:
+        name:
+          type: string
+        enabled:
+          type: boolean
     NetworkAddress:
       required:
       - consistentId
diff --git a/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java b/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java
index df854616d8..6ab45ff11f 100644
--- a/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java
+++ b/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java
@@ -38,6 +38,7 @@ import org.apache.ignite.internal.rest.api.cluster.ClusterManagementApi;
 import org.apache.ignite.internal.rest.api.cluster.TopologyApi;
 import org.apache.ignite.internal.rest.api.configuration.ClusterConfigurationApi;
 import org.apache.ignite.internal.rest.api.configuration.NodeConfigurationApi;
+import org.apache.ignite.internal.rest.api.metric.NodeMetricApi;
 import org.apache.ignite.internal.rest.api.node.NodeManagementApi;
 import org.apache.ignite.lang.IgniteInternalException;
 import org.jetbrains.annotations.Nullable;
@@ -58,6 +59,7 @@ import org.jetbrains.annotations.Nullable;
         NodeConfigurationApi.class,
         ClusterManagementApi.class,
         NodeManagementApi.class,
+        NodeMetricApi.class,
         TopologyApi.class
 })
 public class RestComponent implements IgniteComponent {
diff --git a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
index e96e576913..0af27b5607 100644
--- a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
+++ b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
@@ -63,6 +63,7 @@ import org.apache.ignite.internal.metastorage.MetaStorageManager;
 import org.apache.ignite.internal.metastorage.server.persistence.RocksDbKeyValueStorage;
 import org.apache.ignite.internal.metrics.MetricManager;
 import org.apache.ignite.internal.metrics.configuration.MetricConfiguration;
+import org.apache.ignite.internal.metrics.rest.MetricRestFactory;
 import org.apache.ignite.internal.raft.Loza;
 import org.apache.ignite.internal.raft.configuration.RaftConfiguration;
 import org.apache.ignite.internal.raft.storage.impl.VolatileLogStorageFactoryCreator;
@@ -328,14 +329,7 @@ public class IgniteImpl implements Ignite {
 
         metricManager.configure(clusterCfgMgr.configurationRegistry().getConfiguration(MetricConfiguration.KEY));
 
-        RestFactory presentationsFactory = new PresentationsFactory(nodeCfgMgr, clusterCfgMgr);
-        RestFactory clusterManagementRestFactory = new ClusterManagementRestFactory(clusterSvc, cmgMgr);
-        RestFactory nodeManagementRestFactory = new NodeManagementRestFactory(lifecycleManager, () -> name);
-        RestConfiguration restConfiguration = nodeCfgMgr.configurationRegistry().getConfiguration(RestConfiguration.KEY);
-        restComponent = new RestComponent(
-                List.of(presentationsFactory, clusterManagementRestFactory, nodeManagementRestFactory),
-                restConfiguration
-        );
+        restComponent = createRestComponent(name);
 
         baselineMgr = new BaselineManager(
                 clusterCfgMgr,
@@ -416,6 +410,21 @@ public class IgniteImpl implements Ignite {
         );
     }
 
+    private RestComponent createRestComponent(String name) {
+        RestFactory presentationsFactory = new PresentationsFactory(nodeCfgMgr, clusterCfgMgr);
+        RestFactory clusterManagementRestFactory = new ClusterManagementRestFactory(clusterSvc, cmgMgr);
+        RestFactory nodeManagementRestFactory = new NodeManagementRestFactory(lifecycleManager, () -> name);
+        RestFactory nodeMetricRestFactory = new MetricRestFactory(metricManager);
+        RestConfiguration restConfiguration = nodeCfgMgr.configurationRegistry().getConfiguration(RestConfiguration.KEY);
+        return new RestComponent(
+                List.of(presentationsFactory,
+                        clusterManagementRestFactory,
+                        nodeManagementRestFactory,
+                        nodeMetricRestFactory),
+                restConfiguration
+        );
+    }
+
     private static ConfigurationModules loadConfigurationModules(ClassLoader classLoader) {
         var modulesProvider = new ServiceLoaderModulesProvider();
         List<ConfigurationModule> modules = modulesProvider.modules(classLoader);