diff --git a/CHANGES.md b/CHANGES.md
index 986942efa27bfd61eae7552a111a716b0faa1d3b..09d958f87fa3b039dcf84317b213d94a32ebc8cc 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -59,6 +59,7 @@ Release Notes.
* Add functions in MAL to filter metrics according to the metric value.
* Optimize the self monitoring grafana dashboard.
* Enhance the export service.
+* Add function `retagByK8sMeta` and opt type `K8sRetagType.Pod2Service` in MAL for k8s to relate pods and services.
#### UI
* Update selector scroller to show in all pages.
diff --git a/docs/en/concepts-and-designs/mal.md b/docs/en/concepts-and-designs/mal.md
index 7770b62038439b7882b5d711bc521279c9a5349c..4a3d7fb8560388ac7ba58ba56a550b569a6d58ee 100644
--- a/docs/en/concepts-and-designs/mal.md
+++ b/docs/en/concepts-and-designs/mal.md
@@ -57,6 +57,31 @@ For example, this filters all instance_trace_count samples for values >= 33:
```
instance_trace_count.valueGreaterEqual(33)
```
+### Tag manipulator
+MAL provides tag manipulators to change(add/delete/update) tags and their values.
+
+#### K8s
+MAL supports using the metadata of k8s to manipulate the tags and their values.
+This feature requires OAP Server to have the authority to access the K8s's `API Server`.
+
+##### retagByK8sMeta
+`retagByK8sMeta(newLabelName, K8sRetagType, existingLabelName)`. Add a new tag to the sample family based on an existing label's value. Provide several internal converting types, including
+- K8sRetagType.Pod2Service
+
+Add a tag to the sample by using `service` as the key, `$serviceName.$namespace` as the value, by the given value of the tag key, which represents the name of a pod.
+
+For example:
+```
+container_cpu_usage_seconds_total{container=my-nginx, cpu=total, pod=my-nginx-5dc4865748-mbczh} 2
+```
+Expression:
+```
+container_cpu_usage_seconds_total.retagByK8sMeta('service' , K8sRetagType.Pod2Service , 'pod')
+```
+Output:
+```
+container_cpu_usage_seconds_total{container=my-nginx, cpu=total, pod=my-nginx-5dc4865748-mbczh, service='nginx-service.default'} 2
+```
### Binary operators
@@ -186,6 +211,7 @@ Examples:
#### time
`time()`. returns the number of seconds since January 1, 1970 UTC.
+
## Down Sampling Operation
MAL should instruct meter-system how to do downsampling for metrics. It doesn't only refer to aggregate raw samples to
`minute` level, but also hints data from `minute` to higher levels, for instance, `hour` and `day`.
diff --git a/oap-server/analyzer/meter-analyzer/pom.xml b/oap-server/analyzer/meter-analyzer/pom.xml
index 5b990368ad6f08d5b3e118c1eb6a1e9d6d258d4e..d4723b79fc0d2b1c98056cf49a500cf866a70b50 100644
--- a/oap-server/analyzer/meter-analyzer/pom.xml
+++ b/oap-server/analyzer/meter-analyzer/pom.xml
@@ -33,7 +33,6 @@
server-core${project.version}
-
org.codehaus.groovygroovy
@@ -42,6 +41,11 @@
io.vavrvavr
+
+ io.kubernetes
+ client-java
+ ${kubernetes.version}
+
\ No newline at end of file
diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/Analyzer.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/Analyzer.java
index 7c39d87ff4cae49c7581af9d5059933410f84c0a..4b9975e780a5a9f0a49790a09fcde88eeb7ea71f 100644
--- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/Analyzer.java
+++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/Analyzer.java
@@ -34,6 +34,7 @@ import org.apache.skywalking.oap.meter.analyzer.dsl.ExpressionParsingContext;
import org.apache.skywalking.oap.meter.analyzer.dsl.Result;
import org.apache.skywalking.oap.meter.analyzer.dsl.Sample;
import org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamily;
+import org.apache.skywalking.oap.meter.analyzer.k8s.K8sInfoRegistry;
import org.apache.skywalking.oap.server.core.analysis.NodeType;
import org.apache.skywalking.oap.server.core.analysis.TimeBucket;
import org.apache.skywalking.oap.server.core.analysis.manual.endpoint.EndpointTraffic;
@@ -219,6 +220,10 @@ public class Analyzer {
}
}
createMetric(ctx.getScopeType(), metricType.literal, ctx.getDownsampling());
+
+ if (ctx.isRetagByK8sMeta()) {
+ K8sInfoRegistry.getInstance().start();
+ }
}
private void createMetric(final ScopeType scopeType,
diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DSL.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DSL.java
index 45fa7eeb0c9ff8b14fd6d95038b5d8cd4ad27d93..65a93aeb7ff2e094d8bb0f84b63c4ae2bffd5ab7 100644
--- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DSL.java
+++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DSL.java
@@ -21,7 +21,9 @@ package org.apache.skywalking.oap.meter.analyzer.dsl;
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.util.DelegatingScript;
+import org.apache.skywalking.oap.meter.analyzer.dsl.tagOpt.K8sRetagType;
import org.codehaus.groovy.control.CompilerConfiguration;
+import org.codehaus.groovy.control.customizers.ImportCustomizer;
/**
* DSL combines methods to parse groovy based DSL expression.
@@ -37,6 +39,9 @@ public final class DSL {
public static Expression parse(final String expression) {
CompilerConfiguration cc = new CompilerConfiguration();
cc.setScriptBaseClass(DelegatingScript.class.getName());
+ ImportCustomizer icz = new ImportCustomizer();
+ icz.addImport("K8sRetagType", K8sRetagType.class.getName());
+ cc.addCompilationCustomizers(icz);
GroovyShell sh = new GroovyShell(new Binding(), cc);
DelegatingScript script = (DelegatingScript) sh.parse(expression);
return new Expression(expression, script);
diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext.java
index 2a421fc406a3add26403f34f2c81d4c51f823598..f571ccae39a4906bf892c62d965e333cb9d9e220 100644
--- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext.java
+++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext.java
@@ -61,7 +61,6 @@ public class ExpressionParsingContext implements Closeable {
List samples;
boolean isHistogram;
-
int[] percentiles;
Set aggregationLabels;
@@ -72,6 +71,11 @@ public class ExpressionParsingContext implements Closeable {
ScopeType scopeType;
+ /**
+ * Mark whether the retagByK8sMeta func in expressions is active
+ */
+ boolean isRetagByK8sMeta;
+
/**
* Get labels no scope related.
*
diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/SampleFamily.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/SampleFamily.java
index be53356197d0909a5bcbed13ab96abf5948d629c..b4c82d0359f92a326cb2d6d49a1145e335536a26 100644
--- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/SampleFamily.java
+++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/SampleFamily.java
@@ -38,6 +38,7 @@ import org.apache.skywalking.oap.meter.analyzer.dsl.EntityDescription.EndpointEn
import org.apache.skywalking.oap.meter.analyzer.dsl.EntityDescription.EntityDescription;
import org.apache.skywalking.oap.meter.analyzer.dsl.EntityDescription.InstanceEntityDescription;
import org.apache.skywalking.oap.meter.analyzer.dsl.EntityDescription.ServiceEntityDescription;
+import org.apache.skywalking.oap.meter.analyzer.dsl.tagOpt.K8sRetagType;
import org.apache.skywalking.oap.server.core.UnexpectedException;
import org.apache.skywalking.oap.server.core.analysis.meter.MeterEntity;
import org.apache.skywalking.oap.server.core.analysis.meter.ScopeType;
@@ -310,6 +311,18 @@ public class SampleFamily {
);
}
+ /* k8s retags*/
+ public SampleFamily retagByK8sMeta(String newLabelName, K8sRetagType type, String existingLabelName) {
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(newLabelName));
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(existingLabelName));
+ ExpressionParsingContext.get().ifPresent(ctx -> ctx.isRetagByK8sMeta = true);
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+
+ return SampleFamily.build(this.context, type.execute(samples, newLabelName, existingLabelName));
+ }
+
public SampleFamily histogram() {
return histogram("le", this.context.defaultHistogramBucketUnit);
}
diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/tagOpt/K8sRetagType.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/tagOpt/K8sRetagType.java
new file mode 100644
index 0000000000000000000000000000000000000000..8647831bb5b2d26dc10929dd5671161e5c4ff2cd
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/tagOpt/K8sRetagType.java
@@ -0,0 +1,50 @@
+/*
+ * 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.skywalking.oap.meter.analyzer.dsl.tagOpt;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import java.util.Arrays;
+import java.util.Map;
+import org.apache.skywalking.oap.meter.analyzer.dsl.Sample;
+import org.apache.skywalking.oap.meter.analyzer.k8s.K8sInfoRegistry;
+
+public enum K8sRetagType implements Retag {
+
+ Pod2Service {
+ @Override
+ public Sample[] execute(final Sample[] ss, final String newLabelName, final String existingLabelName) {
+ Sample[] samples = Arrays.stream(ss).map(sample -> {
+ String podName = sample.getLabels().get(existingLabelName);
+
+ if (!Strings.isNullOrEmpty(podName)) {
+ String serviceName = K8sInfoRegistry.getInstance().findServiceName(podName);
+ if (!Strings.isNullOrEmpty(serviceName)) {
+ Map labels = Maps.newHashMap(sample.getLabels());
+ labels.put(newLabelName, serviceName);
+ return sample.toBuilder().labels(ImmutableMap.copyOf(labels)).build();
+ }
+ }
+ return sample;
+ }).toArray(Sample[]::new);
+ return samples;
+ }
+ }
+}
diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/tagOpt/Retag.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/tagOpt/Retag.java
new file mode 100644
index 0000000000000000000000000000000000000000..eda574e35983374aa0b2fd58fb94107104fc3c9a
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/tagOpt/Retag.java
@@ -0,0 +1,25 @@
+/*
+ * 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.skywalking.oap.meter.analyzer.dsl.tagOpt;
+
+import org.apache.skywalking.oap.meter.analyzer.dsl.Sample;
+
+public interface Retag {
+ Sample[] execute(Sample[] ss, String newLabelName, String existingLabelName);
+}
diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/k8s/K8sInfoRegistry.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/k8s/K8sInfoRegistry.java
new file mode 100644
index 0000000000000000000000000000000000000000..694cd311a5de31d7702e7f81420665711cb9cba1
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/k8s/K8sInfoRegistry.java
@@ -0,0 +1,224 @@
+/*
+ * 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.skywalking.oap.meter.analyzer.k8s;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import io.kubernetes.client.informer.ResourceEventHandler;
+import io.kubernetes.client.informer.SharedInformerFactory;
+import io.kubernetes.client.openapi.Configuration;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.models.V1Endpoints;
+import io.kubernetes.client.openapi.models.V1EndpointsList;
+import io.kubernetes.client.openapi.models.V1ObjectMeta;
+import io.kubernetes.client.openapi.models.V1PodList;
+import io.kubernetes.client.util.Config;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.models.V1Pod;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static java.util.Objects.isNull;
+import static java.util.Optional.ofNullable;
+
+@Slf4j
+public class K8sInfoRegistry {
+
+ private final static K8sInfoRegistry INSTANCE = new K8sInfoRegistry();
+ private final AtomicBoolean isStarted = new AtomicBoolean(false);
+ private final Map ipPodMap = new ConcurrentHashMap<>();
+ private final Map ipServiceMap = new ConcurrentHashMap<>();
+ private final Map podServiceMap = new ConcurrentHashMap<>();
+ private ExecutorService executor;
+
+ public static K8sInfoRegistry getInstance() {
+ return INSTANCE;
+ }
+
+ private void init() {
+ executor = Executors.newCachedThreadPool(
+ new ThreadFactoryBuilder()
+ .setNameFormat("K8sInfoRegistry-%d")
+ .setDaemon(true)
+ .build()
+ );
+ }
+
+ @SneakyThrows
+ public void start() {
+ if (isStarted.compareAndSet(false, true)) {
+ init();
+ final ApiClient apiClient = Config.defaultClient();
+ apiClient.setHttpClient(apiClient.getHttpClient()
+ .newBuilder()
+ .readTimeout(0, TimeUnit.SECONDS)
+ .build());
+ Configuration.setDefaultApiClient(apiClient);
+
+ final CoreV1Api coreV1Api = new CoreV1Api();
+ final SharedInformerFactory factory = new SharedInformerFactory(executor);
+
+ listenEndpointsEvents(coreV1Api, factory);
+ listenPodEvents(coreV1Api, factory);
+ factory.startAllRegisteredInformers();
+ }
+ }
+
+ private void listenEndpointsEvents(final CoreV1Api coreV1Api, final SharedInformerFactory factory) {
+ factory.sharedIndexInformerFor(
+ params -> coreV1Api.listEndpointsForAllNamespacesCall(
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ params.resourceVersion,
+ 300,
+ params.watch,
+ null
+ ),
+ V1Endpoints.class,
+ V1EndpointsList.class
+ ).addEventHandler(new ResourceEventHandler() {
+ @Override
+ public void onAdd(final V1Endpoints endpoints) {
+ addEndpoints(endpoints);
+ }
+
+ @Override
+ public void onUpdate(final V1Endpoints oldEndpoints, final V1Endpoints newEndpoints) {
+ addEndpoints(newEndpoints);
+ }
+
+ @Override
+ public void onDelete(final V1Endpoints endpoints, final boolean deletedFinalStateUnknown) {
+ removeEndpoints(endpoints);
+ }
+ });
+ }
+
+ private void listenPodEvents(final CoreV1Api coreV1Api, final SharedInformerFactory factory) {
+ factory.sharedIndexInformerFor(
+ params -> coreV1Api.listPodForAllNamespacesCall(
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ params.resourceVersion,
+ 300,
+ params.watch,
+ null
+ ),
+ V1Pod.class,
+ V1PodList.class
+ ).addEventHandler(new ResourceEventHandler() {
+ @Override
+ public void onAdd(final V1Pod pod) {
+ addPod(pod);
+ }
+
+ @Override
+ public void onUpdate(final V1Pod oldPod, final V1Pod newPod) {
+ addPod(newPod);
+ }
+
+ @Override
+ public void onDelete(final V1Pod pod, final boolean deletedFinalStateUnknown) {
+ removePod(pod);
+ }
+ });
+ }
+
+ private void addPod(final V1Pod pod) {
+ ofNullable(pod.getStatus()).ifPresent(
+ status -> ofNullable(status.getPodIP()).ifPresent(
+ ip -> ipPodMap.put(ip, pod))
+ );
+
+ recompose();
+ }
+
+ private void removePod(final V1Pod pod) {
+ ofNullable(pod.getStatus()).ifPresent(
+ status -> ipPodMap.remove(status.getPodIP())
+ );
+ ofNullable(pod.getMetadata()).ifPresent(
+ metadata -> podServiceMap.remove(pod.getMetadata().getName())
+ );
+ }
+
+ private void addEndpoints(final V1Endpoints endpoints) {
+ V1ObjectMeta endpointsMetadata = endpoints.getMetadata();
+ if (isNull(endpointsMetadata)) {
+ log.error("Endpoints metadata is null: {}", endpoints);
+ return;
+ }
+
+ final String namespace = endpointsMetadata.getNamespace();
+ final String name = endpointsMetadata.getName();
+
+ ofNullable(endpoints.getSubsets()).ifPresent(subsets -> subsets.forEach(
+ subset -> ofNullable(subset.getAddresses()).ifPresent(addresses -> addresses.forEach(
+ address -> ipServiceMap.put(address.getIp(), name + "." + namespace)
+ ))
+ ));
+
+ recompose();
+ }
+
+ private void removeEndpoints(final V1Endpoints endpoints) {
+ ofNullable(endpoints.getSubsets()).ifPresent(subsets -> subsets.forEach(
+ subset -> ofNullable(subset.getAddresses()).ifPresent(addresses -> addresses.forEach(
+ address -> ipServiceMap.remove(address.getIp())
+ ))
+ ));
+ recompose();
+ }
+
+ private void recompose() {
+ ipPodMap.forEach((ip, pod) -> {
+ final String namespaceService = ipServiceMap.get(ip);
+ if (isNullOrEmpty(namespaceService)) {
+ podServiceMap.remove(ip);
+ return;
+ }
+
+ final V1ObjectMeta podMetadata = pod.getMetadata();
+ if (isNull(podMetadata)) {
+ log.warn("Pod metadata is null, {}", pod);
+ return;
+ }
+
+ podServiceMap.put(pod.getMetadata().getName(), namespaceService);
+ });
+ }
+
+ public String findServiceName(String podName) {
+ return this.podServiceMap.get(podName);
+ }
+}
diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/dsl/K8sTagTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/dsl/K8sTagTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..199628754b06bb013d65ce95517b2a34f30f0922
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/dsl/K8sTagTest.java
@@ -0,0 +1,212 @@
+/*
+ * 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.skywalking.oap.meter.analyzer.dsl;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Arrays;
+import java.util.Collection;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.oap.meter.analyzer.k8s.K8sInfoRegistry;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.mockito.Mockito;
+import org.powermock.reflect.Whitebox;
+
+import static com.google.common.collect.ImmutableMap.of;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.when;
+
+@Slf4j
+@RunWith(Parameterized.class)
+public class K8sTagTest {
+
+ @Parameterized.Parameter
+ public String name;
+
+ @Parameterized.Parameter(1)
+ public ImmutableMap input;
+
+ @Parameterized.Parameter(2)
+ public String expression;
+
+ @Parameterized.Parameter(3)
+ public Result want;
+
+ @Parameterized.Parameter(4)
+ public boolean isThrow;
+
+ @Parameterized.Parameters(name = "{index}: {0}")
+ public static Collection