Repository: apache/skywalking (language: python)

Issue:
[Feature] Support adding additional attr[0-4] for service level metrics
### Search before asking

- [X] I had searched in the [issues](https://github.com/apache/skywalking/issues?q=is%3Aissue) and found no similar feature requirement.


### Description

Add attr[0-4] to all service level metrics through OAL and MAL metrics, and support customers to set these attrs through OAL filter(code extension) or MAL SampleFamily processes.
To keep the costs limited, this feature would be totally optional, and we will only wave `layer` as attr0 for the default implementations. 
FYI @Fine0830 We will need UI side enhancement to add a new kind of widget to query layer-based TopN on the root dashboard to get the service list. And those services could jump into the service dashboard like the service table widget.

### Use case

Currently, all service topN needs to be globally query. But, within v9 and v10, we introduced the layer concept, and many more monitoring are added.
Then, people are wondering, whether SkyWalking would provide group or layer-based topN query? Sadly, we can't as service metrics(all metrics actually) don't have extra extension fields.

So, with this extension and MQE extension, we could have new expression such as `service_cpm{attr0="general"}` to get a layer-based topN.

### Related issues

_No response_

### Are you willing to submit a pull request to implement this on your own?

- [ ] Yes I am willing to submit a pull request on my own!

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](https://www.apache.org/foundation/policies/conduct)



Discussion / hints:

Fix patch:
diff --git a/docs/en/api/metrics-query-expression.md b/docs/en/api/metrics-query-expression.md
index c7031ceff16d..8e7833b93168 100644
--- a/docs/en/api/metrics-query-expression.md
+++ b/docs/en/api/metrics-query-expression.md
@@ -229,14 +229,27 @@ The result depends on the `entity` condition in the query.
 
 Expression:
 ```text
-top_n(<metric_name>, <top_number>, <order>)
+top_n(<metric_name>, <top_number>, <order>, <attrs>)
 ```
 
 - `top_number` is the number of the top results, should be a positive integer.
-- `order` is the order of the top results. The value of `order` can be `asc` or `des`.
+- `order` is the order of the top results. The value of `order` can be `asc` or `des`. 
+- `attrs` optional, attrs is the attributes of the metrics, could be used to filter the topN results. 
+   SkyWalking supports 5 attrs: `attr0`, `attr1`, `attr2`, `attr3`, `attr4`. 
+   The format is `attr0='value', attr1='value'...attr4='value4'`, could use one or multiple attrs to filter the topN results.
+  **Notice**: The `attrs` only support Service metrics for now and should be added in the metrics first, see [Metrics Additional Attributes](../concepts-and-designs/metrics-additional-attributes.md).
 
 For example:
-If we want to query the current service's top 10 instances with the highest `service_instance_cpm` metric value, we can use the following expression
+1. If we want to query the top 10 services with the highest `service_cpm` metric value, we can use the following expression and make sure the `entity` is empty:
+```text
+top_n(service_cpm, 10, des)
+```
+If we want to filter the result by `Layer`, we can use the following expression:
+```text
+top_n(service_cpm, 10, des, attr0='GENERAL')
+```
+
+2. If we want to query the current service's top 10 instances with the highest `service_instance_cpm` metric value, we can use the following expression
 under specific service:
 
 ```text
diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index 78e750d08f12..7107a91f821d 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -11,6 +11,7 @@
 * Change the endpoint_traffic to updatable for the additional column `last_ping`.
 * Add Component ID(5023) for the GoZero framework.
 * Support Kong monitoring.
+* Support adding additional attr[0-4] for service level metrics.
 
 #### UI
 
diff --git a/docs/en/concepts-and-designs/mal.md b/docs/en/concepts-and-designs/mal.md
index a3c9a33ef28e..d804ed36c5ad 100644
--- a/docs/en/concepts-and-designs/mal.md
+++ b/docs/en/concepts-and-designs/mal.md
@@ -268,6 +268,10 @@ They extract level relevant labels from metric labels, then informs the meter-sy
  - `processRelation(detect_point_label, [service_label1...], [instance_label1...], source_process_id_label, dest_process_id_label, component_label)` extracts `DetectPoint` labels from first argument, the label value should be `client` or `server`.
    extracts `Service` labels from the first array argument, extracts `Instance` labels from the second array argument, extracts `ProcessID` labels from the fourth and fifth arguments of the source and destination.
 
+## Decorate function
+`decorate({ me -> me.attr0 = ...})`: Decorate the [MeterEntity](../../../oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/MeterEntity.java) with additional attributes. 
+The closure takes the MeterEntity as an argument. This function is used to add additional attributes to the metrics. More details, see [Metrics Additional Attributes](metrics-additional-attributes.md).
+
 ## Configuration file
 
 The OAP can load the configuration at bootstrap. If the new configuration is not well-formed, the OAP fails to start up. The files
diff --git a/docs/en/concepts-and-designs/metrics-additional-attributes.md b/docs/en/concepts-and-designs/metrics-additional-attributes.md
new file mode 100644
index 000000000000..2a5ad7e45965
--- /dev/null
+++ b/docs/en/concepts-and-designs/metrics-additional-attributes.md
@@ -0,0 +1,41 @@
+# Metrics Additional Attributes
+SkyWalking provides [OAL](oal.md) and [MAL](mal.md) to analyze the data source and generate the metrics. 
+Generally, the metrics fields in the storage include `name`, `entity_id`, `service_id` `value` and `timebucket` etc.
+In most cases, these fields are enough to query a metric. However, sometimes we need to add more fields to the metrics to make the query more flexible.
+A typical case is the `topN` query, when we query the top N services by the metric value. 
+If we want to filter the services by `Layer` or `Group` or other specific conditions, it is impossible to do this with the current metrics fields.
+
+Since 10.2.0, SkyWalking supports the metrics attributes through source decorate. 
+SkyWalking provides additional attributes(`attr0...attr4`) fields to the metrics and source. 
+By default, these fields are empty, we can fill them by set a specific decorate logic for source. 
+According to the difference between the `OAL` and `MAL`, the usage of decorate is different.
+
+**Notice:** For now, the metrics attributes only support the `service metrics` and `non-labeled` metrics.
+
+## OAL Source Decorate
+In the OAL script, you can use the [decorator](oal.md#decorator) function to specify a Java Class to decorate the source,
+and the Java Class must follow the following rules:
+- The Class must implement the [ISourceDecorator](../../../oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/ISourceDecorator.java) interface.
+- The Class package must be under the `org.apache.skywalking.*`.
+
+SkyWalking provides a default implementation [ServiceDecorator](../../../oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/ServiceDecorator.java) which set the `attr0` to the service `Layer`.
+The following OAL metrics had been decorated by the `ServiceDecorator` by default:
+```text
+// Service scope metrics
+service_resp_time = from(Service.latency).longAvg().decorator("ServiceDecorator");
+service_sla = from(Service.*).percent(status == true).decorator("ServiceDecorator");
+service_cpm = from(Service.*).cpm().decorator("ServiceDecorator");
+service_apdex = from(Service.latency).apdex(name, status).decorator("ServiceDecorator");
+```
+
+## MAL Source Decorate
+In the MAL script, you can use the [decorate](mal.md#decorate-function) function to decorate the source, and must follow the following rules:
+- The decorate function must after service() function.
+- Not supported for histogram metrics.
+- Not supported for labeled metrics.
+
+SkyWalking does not provide a default script for MAL, you can refer to the following example to set the `attr0` to the service `Layer`:
+```text
+  - name: cpu_load1
+    exp: (node_load1 * 100).service(['node_identifier_host_name'] , Layer.OS_LINUX).decorate({ me -> me.attr0 = me.layer.name()})
+```
diff --git a/docs/en/concepts-and-designs/oal.md b/docs/en/concepts-and-designs/oal.md
index 34e55074ef6f..08c9a739906b 100644
--- a/docs/en/concepts-and-designs/oal.md
+++ b/docs/en/concepts-and-designs/oal.md
@@ -132,6 +132,13 @@ Cast statement is supported in
 2. **Filter expression**. `.filter((cast)tag["transmission.latency"] > 0)`
 3. **Aggregation function parameter**. `.longAvg((cast)strField1== 1,  (cast)strField2)`
 
+## Decorator
+`decorator` is to select a specific decorator to decorate the source for the metrics.
+> service_resp_time = from(Service.latency).longAvg().decorator("ServiceDecorator");
+
+In this case, the `ServiceDecorator` is the `Java Class simple name` which used to decorate the source of `Service` before the aggregation function.
+This function is used to add additional attributes to the metrics. More details, see [Metrics Additional Attributes](metrics-additional-attributes.md).
+
 ## Disable
 `Disable` is an advanced statement in OAL, which is only used in certain cases.
 Some of the aggregation and metrics are defined through core hard codes. Examples include `segment` and `top_n_database_statement`.
diff --git a/docs/menu.yml b/docs/menu.yml
index f349948d22f5..8fb930c30bd5 100644
--- a/docs/menu.yml
+++ b/docs/menu.yml
@@ -342,6 +342,8 @@ catalog:
         path: "/en/concepts-and-designs/profiling"
       - name : "Service Hierarchy Configuration"
         path: "/en/concepts-and-designs/service-hierarchy-configuration"
+      - name: "Metrics Attributes"
+        path: "/en/concepts-and-designs/metrics-attributes"
   - name: "Security Notice"
     path: "/en/security/readme"
   - name: "Debugging"
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 e91a98f2ace0..a51a968a7f5e 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
@@ -693,6 +693,33 @@ public SampleFamily downsampling(final DownsamplingType type) {
         return this;
     }
 
+    /**
+     * Decorate the service meter entity with the given closure.
+     */
+    public SampleFamily decorate(Closure<Void> c) {
+        ExpressionParsingContext.get().ifPresent(ctx -> {
+            if (ctx.getScopeType() != ScopeType.SERVICE) {
+                throw new IllegalStateException("decorate() should be invoked after service()");
+            }
+            if (ctx.isHistogram()) {
+                throw new IllegalStateException("decorate() not supported for histogram metrics");
+            }
+            if (!ctx.getLabels().isEmpty()) {
+                throw new IllegalStateException("decorate() not supported for labeled metrics");
+            }
+        });
+        if (this == EMPTY) {
+            return EMPTY;
+        }
+        this.context.getMeterSamples().keySet().forEach(meterEntity -> {
+            // Only service meter entity can be decorated
+            if (meterEntity.getScopeType().equals(ScopeType.SERVICE)) {
+                c.call(meterEntity);
+            }
+        });
+        return this;
+    }
+
     /**
      * The parsing context holds key results more than sample collection.
      */
diff --git a/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQELexer.g4 b/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQELexer.g4
index b91139ee8552..3fc984fd03ee 100644
--- a/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQELexer.g4
+++ b/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQELexer.g4
@@ -85,6 +85,13 @@ AGGREGATE_LABELS:   'aggregate_labels';
 SORT_VALUES: 'sort_values';
 SORT_LABEL_VALUES: 'sort_label_values';
 
+// Attributes
+ATTR0: 'attr0';
+ATTR1: 'attr1';
+ATTR2: 'attr2';
+ATTR3: 'attr3';
+ATTR4: 'attr4';
+
 // Literals
 INTEGER: Digit+;
 DECIMAL: Digit+ DOT Digit+;
diff --git a/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQEParser.g4 b/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQEParser.g4
index d83c72365c82..1d421e9d7d82 100644
--- a/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQEParser.g4
+++ b/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQEParser.g4
@@ -33,7 +33,7 @@ expression
     | mathematical_operator1 L_PAREN expression COMMA parameter R_PAREN #mathematicalOperator1OP
     | trend L_PAREN metric COMMA INTEGER R_PAREN #trendOP
     | logical_operator L_PAREN expressionList R_PAREN #logicalOperatorOP
-    | topN L_PAREN metric COMMA INTEGER COMMA order R_PAREN  #topNOP
+    | topN L_PAREN metric COMMA INTEGER COMMA order (COMMA attributeList)? R_PAREN  #topNOP
     | relabels L_PAREN expression COMMA label COMMA replaceLabel R_PAREN #relablesOP
     | aggregateLabels L_PAREN expression COMMA aggregateLabelsFunc R_PAREN #aggregateLabelsOp
     | sort_values L_PAREN expression (COMMA INTEGER)? COMMA order R_PAREN #sortValuesOP
@@ -99,3 +99,8 @@ sort_values:
 
 sort_label_values:
     SORT_LABEL_VALUES;
+
+attributeName:
+    ATTR0 | ATTR1 | ATTR2 | ATTR3 | ATTR4;
+attribute: attributeName EQ VALUE_STRING;
+attributeList: attribute (COMMA attribute)*;
diff --git a/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALLexer.g4 b/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALLexer.g4
index b625de9b4527..ccb6cd74d5c6 100644
--- a/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALLexer.g4
+++ b/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALLexer.g4
@@ -63,6 +63,7 @@ SRC_CILIUM_ENDPOINT: 'CiliumEndpoint';
 SRC_CILIUM_SERVICE_RELATION: 'CiliumServiceRelation';
 SRC_CILIUM_SERVICE_INSTANCE_RELATION: 'CiliumServiceInstanceRelation';
 SRC_CILIUM_ENDPOINT_RELATION: 'CiliumEndpointRelation';
+DECORATOR: 'decorator';
 
 
 // Browser keywords
diff --git a/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALParser.g4 b/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALParser.g4
index f44ddb281dad..e78a3102da75 100644
--- a/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALParser.g4
+++ b/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALParser.g4
@@ -38,7 +38,7 @@ disableStatement
     ;
 
 metricStatement
-    : FROM LR_BRACKET (sourceAttrCast)? source (sourceAttributeStmt+) RR_BRACKET (filterStatement+)? DOT aggregateFunction
+    : FROM LR_BRACKET (sourceAttrCast)? source (sourceAttributeStmt+) RR_BRACKET (filterStatement+)? DOT aggregateFunction (decorateSource)?
     ;
 
 filterStatement
@@ -49,6 +49,10 @@ filterExpression
     : expression
     ;
 
+decorateSource
+    : DOT DECORATOR LR_BRACKET STRING_LITERAL RR_BRACKET
+    ;
+
 source
     : SRC_SERVICE | SRC_TCP_SERVICE | SRC_DATABASE_ACCESS | SRC_SERVICE_INSTANCE | SRC_TCP_SERVICE_INSTANCE | SRC_ENDPOINT | SRC_CACHE_ACCESS |
       SRC_SERVICE_RELATION | SRC_TCP_SERVICE_RELATION | SRC_SERVICE_INSTANCE_RELATION | SRC_TCP_SERVICE_INSTANCE_RELATION | SRC_ENDPOINT_RELATION |
diff --git a/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/output/DispatcherContext.java b/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/output/DispatcherContext.java
index e779c4dc6c32..4430fb142eac 100644
--- a/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/output/DispatcherContext.java
+++ b/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/output/DispatcherContext.java
@@ -32,4 +32,5 @@ public class DispatcherContext {
     private String source;
     private String packageName;
     private List<AnalysisResult> metrics = new ArrayList<>();
+    private String sourceDecorator;
 }
diff --git a/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/parser/AnalysisResult.java b/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/parser/AnalysisResult.java
index 68c493985e3b..b6d57d57b444 100644
--- a/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/parser/AnalysisResult.java
+++ b/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/parser/AnalysisResult.java
@@ -89,6 +89,8 @@ public class AnalysisResult {
      */
     private PersistenceColumns serializeFields;
 
+    private String sourceDecorator;
+
     public void addPersistentField(String fieldName, String columnName, Class<?> type) {
         if (persistentFields == null) {
             persistentFields = new ArrayList<>();
diff --git a/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/parser/OALListener.java b/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/parser/OALListener.java
index 5b7ef9f83a2b..4b7e954ba431 100644
--- a/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/parser/OALListener.java
+++ b/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/parser/OALListener.java
@@ -21,10 +21,16 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 import org.antlr.v4.runtime.misc.NotNull;
 import org.apache.skywalking.oal.rt.grammar.OALParser;
 import org.apache.skywalking.oal.rt.grammar.OALParserBaseListener;
+import org.apache.skywalking.oap.server.core.analysis.ISourceDecorator;
+import org.apache.skywalking.oap.server.core.analysis.SourceDecoratorManager;
+import org.apache.skywalking.oap.server.core.analysis.metrics.LabeledValueHolder;
+import org.apache.skywalking.oap.server.core.analysis.metrics.Metrics;
 import org.apache.skywalking.oap.server.core.source.DefaultScopeDefine;
+import org.apache.skywalking.oap.server.core.source.ISource;
 
 public class OALListener extends OALParserBaseListener {
     private List<AnalysisResult> results;
@@ -107,6 +113,32 @@ public void exitFuncParamExpression(OALParser.FuncParamExpressionContext ctx) {
         conditionExpression = null;
     }
 
+    @Override
+    public void enterDecorateSource(OALParser.DecorateSourceContext ctx) {
+        Class<? extends Metrics> metricsClass = MetricsHolder.find(current.getAggregationFuncStmt().getAggregationFunctionName());
+        if (LabeledValueHolder.class.isAssignableFrom(metricsClass)) {
+            throw new IllegalArgumentException(
+                "OAL metric: " + current.getMetricsName() + ", decorate source not support labeled value metrics.");
+        }
+        String decoratorName = ctx.STRING_LITERAL().getText();
+        String decoratorNameTrim = decoratorName.substring(1, decoratorName.length() - 1);
+        current.setSourceDecorator(decoratorNameTrim);
+        Map<String, ISourceDecorator<ISource>> map = SourceDecoratorManager.DECORATOR_MAP;
+        int currentScopeId = current.getFrom().getSourceScopeId();
+        if (currentScopeId != DefaultScopeDefine.SERVICE) {
+            throw new IllegalArgumentException("OAL metric: " + current.getMetricsName() + ", decorate source only support service scope.");
+        }
+        ISourceDecorator<ISource> decorator = map.get(decoratorNameTrim);
+        if (decorator == null) {
+            throw new IllegalArgumentException("OAL metric: " + current.getMetricsName() + " define a decorator: " + decoratorNameTrim
+            + ", but can't find it.");
+        }
+        int scopeId = decorator.getSourceScope();
+        if (scopeId != currentScopeId) {
+            throw new IllegalArgumentException("OAL Decorate Source, expect decorator scope id is: " + currentScopeId + ", but got: " + scopeId);
+        }
+    }
+
     /////////////
     // Expression
     ////////////
diff --git a/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/parser/SourceColumn.java b/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/parser/SourceColumn.java
index f8cea14003c7..c76ac3f8038d 100644
--- a/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/parser/SourceColumn.java
+++ b/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/parser/SourceColumn.java
@@ -35,9 +35,10 @@ public class SourceColumn {
     private String fieldSetter;
     private String fieldGetter;
     private final boolean groupByCondInTopN;
+    private final boolean attribute;
 
     public SourceColumn(String fieldName, String columnName, Class<?> type, boolean isID, int length,
-                        boolean groupByCondInTopN) {
+                        boolean groupByCondInTopN, boolean attribute) {
         this.fieldName = fieldName;
         this.columnName = columnName;
         this.type = type;
@@ -48,6 +49,7 @@ public SourceColumn(String fieldName, String columnName, Class<?> type, boolean
         this.fieldGetter = ClassMethodUtil.toGetMethod(fieldName);
         this.fieldSetter = ClassMethodUtil.toSetMethod(fieldName);
         this.groupByCondInTopN = groupByCondInTopN;
+        this.attribute = attribute;
     }
 
     public void setFieldName(String fieldName) {
diff --git a/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/parser/SourceColumnsFactory.java b/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/parser/SourceColumnsFactory.java
index fdd3c0d6d7b2..dadcbee20598 100644
--- a/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/parser/SourceColumnsFactory.java
+++ b/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/parser/SourceColumnsFactory.java
@@ -31,7 +31,7 @@ public static List<SourceColumn> getColumns(String source) {
         for (ScopeDefaultColumn defaultColumn : columns) {
             sourceColumns.add(
                 new SourceColumn(defaultColumn.getFieldName(), defaultColumn.getColumnName(), defaultColumn
-                    .getType(), defaultColumn.isID(), defaultColumn.getLength(), defaultColumn.isGroupByCondInTopN()));
+                    .getType(), defaultColumn.isID(), defaultColumn.getLength(), defaultColumn.isGroupByCondInTopN(), defaultColumn.isAttribute()));
         }
         return sourceColumns;
     }
diff --git a/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/util/OALClassGenerator.java b/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/util/OALClassGenerator.java
index d3f0af796e33..3d3ab1643b4d 100644
--- a/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/util/OALClassGenerator.java
+++ b/oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/rt/util/OALClassGenerator.java
@@ -428,6 +428,7 @@ private void buildDispatcherContext(AnalysisResult metricsStmt) {
             absent.setSourcePackage(oalDefine.getSourcePackage());
             absent.setSource(name);
             absent.setPackageName(name.toLowerCase());
+            absent.setSourceDecorator(metricsStmt.getSourceDecorator());
             return absent;
         });
         metricsStmt.setMetricsClassPackage(oalDefine.getDynamicMetricsClassPackage());
diff --git a/oap-server/oal-rt/src/main/resources/code-templates/dispatcher/doMetrics.ftl b/oap-server/oal-rt/src/main/resources/code-templates/dispatcher/doMetrics.ftl
index bcbb7f75090e..5d82d001973b 100644
--- a/oap-server/oal-rt/src/main/resources/code-templates/dispatcher/doMetrics.ftl
+++ b/oap-server/oal-rt/src/main/resources/code-templates/dispatcher/doMetrics.ftl
@@ -9,9 +9,16 @@ private void do${metricsName}(${sourcePackage}${from.sourceName} source) {
 </#if>
 
 ${metricsClassPackage}${metricsName}Metrics metrics = new ${metricsClassPackage}${metricsName}Metrics();
+<#if sourceDecorator??>
+    source.decorate("${sourceDecorator}");
+</#if>
 metrics.setTimeBucket(source.getTimeBucket());
 <#list fieldsFromSource as field>
-    metrics.${field.fieldSetter}(source.${field.fieldGetter}());
+    <#if field.attribute && !sourceDecorator??>
+        <#--Metrics share the source instance, do not process attributes unless added decorator func-->
+    <#else>
+        metrics.${field.fieldSetter}(source.${field.fieldGetter}());
+    </#if>
 </#list>
 metrics.${entryMethod.methodName}(
 <#list entryMethod.argsExpressions as arg>
diff --git a/oap-server/oal-rt/src/main/resources/code-templates/metrics/deserialize.ftl b/oap-server/oal-rt/src/main/resources/code-templates/metrics/deserialize.ftl
index 455634f5206e..8063915f8df0 100644
--- a/oap-server/oal-rt/src/main/resources/code-templates/metrics/deserialize.ftl
+++ b/oap-server/oal-rt/src/main/resources/code-templates/metrics/deserialize.ftl
@@ -1,6 +1,8 @@
 public void deserialize(org.apache.skywalking.oap.server.core.remote.grpc.proto.RemoteData remoteData) {
 <#list serializeFields.stringFields as field>
-    ${field.setter}(remoteData.getDataStrings(${field?index}));
+    if (remoteData.getDataStrings(${field?index}) != "") {
+        ${field.setter}(remoteData.getDataStrings(${field?index}));
+    }
 </#list>
 
 <#list serializeFields.longFields as field>
@@ -19,4 +21,4 @@ public void deserialize(org.apache.skywalking.oap.server.core.remote.grpc.proto.
     ${field.setter}(new ${field.fieldType}(remoteData.getDataObjectStrings(${field?index})));
 </#list>
 
-}
\ No newline at end of file
+}
diff --git a/oap-server/oal-rt/src/main/resources/code-templates/metrics/serialize.ftl b/oap-server/oal-rt/src/main/resources/code-templates/metrics/serialize.ftl
index 5a6f92446da2..0fd9fdcd2273 100644
--- a/oap-server/oal-rt/src/main/resources/code-templates/metrics/serialize.ftl
+++ b/oap-server/oal-rt/src/main/resources/code-templates/metrics/serialize.ftl
@@ -1,7 +1,7 @@
 public org.apache.skywalking.oap.server.core.remote.grpc.proto.RemoteData.Builder serialize() {
 org.apache.skywalking.oap.server.core.remote.grpc.proto.RemoteData.Builder remoteBuilder = org.apache.skywalking.oap.server.core.remote.grpc.proto.RemoteData.newBuilder();
 <#list serializeFields.stringFields as field>
-    remoteBuilder.addDataStrings(${field.getter}());
+    remoteBuilder.addDataStrings(${field.getter}() == null ? "" : ${field.getter}());
 </#list>
 
 <#list serializeFields.longFields as field>
@@ -21,4 +21,4 @@ org.apache.skywalking.oap.server.core.remote.grpc.proto.RemoteData.Builder remot
 </#list>
 
 return remoteBuilder;
-}
\ No newline at end of file
+}
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/CoreModuleProvider.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/CoreModuleProvider.java
index 3abbca27799b..e51f32fa4ac6 100755
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/CoreModuleProvider.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/CoreModuleProvider.java
@@ -380,9 +380,6 @@ public void start() throws ModuleStartException {
             moduleConfig.getMaxHttpUrisNumberPerService()
         );
 
-        // Disable OAL script has higher priority
-        oalEngineLoaderService.load(DisableOALDefine.INSTANCE);
-
         try {
             receiver.scan();
             annotationScan.scan();
@@ -390,6 +387,9 @@ public void start() throws ModuleStartException {
             throw new ModuleStartException(e.getMessage(), e);
         }
 
+        // Disable OAL script has higher priority
+        oalEngineLoaderService.load(DisableOALDefine.INSTANCE);
+
         Address gRPCServerInstanceAddress = new Address(moduleConfig.getGRPCHost(), moduleConfig.getGRPCPort(), true);
         TelemetryRelatedContext.INSTANCE.setId(gRPCServerInstanceAddress.toString());
         ClusterCoordinator coordinator = this.getManager()
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/DispatcherManager.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/DispatcherManager.java
index 100f423c6431..52e9a274ad6b 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/DispatcherManager.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/DispatcherManager.java
@@ -18,9 +18,6 @@
 
 package org.apache.skywalking.oap.server.core.analysis;
 
-import com.google.common.collect.ImmutableSet;
-import com.google.common.reflect.ClassPath;
-import java.io.IOException;
 import java.lang.reflect.Modifier;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
@@ -63,22 +60,6 @@ public void forward(ISource source) {
         }
     }
 
-    /**
-     * Scan all classes under `org.apache.skywalking` package,
-     * <p>
-     * If it implement {@link org.apache.skywalking.oap.server.core.analysis.SourceDispatcher}, then, it will be added
-     * into this DispatcherManager based on the Source definition.
-     */
-    public void scan() throws IOException, IllegalAccessException, InstantiationException {
-        ClassPath classpath = ClassPath.from(this.getClass().getClassLoader());
-        ImmutableSet<ClassPath.ClassInfo> classes = classpath.getTopLevelClassesRecursive("org.apache.skywalking");
-        for (ClassPath.ClassInfo classInfo : classes) {
-            Class<?> aClass = classInfo.load();
-
-            addIfAsSourceDispatcher(aClass);
-        }
-    }
-
     @Override
     public void addIfAsSourceDispatcher(Class aClass) throws IllegalAccessException, InstantiationException {
         if (!aClass.isInterface() && !Modifier.isAbstract(
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/ISourceDecorator.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/ISourceDecorator.java
new file mode 100644
index 000000000000..0ca26a22a312
--- /dev/null
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/ISourceDecorator.java
@@ -0,0 +1,39 @@
+/*
+ * 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.server.core.analysis;
+
+import org.apache.skywalking.oap.server.core.source.ISource;
+
+/**
+ * Source decorate extension for OAL, the implement class should be under the package `org.apache.skywalking`.
+ * @param <SOURCE> The source type to be decorated, for now, only support {@link org.apache.skywalking.oap.server.core.source}
+ */
+public interface ISourceDecorator<SOURCE extends ISource> {
+    /**
+     * The scope of the source which will be decorated.
+     * It's defined in the {@link org.apache.skywalking.oap.server.core.source.DefaultScopeDefine}
+     */
+    int getSourceScope();
+
+    /**
+     * Decorate the source, such as fill the Service attr0...attr4 through the original Service fields for store and query.
+     * @param source The source instance to be decorated
+     */
+    void decorate(SOURCE source);
+}
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/SourceDecoratorManager.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/SourceDecoratorManager.java
new file mode 100644
index 000000000000..25b9319a2692
--- /dev/null
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/SourceDecoratorManager.java
@@ -0,0 +1,65 @@
+/*
+ * 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.server.core.analysis;
+
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.Map;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.oap.server.core.UnexpectedException;
+import org.apache.skywalking.oap.server.core.source.ISource;
+
+@Slf4j
+public class SourceDecoratorManager {
+    public static final Map<String, ISourceDecorator<ISource>> DECORATOR_MAP = new HashMap<>();
+
+    public void addIfAsSourceDecorator(Class<?> aClass) throws IllegalAccessException, InstantiationException {
+        if (!aClass.isInterface() && !Modifier.isAbstract(
+            aClass.getModifiers()) && ISourceDecorator.class.isAssignableFrom(aClass)) {
+            Type[] genericInterfaces = aClass.getGenericInterfaces();
+            for (Type genericInterface : genericInterfaces) {
+                ParameterizedType anInterface = (ParameterizedType) genericInterface;
+                if (anInterface.getRawType().getTypeName().equals(ISourceDecorator.class.getName())) {
+                    Type[] arguments = anInterface.getActualTypeArguments();
+
+                    if (arguments.length != 1) {
+                        throw new UnexpectedException("unexpected type argument number, class " + aClass.getName());
+                    }
+                    Type argument = arguments[0];
+
+                    Object source = ((Class<?>) argument).newInstance();
+
+                    if (!ISource.class.isAssignableFrom(source.getClass())) {
+                        throw new UnexpectedException(
+                            "unexpected type argument of class " + aClass.getName() + ", should be `org.apache.skywalking.oap.server.core.source.Source`. ");
+                    }
+                    ISourceDecorator<ISource> decorator = (ISourceDecorator) aClass.newInstance();
+                    ISourceDecorator<ISource> exist = DECORATOR_MAP.put(aClass.getSimpleName(), decorator);
+                    if (exist != null) {
+                        throw new IllegalStateException("Deprecated decorator: " + aClass.getName() + " the class simple name is already used.");
+                    }
+                    log.info("Decorator {} is added into DefaultScopeDefine {}.", decorator.getClass()
+                                                                                                .getName(), ((ISource) source).scope());
+                }
+            }
+        }
+    }
+}
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/Meter.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/Meter.java
index 7ce449157a68..8ba687820402 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/Meter.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/Meter.java
@@ -18,10 +18,13 @@
 
 package org.apache.skywalking.oap.server.core.analysis.meter;
 
+import lombok.Getter;
+import lombok.Setter;
 import org.apache.skywalking.oap.server.core.analysis.metrics.Metrics;
 import org.apache.skywalking.oap.server.core.analysis.metrics.MetricsMetaInfo;
 import org.apache.skywalking.oap.server.core.analysis.metrics.WithMetadata;
 import org.apache.skywalking.oap.server.core.source.DefaultScopeDefine;
+import org.apache.skywalking.oap.server.core.storage.annotation.Column;
 
 /**
  * Meter is the abstract parent for all {@link org.apache.skywalking.oap.server.core.analysis.meter.function.MeterFunction} annotated functions.
@@ -30,8 +33,39 @@
  * @since 9.0.0
  */
 public abstract class Meter extends Metrics implements WithMetadata {
+    protected static final String ATTR0 = "attr0";
+    protected static final String ATTR1 = "attr1";
+    protected static final String ATTR2 = "attr2";
+    protected static final String ATTR3 = "attr3";
+    protected static final String ATTR4 = "attr4";
+
     private MetricsMetaInfo metadata = new MetricsMetaInfo("UNKNOWN", DefaultScopeDefine.UNKNOWN);
 
+    @Setter
+    @Getter
+    @Column(name = ATTR0)
+    private String attr0;
+
+    @Setter
+    @Getter
+    @Column(name = ATTR1)
+    private String attr1;
+
+    @Setter
+    @Getter
+    @Column(name = ATTR2)
+    private String attr2;
+
+    @Setter
+    @Getter
+    @Column(name = ATTR3)
+    private String attr3;
+
+    @Setter
+    @Getter
+    @Column(name = ATTR4)
+    private String attr4;
+
     /**
      * @return entity ID to represent this metric object. Typically, meter function should have a String type field, named entityId.
      * See {@link org.apache.skywalking.oap.server.core.analysis.meter.function.avg.AvgFunction#getEntityId()} as an example.
@@ -54,4 +88,18 @@ public MetricsMetaInfo getMeta() {
         this.metadata.setId(this.getEntityId());
         return metadata;
     }
+
+    /**
+     * Decorate the metric with the entity attributes.
+     * Only single value metrics can be decorated.
+     * Because we only support query the decorated condition in top_n query for now.
+     * @param entity The metric entity
+     */
+    protected void decorate(MeterEntity entity) {
+        attr0 = entity.getAttr0();
+        attr1 = entity.getAttr1();
+        attr2 = entity.getAttr2();
+        attr3 = entity.getAttr3();
+        attr4 = entity.getAttr4();
+    }
 }
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/MeterEntity.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/MeterEntity.java
index ef4aed500c00..073981d36a1d 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/MeterEntity.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/MeterEntity.java
@@ -20,6 +20,7 @@
 
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
+import lombok.Setter;
 import lombok.ToString;
 import java.util.Map;
 import org.apache.skywalking.oap.server.core.UnexpectedException;
@@ -50,6 +51,16 @@ public class MeterEntity {
     private DetectPoint detectPoint;
     private Layer layer;
     private int componentId;
+    @Setter
+    private String attr0;
+    @Setter
+    private String attr1;
+    @Setter
+    private String attr2;
+    @Setter
+    private String attr3;
+    @Setter
+    private String attr4;
 
     private MeterEntity() {
 
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/avg/AvgFunction.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/avg/AvgFunction.java
index caba0d7c6d64..3f96cf79e5e6 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/avg/AvgFunction.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/avg/AvgFunction.java
@@ -22,6 +22,7 @@
 import lombok.Getter;
 import lombok.Setter;
 import lombok.ToString;
+import org.apache.skywalking.oap.server.core.Const;
 import org.apache.skywalking.oap.server.core.UnexpectedException;
 import org.apache.skywalking.oap.server.core.analysis.manual.instance.InstanceTraffic;
 import org.apache.skywalking.oap.server.core.analysis.meter.Meter;
@@ -37,6 +38,7 @@
 import org.apache.skywalking.oap.server.core.storage.type.Convert2Entity;
 import org.apache.skywalking.oap.server.core.storage.type.Convert2Storage;
 import org.apache.skywalking.oap.server.core.storage.type.StorageBuilder;
+import org.apache.skywalking.oap.server.library.util.StringUtil;
 
 @MeterFunction(functionName = "avg")
 @ToString
@@ -102,6 +104,12 @@ public Metrics toHour() {
         metrics.setServiceId(getServiceId());
         metrics.setSummation(getSummation());
         metrics.setCount(getCount());
+
+        metrics.setAttr0(getAttr0());
+        metrics.setAttr1(getAttr1());
+        metrics.setAttr2(getAttr2());
+        metrics.setAttr3(getAttr3());
+        metrics.setAttr4(getAttr4());
         return metrics;
     }
 
@@ -113,6 +121,12 @@ public Metrics toDay() {
         metrics.setServiceId(getServiceId());
         metrics.setSummation(getSummation());
         metrics.setCount(getCount());
+
+        metrics.setAttr0(getAttr0());
+        metrics.setAttr1(getAttr1());
+        metrics.setAttr2(getAttr2());
+        metrics.setAttr3(getAttr3());
+        metrics.setAttr4(getAttr4());
         return metrics;
     }
 
@@ -129,6 +143,26 @@ public void deserialize(final RemoteData remoteData) {
 
         this.entityId = remoteData.getDataStrings(0);
         this.serviceId = remoteData.getDataStrings(1);
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(2))) {
+            setAttr0(remoteData.getDataStrings(2));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(3))) {
+            setAttr1(remoteData.getDataStrings(3));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(4))) {
+            setAttr2(remoteData.getDataStrings(4));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(5))) {
+            setAttr3(remoteData.getDataStrings(5));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(6))) {
+            setAttr4(remoteData.getDataStrings(6));
+        }
     }
 
     @Override
@@ -141,6 +175,11 @@ public RemoteData.Builder serialize() {
         remoteBuilder.addDataStrings(entityId);
         remoteBuilder.addDataStrings(serviceId);
 
+        remoteBuilder.addDataStrings(getAttr0() == null ? Const.EMPTY_STRING : getAttr0());
+        remoteBuilder.addDataStrings(getAttr1() == null ? Const.EMPTY_STRING : getAttr1());
+        remoteBuilder.addDataStrings(getAttr2() == null ? Const.EMPTY_STRING : getAttr2());
+        remoteBuilder.addDataStrings(getAttr3() == null ? Const.EMPTY_STRING : getAttr3());
+        remoteBuilder.addDataStrings(getAttr4() == null ? Const.EMPTY_STRING : getAttr4());
         return remoteBuilder;
     }
 
@@ -153,6 +192,7 @@ protected StorageID id0() {
 
     @Override
     public void accept(final MeterEntity entity, final Long value) {
+        decorate(entity);
         this.entityId = entity.id();
         this.serviceId = entity.serviceId();
         this.summation += value;
@@ -179,6 +219,12 @@ public AcceptableValue<Long> createNew() {
             metrics.setTimeBucket(((Number) converter.get(TIME_BUCKET)).longValue());
             metrics.setServiceId((String) converter.get(InstanceTraffic.SERVICE_ID));
             metrics.setEntityId((String) converter.get(ENTITY_ID));
+
+            metrics.setAttr0((String) converter.get(ATTR0));
+            metrics.setAttr1((String) converter.get(ATTR1));
+            metrics.setAttr2((String) converter.get(ATTR2));
+            metrics.setAttr3((String) converter.get(ATTR3));
+            metrics.setAttr4((String) converter.get(ATTR4));
             return metrics;
         }
 
@@ -190,6 +236,12 @@ public void entity2Storage(final AvgFunction storageData, final Convert2Storage
             converter.accept(TIME_BUCKET, storageData.getTimeBucket());
             converter.accept(InstanceTraffic.SERVICE_ID, storageData.getServiceId());
             converter.accept(ENTITY_ID, storageData.getEntityId());
+
+            converter.accept(ATTR0, storageData.getAttr0());
+            converter.accept(ATTR1, storageData.getAttr1());
+            converter.accept(ATTR2, storageData.getAttr2());
+            converter.accept(ATTR3, storageData.getAttr3());
+            converter.accept(ATTR4, storageData.getAttr4());
         }
     }
 
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/max/MaxFunction.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/max/MaxFunction.java
index 03c2163168d9..ff04af66ef94 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/max/MaxFunction.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/max/MaxFunction.java
@@ -21,6 +21,7 @@
 import lombok.Getter;
 import lombok.Setter;
 import lombok.ToString;
+import org.apache.skywalking.oap.server.core.Const;
 import org.apache.skywalking.oap.server.core.UnexpectedException;
 import org.apache.skywalking.oap.server.core.analysis.manual.instance.InstanceTraffic;
 import org.apache.skywalking.oap.server.core.analysis.meter.Meter;
@@ -38,6 +39,7 @@
 import org.apache.skywalking.oap.server.core.storage.type.StorageBuilder;
 
 import java.util.Objects;
+import org.apache.skywalking.oap.server.library.util.StringUtil;
 
 @MeterFunction(functionName = "max")
 @ToString
@@ -67,6 +69,7 @@ public abstract class MaxFunction extends Meter implements AcceptableValue<Long>
 
     @Override
     public void accept(MeterEntity entity, Long value) {
+        decorate(entity);
         setEntityId(entity.id());
         setServiceId(entity.serviceId());
         if (this.value < value) {
@@ -95,6 +98,12 @@ public Metrics toHour() {
         metrics.setTimeBucket(toTimeBucketInHour());
         metrics.setServiceId(getServiceId());
         metrics.setValue(getValue());
+
+        metrics.setAttr0(getAttr0());
+        metrics.setAttr1(getAttr1());
+        metrics.setAttr2(getAttr2());
+        metrics.setAttr3(getAttr3());
+        metrics.setAttr4(getAttr4());
         return metrics;
     }
 
@@ -105,6 +114,12 @@ public Metrics toDay() {
         metrics.setTimeBucket(toTimeBucketInDay());
         metrics.setServiceId(getServiceId());
         metrics.setValue(getValue());
+
+        metrics.setAttr0(getAttr0());
+        metrics.setAttr1(getAttr1());
+        metrics.setAttr2(getAttr2());
+        metrics.setAttr3(getAttr3());
+        metrics.setAttr4(getAttr4());
         return metrics;
     }
 
@@ -119,6 +134,26 @@ public void deserialize(RemoteData remoteData) {
         setServiceId(remoteData.getDataStrings(1));
         setTimeBucket(remoteData.getDataLongs(1));
         setValue(remoteData.getDataLongs(0));
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(2))) {
+            setAttr0(remoteData.getDataStrings(2));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(3))) {
+            setAttr1(remoteData.getDataStrings(3));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(4))) {
+            setAttr2(remoteData.getDataStrings(4));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(5))) {
+            setAttr3(remoteData.getDataStrings(5));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(6))) {
+            setAttr4(remoteData.getDataStrings(6));
+        }
     }
 
     @Override
@@ -128,6 +163,12 @@ public RemoteData.Builder serialize() {
         remoteBuilder.addDataLongs(getTimeBucket());
         remoteBuilder.addDataStrings(getEntityId());
         remoteBuilder.addDataStrings(getServiceId());
+
+        remoteBuilder.addDataStrings(getAttr0() == null ? Const.EMPTY_STRING : getAttr0());
+        remoteBuilder.addDataStrings(getAttr1() == null ? Const.EMPTY_STRING : getAttr1());
+        remoteBuilder.addDataStrings(getAttr2() == null ? Const.EMPTY_STRING : getAttr2());
+        remoteBuilder.addDataStrings(getAttr3() == null ? Const.EMPTY_STRING : getAttr3());
+        remoteBuilder.addDataStrings(getAttr4() == null ? Const.EMPTY_STRING : getAttr4());
         return remoteBuilder;
     }
 
@@ -157,6 +198,12 @@ public AcceptableValue<Long> createNew() {
             metrics.setServiceId((String) converter.get(InstanceTraffic.SERVICE_ID));
             metrics.setTimeBucket(((Number) converter.get(TIME_BUCKET)).longValue());
             metrics.setValue(((Number) converter.get(VALUE)).longValue());
+
+            metrics.setAttr0((String) converter.get(ATTR0));
+            metrics.setAttr1((String) converter.get(ATTR1));
+            metrics.setAttr2((String) converter.get(ATTR2));
+            metrics.setAttr3((String) converter.get(ATTR3));
+            metrics.setAttr4((String) converter.get(ATTR4));
             return metrics;
         }
 
@@ -166,6 +213,12 @@ public void entity2Storage(final MaxFunction storageData, final Convert2Storage
             converter.accept(InstanceTraffic.SERVICE_ID, storageData.getServiceId());
             converter.accept(TIME_BUCKET, storageData.getTimeBucket());
             converter.accept(VALUE, storageData.getValue());
+
+            converter.accept(ATTR0, storageData.getAttr0());
+            converter.accept(ATTR1, storageData.getAttr1());
+            converter.accept(ATTR2, storageData.getAttr2());
+            converter.accept(ATTR3, storageData.getAttr3());
+            converter.accept(ATTR4, storageData.getAttr4());
         }
     }
 
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/min/MinFunction.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/min/MinFunction.java
index 3a3651c3c198..edbae2b9d238 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/min/MinFunction.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/min/MinFunction.java
@@ -21,6 +21,7 @@
 import lombok.Getter;
 import lombok.Setter;
 import lombok.ToString;
+import org.apache.skywalking.oap.server.core.Const;
 import org.apache.skywalking.oap.server.core.UnexpectedException;
 import org.apache.skywalking.oap.server.core.analysis.manual.instance.InstanceTraffic;
 import org.apache.skywalking.oap.server.core.analysis.meter.Meter;
@@ -38,6 +39,7 @@
 import org.apache.skywalking.oap.server.core.storage.type.StorageBuilder;
 
 import java.util.Objects;
+import org.apache.skywalking.oap.server.library.util.StringUtil;
 
 @MeterFunction(functionName = "min")
 @ToString
@@ -67,6 +69,7 @@ public abstract class MinFunction extends Meter implements AcceptableValue<Long>
 
     @Override
     public void accept(MeterEntity entity, Long value) {
+        decorate(entity);
         setEntityId(entity.id());
         setServiceId(entity.serviceId());
 
@@ -96,6 +99,12 @@ public Metrics toHour() {
         metrics.setTimeBucket(toTimeBucketInHour());
         metrics.setServiceId(getServiceId());
         metrics.setValue(getValue());
+
+        metrics.setAttr0(getAttr0());
+        metrics.setAttr1(getAttr1());
+        metrics.setAttr2(getAttr2());
+        metrics.setAttr3(getAttr3());
+        metrics.setAttr4(getAttr4());
         return metrics;
     }
 
@@ -106,6 +115,12 @@ public Metrics toDay() {
         metrics.setTimeBucket(toTimeBucketInDay());
         metrics.setServiceId(getServiceId());
         metrics.setValue(getValue());
+
+        metrics.setAttr0(getAttr0());
+        metrics.setAttr1(getAttr1());
+        metrics.setAttr2(getAttr2());
+        metrics.setAttr3(getAttr3());
+        metrics.setAttr4(getAttr4());
         return metrics;
     }
 
@@ -120,6 +135,26 @@ public void deserialize(RemoteData remoteData) {
         setServiceId(remoteData.getDataStrings(1));
         setTimeBucket(remoteData.getDataLongs(1));
         setValue(remoteData.getDataLongs(0));
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(2))) {
+            setAttr0(remoteData.getDataStrings(2));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(3))) {
+            setAttr1(remoteData.getDataStrings(3));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(4))) {
+            setAttr2(remoteData.getDataStrings(4));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(5))) {
+            setAttr3(remoteData.getDataStrings(5));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(6))) {
+            setAttr4(remoteData.getDataStrings(6));
+        }
     }
 
     @Override
@@ -129,6 +164,12 @@ public RemoteData.Builder serialize() {
         remoteBuilder.addDataLongs(getTimeBucket());
         remoteBuilder.addDataStrings(getEntityId());
         remoteBuilder.addDataStrings(getServiceId());
+
+        remoteBuilder.addDataStrings(getAttr0() == null ? Const.EMPTY_STRING : getAttr0());
+        remoteBuilder.addDataStrings(getAttr1() == null ? Const.EMPTY_STRING : getAttr1());
+        remoteBuilder.addDataStrings(getAttr2() == null ? Const.EMPTY_STRING : getAttr2());
+        remoteBuilder.addDataStrings(getAttr3() == null ? Const.EMPTY_STRING : getAttr3());
+        remoteBuilder.addDataStrings(getAttr4() == null ? Const.EMPTY_STRING : getAttr4());
         return remoteBuilder;
     }
 
@@ -158,6 +199,12 @@ public AcceptableValue<Long> createNew() {
             metrics.setServiceId((String) converter.get(InstanceTraffic.SERVICE_ID));
             metrics.setTimeBucket(((Number) converter.get(TIME_BUCKET)).longValue());
             metrics.setValue(((Number) converter.get(VALUE)).longValue());
+
+            metrics.setAttr0((String) converter.get(ATTR0));
+            metrics.setAttr1((String) converter.get(ATTR1));
+            metrics.setAttr2((String) converter.get(ATTR2));
+            metrics.setAttr3((String) converter.get(ATTR3));
+            metrics.setAttr4((String) converter.get(ATTR4));
             return metrics;
         }
 
@@ -167,6 +214,12 @@ public void entity2Storage(final MinFunction storageData, final Convert2Storage
             converter.accept(InstanceTraffic.SERVICE_ID, storageData.getServiceId());
             converter.accept(TIME_BUCKET, storageData.getTimeBucket());
             converter.accept(VALUE, storageData.getValue());
+
+            converter.accept(ATTR0, storageData.getAttr0());
+            converter.accept(ATTR1, storageData.getAttr1());
+            converter.accept(ATTR2, storageData.getAttr2());
+            converter.accept(ATTR3, storageData.getAttr3());
+            converter.accept(ATTR4, storageData.getAttr4());
         }
     }
 
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sum/SumFunction.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sum/SumFunction.java
index d42b35150618..e3004af6f4b0 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sum/SumFunction.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sum/SumFunction.java
@@ -22,6 +22,7 @@
 import lombok.Getter;
 import lombok.Setter;
 import lombok.ToString;
+import org.apache.skywalking.oap.server.core.Const;
 import org.apache.skywalking.oap.server.core.UnexpectedException;
 import org.apache.skywalking.oap.server.core.analysis.manual.instance.InstanceTraffic;
 import org.apache.skywalking.oap.server.core.analysis.meter.Meter;
@@ -37,6 +38,7 @@
 import org.apache.skywalking.oap.server.core.storage.type.Convert2Entity;
 import org.apache.skywalking.oap.server.core.storage.type.Convert2Storage;
 import org.apache.skywalking.oap.server.core.storage.type.StorageBuilder;
+import org.apache.skywalking.oap.server.library.util.StringUtil;
 
 @ToString
 @MeterFunction(functionName = "sum")
@@ -78,6 +80,12 @@ public Metrics toHour() {
         metrics.setTimeBucket(toTimeBucketInHour());
         metrics.setServiceId(getServiceId());
         metrics.setValue(getValue());
+
+        metrics.setAttr0(getAttr0());
+        metrics.setAttr1(getAttr1());
+        metrics.setAttr2(getAttr2());
+        metrics.setAttr3(getAttr3());
+        metrics.setAttr4(getAttr4());
         return metrics;
     }
 
@@ -88,6 +96,12 @@ public Metrics toDay() {
         metrics.setTimeBucket(toTimeBucketInDay());
         metrics.setServiceId(getServiceId());
         metrics.setValue(getValue());
+
+        metrics.setAttr0(getAttr0());
+        metrics.setAttr1(getAttr1());
+        metrics.setAttr2(getAttr2());
+        metrics.setAttr3(getAttr3());
+        metrics.setAttr4(getAttr4());
         return metrics;
     }
 
@@ -103,6 +117,26 @@ public void deserialize(final RemoteData remoteData) {
 
         setEntityId(remoteData.getDataStrings(0));
         setServiceId(remoteData.getDataStrings(1));
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(2))) {
+            setAttr0(remoteData.getDataStrings(2));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(3))) {
+            setAttr1(remoteData.getDataStrings(3));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(4))) {
+            setAttr2(remoteData.getDataStrings(4));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(5))) {
+            setAttr3(remoteData.getDataStrings(5));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(6))) {
+            setAttr4(remoteData.getDataStrings(6));
+        }
     }
 
     @Override
@@ -115,6 +149,11 @@ public RemoteData.Builder serialize() {
         remoteBuilder.addDataStrings(getEntityId());
         remoteBuilder.addDataStrings(getServiceId());
 
+        remoteBuilder.addDataStrings(getAttr0() == null ? Const.EMPTY_STRING : getAttr0());
+        remoteBuilder.addDataStrings(getAttr1() == null ? Const.EMPTY_STRING : getAttr1());
+        remoteBuilder.addDataStrings(getAttr2() == null ? Const.EMPTY_STRING : getAttr2());
+        remoteBuilder.addDataStrings(getAttr3() == null ? Const.EMPTY_STRING : getAttr3());
+        remoteBuilder.addDataStrings(getAttr4() == null ? Const.EMPTY_STRING : getAttr4());
         return remoteBuilder;
     }
 
@@ -127,6 +166,7 @@ protected StorageID id0() {
 
     @Override
     public void accept(final MeterEntity entity, final Long value) {
+        decorate(entity);
         setEntityId(entity.id());
         setServiceId(entity.serviceId());
         setValue(getValue() + value);
@@ -150,6 +190,12 @@ public AcceptableValue<Long> createNew() {
             metrics.setTimeBucket(((Number) converter.get(TIME_BUCKET)).longValue());
             metrics.setServiceId((String) converter.get(InstanceTraffic.SERVICE_ID));
             metrics.setEntityId((String) converter.get(ENTITY_ID));
+
+            metrics.setAttr0((String) converter.get(ATTR0));
+            metrics.setAttr1((String) converter.get(ATTR1));
+            metrics.setAttr2((String) converter.get(ATTR2));
+            metrics.setAttr3((String) converter.get(ATTR3));
+            metrics.setAttr4((String) converter.get(ATTR4));
             return metrics;
         }
 
@@ -159,6 +205,12 @@ public void entity2Storage(final SumFunction storageData, final Convert2Storage
             converter.accept(TIME_BUCKET, storageData.getTimeBucket());
             converter.accept(InstanceTraffic.SERVICE_ID, storageData.getServiceId());
             converter.accept(ENTITY_ID, storageData.getEntityId());
+
+            converter.accept(ATTR0, storageData.getAttr0());
+            converter.accept(ATTR1, storageData.getAttr1());
+            converter.accept(ATTR2, storageData.getAttr2());
+            converter.accept(ATTR3, storageData.getAttr3());
+            converter.accept(ATTR4, storageData.getAttr4());
         }
     }
 
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sumpermin/SumPerMinFunction.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sumpermin/SumPerMinFunction.java
index 733bc8bdf4f2..8a7e2781879a 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sumpermin/SumPerMinFunction.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sumpermin/SumPerMinFunction.java
@@ -22,6 +22,7 @@
 import lombok.Getter;
 import lombok.Setter;
 import lombok.ToString;
+import org.apache.skywalking.oap.server.core.Const;
 import org.apache.skywalking.oap.server.core.UnexpectedException;
 import org.apache.skywalking.oap.server.core.analysis.manual.instance.InstanceTraffic;
 import org.apache.skywalking.oap.server.core.analysis.meter.Meter;
@@ -37,6 +38,7 @@
 import org.apache.skywalking.oap.server.core.storage.type.Convert2Entity;
 import org.apache.skywalking.oap.server.core.storage.type.Convert2Storage;
 import org.apache.skywalking.oap.server.core.storage.type.StorageBuilder;
+import org.apache.skywalking.oap.server.library.util.StringUtil;
 
 @ToString
 @MeterFunction(functionName = "sumPerMin")
@@ -86,6 +88,12 @@ public Metrics toHour() {
         metrics.setTimeBucket(toTimeBucketInHour());
         metrics.setServiceId(getServiceId());
         metrics.setTotal(getTotal());
+
+        metrics.setAttr0(getAttr0());
+        metrics.setAttr1(getAttr1());
+        metrics.setAttr2(getAttr2());
+        metrics.setAttr3(getAttr3());
+        metrics.setAttr4(getAttr4());
         return metrics;
     }
 
@@ -96,6 +104,12 @@ public Metrics toDay() {
         metrics.setTimeBucket(toTimeBucketInDay());
         metrics.setServiceId(getServiceId());
         metrics.setTotal(getTotal());
+
+        metrics.setAttr0(getAttr0());
+        metrics.setAttr1(getAttr1());
+        metrics.setAttr2(getAttr2());
+        metrics.setAttr3(getAttr3());
+        metrics.setAttr4(getAttr4());
         return metrics;
     }
 
@@ -111,6 +125,26 @@ public void deserialize(RemoteData remoteData) {
 
         setEntityId(remoteData.getDataStrings(0));
         setServiceId(remoteData.getDataStrings(1));
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(2))) {
+            setAttr0(remoteData.getDataStrings(2));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(3))) {
+            setAttr1(remoteData.getDataStrings(3));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(4))) {
+            setAttr2(remoteData.getDataStrings(4));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(5))) {
+            setAttr3(remoteData.getDataStrings(5));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(6))) {
+            setAttr4(remoteData.getDataStrings(6));
+        }
     }
 
     @Override
@@ -122,6 +156,12 @@ public RemoteData.Builder serialize() {
 
         remoteBuilder.addDataStrings(getEntityId());
         remoteBuilder.addDataStrings(getServiceId());
+
+        remoteBuilder.addDataStrings(getAttr0() == null ? Const.EMPTY_STRING : getAttr0());
+        remoteBuilder.addDataStrings(getAttr1() == null ? Const.EMPTY_STRING : getAttr1());
+        remoteBuilder.addDataStrings(getAttr2() == null ? Const.EMPTY_STRING : getAttr2());
+        remoteBuilder.addDataStrings(getAttr3() == null ? Const.EMPTY_STRING : getAttr3());
+        remoteBuilder.addDataStrings(getAttr4() == null ? Const.EMPTY_STRING : getAttr4());
         return remoteBuilder;
     }
 
@@ -134,6 +174,7 @@ protected StorageID id0() {
 
     @Override
     public void accept(MeterEntity entity, Long value) {
+        decorate(entity);
         setEntityId(entity.id());
         setServiceId(entity.serviceId());
         setTotal(getTotal() + value);
@@ -158,6 +199,12 @@ public AcceptableValue<Long> createNew() {
             metrics.setTimeBucket(((Number) converter.get(TIME_BUCKET)).longValue());
             metrics.setServiceId((String) converter.get(InstanceTraffic.SERVICE_ID));
             metrics.setEntityId((String) converter.get(ENTITY_ID));
+
+            metrics.setAttr0((String) converter.get(ATTR0));
+            metrics.setAttr1((String) converter.get(ATTR1));
+            metrics.setAttr2((String) converter.get(ATTR2));
+            metrics.setAttr3((String) converter.get(ATTR3));
+            metrics.setAttr4((String) converter.get(ATTR4));
             return metrics;
         }
 
@@ -168,6 +215,12 @@ public void entity2Storage(final SumPerMinFunction storageData, final Convert2St
             converter.accept(TIME_BUCKET, storageData.getTimeBucket());
             converter.accept(InstanceTraffic.SERVICE_ID, storageData.getServiceId());
             converter.accept(ENTITY_ID, storageData.getEntityId());
+
+            converter.accept(ATTR0, storageData.getAttr0());
+            converter.accept(ATTR1, storageData.getAttr1());
+            converter.accept(ATTR2, storageData.getAttr2());
+            converter.accept(ATTR3, storageData.getAttr3());
+            converter.accept(ATTR4, storageData.getAttr4());
         }
     }
 
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/metrics/Metrics.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/metrics/Metrics.java
index 6259fda365bf..439d66ff89cc 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/metrics/Metrics.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/metrics/Metrics.java
@@ -38,6 +38,7 @@
 public abstract class Metrics extends StreamData implements StorageData {
     public static final String ENTITY_ID = "entity_id";
     public static final String ID = "id";
+    public static final String ATTR_NAME_PREFIX = "attr";
 
     /**
      * Time attribute
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/query/input/TopNCondition.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/query/input/TopNCondition.java
index 33f2984277a5..a445e5161ec4 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/query/input/TopNCondition.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/query/input/TopNCondition.java
@@ -57,6 +57,12 @@ public class TopNCondition {
     private Scope scope;
     private int topN;
     private Order order;
+    /**
+     * @since 10.2.0
+     * Attributes for query condition, if the metrics support attributes from
+     * {@link org.apache.skywalking.oap.server.core.analysis.ISourceDecorator}.
+     */
+    private String[] attributes;
 
     /**
      * Sense Scope through metric name.
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/DefaultScopeDefine.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/DefaultScopeDefine.java
index 907717788281..647c2318914d 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/DefaultScopeDefine.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/DefaultScopeDefine.java
@@ -223,7 +223,7 @@ private static void addNewScope(ScopeDeclaration declaration, Class originalClas
         if (virtualColumn != null) {
             scopeDefaultColumns.add(
                 new ScopeDefaultColumn(virtualColumn.fieldName(), virtualColumn.columnName(), virtualColumn
-                    .type(), virtualColumn.isID(), virtualColumn.length(), false));
+                    .type(), virtualColumn.isID(), virtualColumn.length(), false, false));
         }
         Field[] scopeClassField = originalClass.getDeclaredFields();
         if (scopeClassField != null) {
@@ -235,7 +235,7 @@ private static void addNewScope(ScopeDeclaration declaration, Class originalClas
                         scopeDefaultColumns.add(
                             new ScopeDefaultColumn(
                                 field.getName(), definedByField.columnName(), field.getType(), false,
-                                definedByField.length(), definedByField.groupByCondInTopN()
+                                definedByField.length(), definedByField.groupByCondInTopN(), definedByField.isAttribute()
                             ));
                     }
                 }
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/ScopeDefaultColumn.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/ScopeDefaultColumn.java
index 5d2986e10257..71ca602d0e81 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/ScopeDefaultColumn.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/ScopeDefaultColumn.java
@@ -37,14 +37,16 @@ public class ScopeDefaultColumn {
     private boolean isID;
     private int length;
     private final boolean groupByCondInTopN;
+    private final boolean attribute;
 
-    public ScopeDefaultColumn(String fieldName, String columnName, Class<?> type, boolean isID, int length, boolean groupByCondInTopN) {
+    public ScopeDefaultColumn(String fieldName, String columnName, Class<?> type, boolean isID, int length, boolean groupByCondInTopN, boolean attribute) {
         this.fieldName = fieldName;
         this.columnName = columnName;
         this.type = type;
         this.isID = isID;
         this.length = length;
         this.groupByCondInTopN = groupByCondInTopN;
+        this.attribute = attribute;
     }
 
     @Target({ElementType.FIELD})
@@ -71,6 +73,14 @@ public ScopeDefaultColumn(String fieldName, String columnName, Class<?> type, bo
          * @since 9.5.0
          */
         boolean groupByCondInTopN() default false;
+
+        /**
+         * Indicate whether this column is an attribute.
+         * Attributes are optional fields, which are set by the source decorator and can be used for query conditions.
+         *
+         * @since 10.2.0
+         */
+        boolean isAttribute() default false;
     }
 
     @Target({ElementType.TYPE})
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/Service.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/Service.java
index 1306c140ccb5..0160c997f3ec 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/Service.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/Service.java
@@ -23,7 +23,9 @@
 import lombok.Getter;
 import lombok.Setter;
 import org.apache.skywalking.oap.server.core.analysis.IDManager;
+import org.apache.skywalking.oap.server.core.analysis.ISourceDecorator;
 import org.apache.skywalking.oap.server.core.analysis.Layer;
+import org.apache.skywalking.oap.server.core.analysis.SourceDecoratorManager;
 
 import static org.apache.skywalking.oap.server.core.source.DefaultScopeDefine.SERVICE;
 import static org.apache.skywalking.oap.server.core.source.DefaultScopeDefine.SERVICE_CATALOG_NAME;
@@ -82,8 +84,37 @@ public String getEntityId() {
     @Getter
     @Setter
     private SideCar sideCar = new SideCar();
+    @Getter
+    @Setter
+    @ScopeDefaultColumn.DefinedByField(columnName = "attr0", isAttribute = true)
+    private String attr0;
+    @Getter
+    @Setter
+    @ScopeDefaultColumn.DefinedByField(columnName = "attr1", isAttribute = true)
+    private String attr1;
+    @Getter
+    @Setter
+    @ScopeDefaultColumn.DefinedByField(columnName = "attr2", isAttribute = true)
+    private String attr2;
+    @Getter
+    @Setter
+    @ScopeDefaultColumn.DefinedByField(columnName = "attr3", isAttribute = true)
+    private String attr3;
+    @Getter
+    @Setter
+    @ScopeDefaultColumn.DefinedByField(columnName = "attr4", isAttribute = true)
+    private String attr4;
 
     public String getTag(String key) {
         return originalTags.get(key);
     }
+
+    /**
+     * Get the decorator through given name and invoke.
+     * @param decorator The decorator class simpleName.
+     */
+    public void decorate(String decorator) {
+        ISourceDecorator<ISource> sourceDecorator = SourceDecoratorManager.DECORATOR_MAP.get(decorator);
+        sourceDecorator.decorate(this);
+    }
 }
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/ServiceDecorator.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/ServiceDecorator.java
new file mode 100644
index 000000000000..93679dc700ec
--- /dev/null
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/ServiceDecorator.java
@@ -0,0 +1,37 @@
+/*
+ * 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.server.core.source;
+
+import org.apache.skywalking.oap.server.core.analysis.ISourceDecorator;
+
+public class ServiceDecorator implements ISourceDecorator<Service> {
+    @Override
+    public int getSourceScope() {
+        return DefaultScopeDefine.SERVICE;
+    }
+
+    /**
+     * Set the Layer name to attr0
+     * @param source The source instance to be decorated
+     */
+    @Override
+    public void decorate(final Service source) {
+        source.setAttr0(source.getLayer().name());
+    }
+}
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/SourceReceiverImpl.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/SourceReceiverImpl.java
index 1e01da819173..cb85fb0bf067 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/SourceReceiverImpl.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/SourceReceiverImpl.java
@@ -18,17 +18,24 @@
 
 package org.apache.skywalking.oap.server.core.source;
 
+import com.google.common.collect.ImmutableSet;
+import com.google.common.reflect.ClassPath;
 import java.io.IOException;
 import lombok.Getter;
 import org.apache.skywalking.oap.server.core.analysis.DispatcherDetectorListener;
 import org.apache.skywalking.oap.server.core.analysis.DispatcherManager;
+import org.apache.skywalking.oap.server.core.analysis.SourceDecoratorManager;
 
 public class SourceReceiverImpl implements SourceReceiver {
     @Getter
     private final DispatcherManager dispatcherManager;
 
+    @Getter
+    private final SourceDecoratorManager sourceDecoratorManager;
+
     public SourceReceiverImpl() {
         this.dispatcherManager = new DispatcherManager();
+        this.sourceDecoratorManager = new SourceDecoratorManager();
     }
 
     @Override
@@ -42,6 +49,12 @@ public DispatcherDetectorListener getDispatcherDetectorListener() {
     }
 
     public void scan() throws IOException, InstantiationException, IllegalAccessException {
-        dispatcherManager.scan();
+        ClassPath classpath = ClassPath.from(this.getClass().getClassLoader());
+        ImmutableSet<ClassPath.ClassInfo> classes = classpath.getTopLevelClassesRecursive("org.apache.skywalking");
+        for (ClassPath.ClassInfo classInfo : classes) {
+            Class<?> aClass = classInfo.load();
+            sourceDecoratorManager.addIfAsSourceDecorator(aClass);
+            dispatcherManager.addIfAsSourceDispatcher(aClass);
+        }
     }
 }
diff --git a/oap-server/server-query-plugin/query-graphql-plugin/src/main/java/org/apache/skywalking/oap/query/graphql/mqe/rt/MQEVisitor.java b/oap-server/server-query-plugin/query-graphql-plugin/src/main/java/org/apache/skywalking/oap/query/graphql/mqe/rt/MQEVisitor.java
index ae7933e2f072..b354f2edd6be 100644
--- a/oap-server/server-query-plugin/query-graphql-plugin/src/main/java/org/apache/skywalking/oap/query/graphql/mqe/rt/MQEVisitor.java
+++ b/oap-server/server-query-plugin/query-graphql-plugin/src/main/java/org/apache/skywalking/oap/query/graphql/mqe/rt/MQEVisitor.java
@@ -55,6 +55,7 @@
 import org.apache.skywalking.mqe.rt.MQEVisitorBase;
 import org.apache.skywalking.mqe.rt.type.ExpressionResult;
 import org.apache.skywalking.mqe.rt.type.ExpressionResultType;
+import org.apache.skywalking.oap.server.library.util.StringUtil;
 import org.joda.time.DateTime;
 
 import static org.apache.skywalking.oap.server.core.query.type.debugging.DebuggingTraceContext.TRACE_CONTEXT;
@@ -128,8 +129,21 @@ public ExpressionResult visitMetric(MQEParser.MetricContext ctx) {
                         if (topN <= 0) {
                             throw new IllegalExpressionException("TopN value must be > 0.");
                         }
+                        String[] attrsCondition = new String[5];
+                        if (parent.attributeList() != null) {
+                            for (MQEParser.AttributeContext attributeContext : parent.attributeList().attribute()) {
+                                String attrName = attributeContext.attributeName().getText();
+                                String attrValue = attributeContext.VALUE_STRING().getText();
+                                if (StringUtil.isNotBlank(attrValue)) {
+                                    String attrValueTrim = attrValue.substring(1, attrValue.length() - 1);
+                                    int index = Integer.parseInt(attrName.substring(attrName.length() - 1));
+                                    attrsCondition[index] = attrValueTrim;
+                                }
+                            }
+                        }
+
                         querySortMetrics(metricName, Integer.parseInt(parent.INTEGER().getText()),
-                                         Order.valueOf(parent.order().getText().toUpperCase()), result);
+                                         Order.valueOf(parent.order().getText().toUpperCase()), attrsCondition, result);
                     } else if (ctx.parent instanceof MQEParser.TrendOPContext) {
                         //trend query requires get previous data according to the trend range
                         MQEParser.TrendOPContext parent = (MQEParser.TrendOPContext) ctx.parent;
@@ -181,6 +195,7 @@ public ExpressionResult visitMetric(MQEParser.MetricContext ctx) {
     private void querySortMetrics(String metricName,
                                   int topN,
                                   Order order,
+                                  String[] attrs,
                                   ExpressionResult result) throws IOException {
         TopNCondition topNCondition = new TopNCondition();
         topNCondition.setName(metricName);
@@ -188,6 +203,8 @@ private void querySortMetrics(String metricName,
         topNCondition.setParentService(entity.getServiceName());
         topNCondition.setOrder(order);
         topNCondition.setNormal(entity.getNormal());
+        topNCondition.setAttributes(attrs);
+
         List<SelectedRecord> selectedRecords = getAggregationQueryService().sortMetrics(topNCondition, duration);
 
         List<MQEValue> mqeValueList = new ArrayList<>(selectedRecords.size());
diff --git a/oap-server/server-starter/src/main/resources/oal/core.oal b/oap-server/server-starter/src/main/resources/oal/core.oal
index a5ebcb18a291..8bd1bc924108 100755
--- a/oap-server/server-starter/src/main/resources/oal/core.oal
+++ b/oap-server/server-starter/src/main/resources/oal/core.oal
@@ -17,11 +17,11 @@
  */
 
 // Service scope metrics
-service_resp_time = from(Service.latency).longAvg();
-service_sla = from(Service.*).percent(status == true);
-service_cpm = from(Service.*).cpm();
+service_resp_time = from(Service.latency).longAvg().decorator("ServiceDecorator");
+service_sla = from(Service.*).percent(status == true).decorator("ServiceDecorator");
+service_cpm = from(Service.*).cpm().decorator("ServiceDecorator");
 service_percentile = from(Service.latency).percentile2(10); // Multiple values including p50, p75, p90, p95, p99
-service_apdex = from(Service.latency).apdex(name, status);
+service_apdex = from(Service.latency).apdex(name, status).decorator("ServiceDecorator");
 service_mq_consume_count = from(Service.*).filter(type == RequestType.MQ).count();
 service_mq_consume_latency = from((str->long)Service.tag["transmission.latency"]).filter(type == RequestType.MQ).filter(tag["transmission.latency"] != null).longAvg();
 
diff --git a/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/BanyanDBAggregationQueryDAO.java b/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/BanyanDBAggregationQueryDAO.java
index 6c21401d5d2e..5bfa5f17e181 100644
--- a/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/BanyanDBAggregationQueryDAO.java
+++ b/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/BanyanDBAggregationQueryDAO.java
@@ -33,6 +33,7 @@
 import org.apache.skywalking.oap.server.core.query.type.SelectedRecord;
 import org.apache.skywalking.oap.server.core.storage.query.IAggregationQueryDAO;
 import org.apache.skywalking.oap.server.library.util.CollectionUtils;
+import org.apache.skywalking.oap.server.library.util.StringUtil;
 import org.apache.skywalking.oap.server.storage.plugin.banyandb.stream.AbstractBanyanDBDAO;
 import org.apache.skywalking.oap.server.storage.plugin.banyandb.util.ByteUtil;
 
@@ -82,9 +83,9 @@ List<SelectedRecord> serverSideTopN(TopNCondition condition, MetadataRegistry.Sc
                                         TimestampRange timestampRange, List<KeyValue> additionalConditions) throws IOException {
         TopNQueryResponse resp = null;
         if (condition.getOrder() == Order.DES) {
-            resp = topNQueryDebuggable(schema, timestampRange, condition.getTopN(), AbstractQuery.Sort.DESC, additionalConditions);
+            resp = topNQueryDebuggable(schema, timestampRange, condition.getTopN(), AbstractQuery.Sort.DESC, additionalConditions, condition.getAttributes());
         } else {
-            resp = topNQueryDebuggable(schema, timestampRange, condition.getTopN(), AbstractQuery.Sort.ASC, additionalConditions);
+            resp = topNQueryDebuggable(schema, timestampRange, condition.getTopN(), AbstractQuery.Sort.ASC, additionalConditions, condition.getAttributes());
         }
         if (resp.size() == 0) {
             return Collections.emptyList();
@@ -122,6 +123,16 @@ protected void apply(MeasureQuery query) {
                                             additionalCondition.getValue()
                                     )));
                         }
+                        if (CollectionUtils.isNotEmpty(condition.getAttributes())) {
+                            for (int i = 0; i < condition.getAttributes().length; i++) {
+                                if (StringUtil.isNotEmpty(condition.getAttributes()[i])) {
+                                    query.and(eq(
+                                        Metrics.ATTR_NAME_PREFIX + i,
+                                        condition.getAttributes()[i]
+                                    ));
+                                }
+                            }
+                        }
                     }
                 });
 
diff --git a/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/stream/AbstractBanyanDBDAO.java b/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/stream/AbstractBanyanDBDAO.java
index fbf6077e0b6d..a81d8597ed6c 100644
--- a/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/stream/AbstractBanyanDBDAO.java
+++ b/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/stream/AbstractBanyanDBDAO.java
@@ -19,6 +19,7 @@
 package org.apache.skywalking.oap.server.storage.plugin.banyandb.stream;
 
 import com.google.gson.Gson;
+import java.util.Arrays;
 import org.apache.skywalking.banyandb.model.v1.BanyandbModel;
 import org.apache.skywalking.banyandb.v1.client.AbstractCriteria;
 import org.apache.skywalking.banyandb.v1.client.AbstractQuery;
@@ -34,11 +35,13 @@
 import org.apache.skywalking.banyandb.v1.client.TopNQuery;
 import org.apache.skywalking.banyandb.v1.client.TopNQueryResponse;
 import org.apache.skywalking.banyandb.v1.client.Trace;
+import org.apache.skywalking.oap.server.core.analysis.metrics.Metrics;
 import org.apache.skywalking.oap.server.core.query.type.KeyValue;
 import org.apache.skywalking.oap.server.core.query.type.debugging.DebuggingSpan;
 import org.apache.skywalking.oap.server.core.query.type.debugging.DebuggingTraceContext;
 import org.apache.skywalking.oap.server.core.storage.AbstractDAO;
 import org.apache.skywalking.oap.server.library.util.CollectionUtils;
+import org.apache.skywalking.oap.server.library.util.StringUtil;
 import org.apache.skywalking.oap.server.storage.plugin.banyandb.BanyanDBStorageClient;
 import org.apache.skywalking.oap.server.storage.plugin.banyandb.MetadataRegistry;
 import java.io.IOException;
@@ -120,18 +123,28 @@ protected StreamQueryResponse queryDebuggable(String modelName, Set<String> tags
         }
     }
 
-    protected TopNQueryResponse topN(MetadataRegistry.Schema schema, TimestampRange timestampRange, int number,
-                                     List<KeyValue> additionalConditions) throws IOException {
-        return topNQuery(schema, timestampRange, number, AbstractQuery.Sort.DESC, additionalConditions);
+    protected TopNQueryResponse topN(MetadataRegistry.Schema schema,
+                                     TimestampRange timestampRange,
+                                     int number,
+                                     List<KeyValue> additionalConditions,
+                                     String[] attributes) throws IOException {
+        return topNQuery(schema, timestampRange, number, AbstractQuery.Sort.DESC, additionalConditions, attributes);
     }
 
-    protected TopNQueryResponse bottomN(MetadataRegistry.Schema schema, TimestampRange timestampRange, int number,
-                                        List<KeyValue> additionalConditions) throws IOException {
-        return topNQuery(schema, timestampRange, number, AbstractQuery.Sort.ASC, additionalConditions);
+    protected TopNQueryResponse bottomN(MetadataRegistry.Schema schema,
+                                        TimestampRange timestampRange,
+                                        int number,
+                                        List<KeyValue> additionalConditions,
+                                        String[] attributes) throws IOException {
+        return topNQuery(schema, timestampRange, number, AbstractQuery.Sort.ASC, additionalConditions, attributes);
     }
 
-    protected TopNQueryResponse topNQueryDebuggable(MetadataRegistry.Schema schema, TimestampRange timestampRange, int number,
-                                          AbstractQuery.Sort sort, List<KeyValue> additionalConditions) throws IOException {
+    protected TopNQueryResponse topNQueryDebuggable(MetadataRegistry.Schema schema,
+                                                    TimestampRange timestampRange,
+                                                    int number,
+                                                    AbstractQuery.Sort sort,
+                                                    List<KeyValue> additionalConditions,
+                                                    String[] attributes) throws IOException {
         DebuggingTraceContext traceContext = DebuggingTraceContext.TRACE_CONTEXT.get();
         DebuggingSpan span = null;
         try {
@@ -148,10 +161,12 @@ protected TopNQueryResponse topNQueryDebuggable(MetadataRegistry.Schema schema,
                        .append(", Sort: ")
                        .append(sort)
                        .append(", AdditionalConditions: ")
-                       .append(additionalConditions);
+                       .append(additionalConditions)
+                       .append(", Attributes: ")
+                       .append(Arrays.toString(attributes));
                 span.setMsg(builder.toString());
             }
-            TopNQueryResponse response = topNQuery(schema, timestampRange, number, sort, additionalConditions);
+            TopNQueryResponse response = topNQuery(schema, timestampRange, number, sort, additionalConditions, attributes);
             if (traceContext != null && traceContext.isDumpStorageRsp()) {
                 builder.append("\n").append(" Response: ").append(new Gson().toJson(response.getTopNLists()));
                 span.setMsg(builder.toString());
@@ -164,19 +179,32 @@ protected TopNQueryResponse topNQueryDebuggable(MetadataRegistry.Schema schema,
         }
     }
 
-    private TopNQueryResponse topNQuery(MetadataRegistry.Schema schema, TimestampRange timestampRange, int number,
-                                        AbstractQuery.Sort sort, List<KeyValue> additionalConditions) throws IOException {
+    private TopNQueryResponse topNQuery(MetadataRegistry.Schema schema,
+                                        TimestampRange timestampRange,
+                                        int number,
+                                        AbstractQuery.Sort sort,
+                                        List<KeyValue> additionalConditions,
+                                        String[] attributes) throws IOException {
         final TopNQuery q = new TopNQuery(List.of(schema.getMetadata().getGroup()), schema.getTopNSpec().getName(),
                 timestampRange,
                 number, sort);
         q.setAggregationType(MeasureQuery.Aggregation.Type.MEAN);
+        List<PairQueryCondition<?>> conditions = new ArrayList<>();
         if (CollectionUtils.isNotEmpty(additionalConditions)) {
-            List<PairQueryCondition<?>> conditions = new ArrayList<>(additionalConditions.size());
             for (final KeyValue kv : additionalConditions) {
                 conditions.add(PairQueryCondition.StringQueryCondition.eq(kv.getKey(), kv.getValue()));
             }
-            q.setConditions(conditions);
         }
+        if (CollectionUtils.isNotEmpty(attributes)) {
+            for (int i = 0; i < attributes.length; i++) {
+                if (StringUtil.isNotEmpty(attributes[i])) {
+                    conditions.add(
+                        PairQueryCondition.StringQueryCondition.eq(Metrics.ATTR_NAME_PREFIX + i, attributes[i]));
+                }
+            }
+        }
+        q.setConditions(conditions);
+
         return getClient().query(q);
     }
 
diff --git a/oap-server/server-storage-plugin/storage-elasticsearch-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/elasticsearch/query/AggregationQueryEsDAO.java b/oap-server/server-storage-plugin/storage-elasticsearch-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/elasticsearch/query/AggregationQueryEsDAO.java
index 3345e579616a..421466b41254 100644
--- a/oap-server/server-storage-plugin/storage-elasticsearch-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/elasticsearch/query/AggregationQueryEsDAO.java
+++ b/oap-server/server-storage-plugin/storage-elasticsearch-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/elasticsearch/query/AggregationQueryEsDAO.java
@@ -37,6 +37,7 @@
 import org.apache.skywalking.oap.server.core.storage.query.IAggregationQueryDAO;
 import org.apache.skywalking.oap.server.library.client.elasticsearch.ElasticSearchClient;
 import org.apache.skywalking.oap.server.library.util.CollectionUtils;
+import org.apache.skywalking.oap.server.library.util.StringUtil;
 import org.apache.skywalking.oap.server.storage.plugin.elasticsearch.base.EsDAO;
 import org.apache.skywalking.oap.server.storage.plugin.elasticsearch.base.IndexController;
 import org.apache.skywalking.oap.server.storage.plugin.elasticsearch.base.TimeRangeIndexNameGenerator;
@@ -60,27 +61,31 @@ public List<SelectedRecord> sortMetrics(final TopNCondition condition,
         final RangeQueryBuilder basicQuery = Query.range(Metrics.TIME_BUCKET)
                                                   .lte(duration.getEndTimeBucket())
                                                   .gte(duration.getStartTimeBucket());
+        final BoolQueryBuilder boolQuery = Query.bool().must(basicQuery);
         final SearchBuilder search = Search.builder();
 
         final boolean asc = condition.getOrder().equals(Order.ASC);
+        String[] attributes = condition.getAttributes();
+        if (CollectionUtils.isNotEmpty(attributes)) {
+            for (int i = 0; i < attributes.length; i++) {
+                if (StringUtil.isNotEmpty(attributes[i])) {
+                    boolQuery.must(Query.term(Metrics.ATTR_NAME_PREFIX + i, attributes[i]));
+                }
+            }
+        }
 
         if (CollectionUtils.isEmpty(additionalConditions)
             && IndexController.LogicIndicesRegister.isMergedTable(condition.getName())) {
-            final BoolQueryBuilder boolQuery =
-                Query.bool()
-                     .must(basicQuery)
-                     .must(Query.term(
+            boolQuery.must(Query.term(
                          IndexController.LogicIndicesRegister.METRIC_TABLE_NAME,
                          condition.getName()
                      ));
             search.query(boolQuery);
         } else if (CollectionUtils.isEmpty(additionalConditions)) {
-            search.query(basicQuery);
+            search.query(boolQuery);
         } else if (CollectionUtils.isNotEmpty(additionalConditions)
             && IndexController.LogicIndicesRegister.isMergedTable(condition.getName())) {
-            final BoolQueryBuilder boolQuery =
-                Query.bool()
-                     .must(Query.term(
+            boolQuery.must(Query.term(
                          IndexController.LogicIndicesRegister.METRIC_TABLE_NAME,
                          condition.getName()
                      ));
@@ -89,16 +94,13 @@ public List<SelectedRecord> sortMetrics(final TopNCondition condition,
                     additionalCondition.getKey(),
                     additionalCondition.getValue()
                 )));
-            boolQuery.must(basicQuery);
             search.query(boolQuery);
         } else {
-            final BoolQueryBuilder boolQuery = Query.bool();
             additionalConditions.forEach(additionalCondition -> boolQuery
                 .must(Query.terms(
                     additionalCondition.getKey(),
                     additionalCondition.getValue()
                 )));
-            boolQuery.must(basicQuery);
             search.query(boolQuery);
         }
 
diff --git a/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/dao/JDBCAggregationQueryDAO.java b/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/dao/JDBCAggregationQueryDAO.java
index 53b138a3b622..b2462e7ecf6b 100644
--- a/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/dao/JDBCAggregationQueryDAO.java
+++ b/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/dao/JDBCAggregationQueryDAO.java
@@ -28,6 +28,8 @@
 import org.apache.skywalking.oap.server.core.query.type.SelectedRecord;
 import org.apache.skywalking.oap.server.core.storage.query.IAggregationQueryDAO;
 import org.apache.skywalking.oap.server.library.client.jdbc.hikaricp.JDBCClient;
+import org.apache.skywalking.oap.server.library.util.CollectionUtils;
+import org.apache.skywalking.oap.server.library.util.StringUtil;
 import org.apache.skywalking.oap.server.storage.plugin.jdbc.common.JDBCTableInstaller;
 import org.apache.skywalking.oap.server.storage.plugin.jdbc.common.SQLAndParameters;
 import org.apache.skywalking.oap.server.storage.plugin.jdbc.common.TableHelper;
@@ -119,6 +121,14 @@ protected SQLAndParameters buildSQL(
                 parameters.add(query.getValue());
             });
         }
+        if (CollectionUtils.isNotEmpty(metrics.getAttributes())) {
+            for (int i = 0; i < metrics.getAttributes().length; i++) {
+                if (StringUtil.isNotEmpty(metrics.getAttributes()[i])) {
+                    sql.append(" and ").append(Metrics.ATTR_NAME_PREFIX).append(i).append(" = ?");
+                    parameters.add(metrics.getAttributes()[i]);
+                }
+            }
+        }
         sql.append(" group by ").append(Metrics.ENTITY_ID);
         sql.append(")  as T order by result")
            .append(Order.ASC.equals(metrics.getOrder()) ? " asc" : " desc")


Test patch:
diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/dsl/DecorateTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/dsl/DecorateTest.java
new file mode 100644
index 000000000000..f6cb3be41746
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/dsl/DecorateTest.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.skywalking.oap.meter.analyzer.dsl;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.skywalking.oap.server.core.analysis.meter.MeterEntity;
+import org.apache.skywalking.oap.server.core.config.NamingControl;
+import org.apache.skywalking.oap.server.core.config.group.EndpointNameGrouping;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import static com.google.common.collect.ImmutableMap.of;
+
+public class DecorateTest {
+    @Test
+    public void testDecorate() {
+        MeterEntity.setNamingControl(
+            new NamingControl(512, 512, 512, new EndpointNameGrouping()));
+        ImmutableMap<String, SampleFamily> input = ImmutableMap.of(
+            "http_success_request",
+            SampleFamilyBuilder.newBuilder(
+                Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                Sample.builder()
+                      .labels(of("idc", "t1", "region", "us", "instance", "10.0.0.1"))
+                      .value(100)
+                      .name("http_success_request")
+                      .build()
+            ).build()
+        );
+        String expression = "http_success_request.sum(['idc']).service(['idc'], Layer.GENERAL).decorate({ me -> me.attr0 = me.layer.name()})";
+        Expression e = DSL.parse("decorate", expression);
+        Result r = e.run(input);
+        Assertions.assertTrue(r.isSuccess());
+        Assertions.assertEquals(
+            "GENERAL", r.getData().context.getMeterSamples().keySet().stream().findFirst().get().getAttr0());
+    }
+}
diff --git a/oap-server/oal-rt/src/test/java/org/apache/skywalking/oal/rt/parser/DeepAnalysisTest.java b/oap-server/oal-rt/src/test/java/org/apache/skywalking/oal/rt/parser/DeepAnalysisTest.java
index de22950736d7..d4b7b1d4a943 100644
--- a/oap-server/oal-rt/src/test/java/org/apache/skywalking/oal/rt/parser/DeepAnalysisTest.java
+++ b/oap-server/oal-rt/src/test/java/org/apache/skywalking/oal/rt/parser/DeepAnalysisTest.java
@@ -66,7 +66,7 @@ public void testServiceAnalysis() {
         Assertions.assertEquals("(long)(1)", method.getArgsExpressions().get(1));
 
         List<SourceColumn> source = result.getFieldsFromSource();
-        Assertions.assertEquals(1, source.size());
+        Assertions.assertEquals(6, source.size());
 
         List<DataColumn> persistentFields = result.getPersistentFields();
         Assertions.assertEquals(4, persistentFields.size());
diff --git a/oap-server/oal-rt/src/test/java/org/apache/skywalking/oal/rt/parser/ScriptParserTest.java b/oap-server/oal-rt/src/test/java/org/apache/skywalking/oal/rt/parser/ScriptParserTest.java
index 6577786e4c36..5f43e06f13bb 100644
--- a/oap-server/oal-rt/src/test/java/org/apache/skywalking/oal/rt/parser/ScriptParserTest.java
+++ b/oap-server/oal-rt/src/test/java/org/apache/skywalking/oal/rt/parser/ScriptParserTest.java
@@ -18,8 +18,11 @@
 
 package org.apache.skywalking.oal.rt.parser;
 
+import org.apache.skywalking.oap.server.core.analysis.ISourceDecorator;
+import org.apache.skywalking.oap.server.core.analysis.SourceDecoratorManager;
 import org.apache.skywalking.oap.server.core.annotation.AnnotationScan;
 import org.apache.skywalking.oap.server.core.source.DefaultScopeDefine;
+import org.apache.skywalking.oap.server.core.source.ISource;
 import org.apache.skywalking.oap.server.core.storage.StorageException;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Assertions;
@@ -323,6 +326,28 @@ public void testParse12() throws IOException {
         Assertions.assertEquals("(str->long)", castExp.getAggregationFuncStmt().getFuncArgs().get(0).getCastType());
     }
 
+    @Test
+    public void testParseDecorator() throws IOException {
+        SourceDecoratorManager.DECORATOR_MAP.put("ServiceDecorator", new ISourceDecorator<ISource>() {
+            @Override
+            public int getSourceScope() {
+                return DefaultScopeDefine.SERVICE;
+            }
+
+            @Override
+            public void decorate(final ISource source) {
+
+            }
+        });
+        ScriptParser parser = ScriptParser.createFromScriptText(
+            "service_resp_time = from(Service.latency).longAvg().decorator(\"ServiceDecorator\");",
+            TEST_SOURCE_PACKAGE
+        );
+        List<AnalysisResult> results = parser.parse().getMetricsStmts();
+        AnalysisResult castExp = results.get(0);
+        Assertions.assertEquals("ServiceDecorator", castExp.getSourceDecorator());
+    }
+
     @Test
     public void testDisable() throws IOException {
         ScriptParser parser = ScriptParser.createFromScriptText("disable(segment);", TEST_SOURCE_PACKAGE);
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/latest/LatestFunction.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/latest/LatestFunction.java
index 7e354fe7ecad..7bf887378bd0 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/latest/LatestFunction.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/latest/LatestFunction.java
@@ -22,6 +22,7 @@
 import lombok.Getter;
 import lombok.Setter;
 import lombok.ToString;
+import org.apache.skywalking.oap.server.core.Const;
 import org.apache.skywalking.oap.server.core.UnexpectedException;
 import org.apache.skywalking.oap.server.core.analysis.manual.instance.InstanceTraffic;
 import org.apache.skywalking.oap.server.core.analysis.meter.Meter;
@@ -37,6 +38,7 @@
 import org.apache.skywalking.oap.server.core.storage.type.Convert2Entity;
 import org.apache.skywalking.oap.server.core.storage.type.Convert2Storage;
 import org.apache.skywalking.oap.server.core.storage.type.StorageBuilder;
+import org.apache.skywalking.oap.server.library.util.StringUtil;
 
 @MeterFunction(functionName = "latest")
 @ToString
@@ -65,6 +67,7 @@ public abstract class LatestFunction extends Meter implements AcceptableValue<Lo
 
     @Override
     public void accept(MeterEntity entity, Long value) {
+        decorate(entity);
         this.entityId = entity.id();
         this.serviceId = entity.serviceId();
         this.value = value;
@@ -89,6 +92,12 @@ public Metrics toHour() {
         metrics.setTimeBucket(toTimeBucketInHour());
         metrics.setServiceId(getServiceId());
         metrics.setValue(getValue());
+
+        metrics.setAttr0(getAttr0());
+        metrics.setAttr1(getAttr1());
+        metrics.setAttr2(getAttr2());
+        metrics.setAttr3(getAttr3());
+        metrics.setAttr4(getAttr4());
         return metrics;
     }
 
@@ -99,6 +108,12 @@ public Metrics toDay() {
         metrics.setTimeBucket(toTimeBucketInDay());
         metrics.setServiceId(getServiceId());
         metrics.setValue(getValue());
+
+        metrics.setAttr0(getAttr0());
+        metrics.setAttr1(getAttr1());
+        metrics.setAttr2(getAttr2());
+        metrics.setAttr3(getAttr3());
+        metrics.setAttr4(getAttr4());
         return metrics;
     }
 
@@ -114,6 +129,26 @@ public void deserialize(final RemoteData remoteData) {
 
         this.entityId = remoteData.getDataStrings(0);
         this.serviceId = remoteData.getDataStrings(1);
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(2))) {
+            setAttr0(remoteData.getDataStrings(2));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(3))) {
+            setAttr1(remoteData.getDataStrings(3));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(4))) {
+            setAttr2(remoteData.getDataStrings(4));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(5))) {
+            setAttr3(remoteData.getDataStrings(5));
+        }
+
+        if (StringUtil.isNotEmpty(remoteData.getDataStrings(6))) {
+            setAttr4(remoteData.getDataStrings(6));
+        }
     }
 
     @Override
@@ -125,6 +160,11 @@ public RemoteData.Builder serialize() {
         remoteBuilder.addDataStrings(entityId);
         remoteBuilder.addDataStrings(serviceId);
 
+        remoteBuilder.addDataStrings(getAttr0() == null ? Const.EMPTY_STRING : getAttr0());
+        remoteBuilder.addDataStrings(getAttr1() == null ? Const.EMPTY_STRING : getAttr1());
+        remoteBuilder.addDataStrings(getAttr2() == null ? Const.EMPTY_STRING : getAttr2());
+        remoteBuilder.addDataStrings(getAttr3() == null ? Const.EMPTY_STRING : getAttr3());
+        remoteBuilder.addDataStrings(getAttr4() == null ? Const.EMPTY_STRING : getAttr4());
         return remoteBuilder;
     }
 
@@ -153,6 +193,12 @@ public AcceptableValue<Long> createNew() {
             metrics.setTimeBucket(((Number) converter.get(TIME_BUCKET)).longValue());
             metrics.setServiceId((String) converter.get(InstanceTraffic.SERVICE_ID));
             metrics.setEntityId((String) converter.get(ENTITY_ID));
+
+            metrics.setAttr0((String) converter.get(ATTR0));
+            metrics.setAttr1((String) converter.get(ATTR1));
+            metrics.setAttr2((String) converter.get(ATTR2));
+            metrics.setAttr3((String) converter.get(ATTR3));
+            metrics.setAttr4((String) converter.get(ATTR4));
             return metrics;
         }
 
@@ -162,6 +208,12 @@ public void entity2Storage(final LatestFunction storageData, final Convert2Stora
             converter.accept(TIME_BUCKET, storageData.getTimeBucket());
             converter.accept(InstanceTraffic.SERVICE_ID, storageData.getServiceId());
             converter.accept(ENTITY_ID, storageData.getEntityId());
+
+            converter.accept(ATTR0, storageData.getAttr0());
+            converter.accept(ATTR1, storageData.getAttr1());
+            converter.accept(ATTR2, storageData.getAttr2());
+            converter.accept(ATTR3, storageData.getAttr3());
+            converter.accept(ATTR4, storageData.getAttr4());
         }
     }
 
diff --git a/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/avg/AvgFunctionTest.java b/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/avg/AvgFunctionTest.java
new file mode 100644
index 000000000000..d7b3afa938c5
--- /dev/null
+++ b/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/avg/AvgFunctionTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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.server.core.analysis.meter.function.avg;
+
+import java.util.Map;
+import org.apache.skywalking.oap.server.core.analysis.Layer;
+import org.apache.skywalking.oap.server.core.analysis.TimeBucket;
+import org.apache.skywalking.oap.server.core.analysis.meter.MeterEntity;
+import org.apache.skywalking.oap.server.core.analysis.meter.function.AcceptableValue;
+import org.apache.skywalking.oap.server.core.config.NamingControl;
+import org.apache.skywalking.oap.server.core.config.group.EndpointNameGrouping;
+import org.apache.skywalking.oap.server.core.storage.type.HashMapConverter;
+import org.apache.skywalking.oap.server.core.storage.type.StorageBuilder;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mockito;
+import org.mockito.Spy;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+
+@ExtendWith(MockitoExtension.class)
+public class AvgFunctionTest {
+
+    @Spy
+    private AvgFunction function;
+
+    @BeforeAll
+    public static void setup() {
+        MeterEntity.setNamingControl(
+            new NamingControl(512, 512, 512, new EndpointNameGrouping()));
+    }
+
+    @BeforeEach
+    public void before() {
+        function = new AvgFunctionTest.AvgFunctionInst();
+        function.setTimeBucket(TimeBucket.getMinuteTimeBucket(System.currentTimeMillis()));
+    }
+
+    @AfterAll
+    public static void tearDown() {
+        MeterEntity.setNamingControl(null);
+    }
+
+    @Test
+    public void testAccept() {
+        long time = 100;
+        function.accept(MeterEntity.newService("avg_resp_time", Layer.GENERAL), time);
+        assertThat(function.getSummation()).isEqualTo(time);
+        time = 200;
+        function.accept(MeterEntity.newService("avg_resp_time", Layer.GENERAL), time);
+        assertThat(function.getSummation()).isEqualTo(300);
+    }
+
+    @Test
+    public void testCalculate() {
+        long time1 = 100;
+        long time2 = 200;
+        function.accept(MeterEntity.newService("avg_resp_time", Layer.GENERAL), time1);
+        function.accept(MeterEntity.newService("avg_resp_time", Layer.GENERAL), time2);
+        function.calculate();
+        assertThat(function.getValue()).isEqualTo(150);
+    }
+
+    @Test
+    public void testSerialize() {
+        long time = 1597113447737L;
+        MeterEntity meterEntity = MeterEntity.newService("avg_resp_time", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, time);
+        AvgFunction function2 = Mockito.spy(AvgFunction.class);
+        function2.deserialize(function.serialize().build());
+        assertThat(function2.getEntityId()).isEqualTo(function.getEntityId());
+        assertThat(function2.getTimeBucket()).isEqualTo(function.getTimeBucket());
+        assertThat(function2.getAttr0()).isEqualTo(function.getAttr0());
+    }
+
+    @Test
+    public void testBuilder() throws IllegalAccessException, InstantiationException {
+        long time = 1597113447737L;
+        MeterEntity meterEntity = MeterEntity.newService("avg_resp_time", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, time);
+        function.calculate();
+        StorageBuilder<AvgFunction> storageBuilder = function.builder().newInstance();
+
+        final HashMapConverter.ToStorage toStorage = new HashMapConverter.ToStorage();
+        storageBuilder.entity2Storage(function, toStorage);
+        final Map<String, Object> map = toStorage.obtain();
+        map.put(AvgFunction.VALUE, map.get(AvgFunction.VALUE));
+
+        AvgFunction function2 = storageBuilder.storage2Entity(new HashMapConverter.ToEntity(map));
+        assertThat(function2.getAttr0()).isEqualTo(function.getAttr0());
+    }
+
+    @Test
+    public void testToHour() {
+        long time1 = 100;
+        long time2 = 200;
+        function.setTimeBucket(TimeBucket.getMinuteTimeBucket(System.currentTimeMillis()));
+        MeterEntity meterEntity = MeterEntity.newService("avg_resp_time", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, time1);
+        function.accept(meterEntity, time2);
+        function.calculate();
+
+        final AvgFunction hourFunction = (AvgFunction) function.toHour();
+        hourFunction.calculate();
+
+        assertThat(hourFunction.getValue()).isEqualTo(150);
+        assertThat(hourFunction.getAttr0()).isEqualTo("testAttr");
+    }
+
+    @Test
+    public void testToDay() {
+        long time1 = 100;
+        long time2 = 200;
+        MeterEntity meterEntity = MeterEntity.newService("avg_resp_time", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, time1);
+        function.accept(meterEntity, time2);
+        function.calculate();
+
+        final AvgFunction dayFunction = (AvgFunction) function.toDay();
+        dayFunction.calculate();
+        assertThat(dayFunction.getValue()).isEqualTo(150);
+        assertThat(dayFunction.getAttr0()).isEqualTo("testAttr");
+    }
+
+    private static class AvgFunctionInst extends AvgFunction {
+        @Override
+        public AcceptableValue<Long> createNew() {
+            return new AvgFunctionTest.AvgFunctionInst();
+        }
+    }
+}
diff --git a/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/latest/LatestFunctionTest.java b/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/latest/LatestFunctionTest.java
index 5127e6bd05ab..15ba1236afe0 100644
--- a/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/latest/LatestFunctionTest.java
+++ b/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/latest/LatestFunctionTest.java
@@ -19,13 +19,16 @@
 package org.apache.skywalking.oap.server.core.analysis.meter.function.latest;
 
 import org.apache.skywalking.oap.server.core.analysis.Layer;
+import org.apache.skywalking.oap.server.core.analysis.TimeBucket;
 import org.apache.skywalking.oap.server.core.analysis.meter.MeterEntity;
+import org.apache.skywalking.oap.server.core.analysis.meter.function.AcceptableValue;
 import org.apache.skywalking.oap.server.core.config.NamingControl;
 import org.apache.skywalking.oap.server.core.config.group.EndpointNameGrouping;
 import org.apache.skywalking.oap.server.core.storage.type.HashMapConverter;
 import org.apache.skywalking.oap.server.core.storage.type.StorageBuilder;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mockito;
@@ -48,6 +51,12 @@ public static void setup() {
             new NamingControl(512, 512, 512, new EndpointNameGrouping()));
     }
 
+    @BeforeEach
+    public void before() {
+        function = new LatestFunctionTest.LatestFunctionInst();
+        function.setTimeBucket(TimeBucket.getMinuteTimeBucket(System.currentTimeMillis()));
+    }
+
     @AfterAll
     public static void tearDown() {
         MeterEntity.setNamingControl(null);
@@ -76,17 +85,22 @@ public void testCalculate() {
     @Test
     public void testSerialize() {
         long time = 1597113447737L;
-        function.accept(MeterEntity.newService("latest_sync_time", Layer.GENERAL), time);
+        MeterEntity meterEntity = MeterEntity.newService("latest_sync_time", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, time);
         LatestFunction function2 = Mockito.spy(LatestFunction.class);
         function2.deserialize(function.serialize().build());
         assertThat(function2.getEntityId()).isEqualTo(function.getEntityId());
         assertThat(function2.getTimeBucket()).isEqualTo(function.getTimeBucket());
+        assertThat(function2.getAttr0()).isEqualTo(function.getAttr0());
     }
 
     @Test
     public void testBuilder() throws IllegalAccessException, InstantiationException {
         long time = 1597113447737L;
-        function.accept(MeterEntity.newService("latest_sync_time", Layer.GENERAL), time);
+        MeterEntity meterEntity = MeterEntity.newService("latest_sync_time", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, time);
         function.calculate();
         StorageBuilder<LatestFunction> storageBuilder = function.builder().newInstance();
 
@@ -96,6 +110,47 @@ public void testBuilder() throws IllegalAccessException, InstantiationException
         map.put(LatestFunction.VALUE, map.get(LatestFunction.VALUE));
 
         LatestFunction function2 = storageBuilder.storage2Entity(new HashMapConverter.ToEntity(map));
-        assertThat(function2.getValue()).isEqualTo(function.getValue());
+        assertThat(function2.getAttr0()).isEqualTo(function.getAttr0());
+    }
+
+    @Test
+    public void testToHour() {
+        long time1 = 100;
+        long time2 = 200;
+        function.setTimeBucket(TimeBucket.getMinuteTimeBucket(System.currentTimeMillis()));
+        MeterEntity meterEntity = MeterEntity.newService("latest_sync_time", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, time1);
+        function.accept(meterEntity, time2);
+        function.calculate();
+
+        final LatestFunction hourFunction = (LatestFunction) function.toHour();
+        hourFunction.calculate();
+
+        assertThat(hourFunction.getValue()).isEqualTo(time2);
+        assertThat(hourFunction.getAttr0()).isEqualTo("testAttr");
+    }
+
+    @Test
+    public void testToDay() {
+        long time1 = 100;
+        long time2 = 200;
+        MeterEntity meterEntity = MeterEntity.newService("latest_sync_time", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, time1);
+        function.accept(meterEntity, time2);
+        function.calculate();
+
+        final LatestFunction dayFunction = (LatestFunction) function.toDay();
+        dayFunction.calculate();
+        assertThat(dayFunction.getValue()).isEqualTo(time2);
+        assertThat(dayFunction.getAttr0()).isEqualTo("testAttr");
+    }
+
+    private static class LatestFunctionInst extends LatestFunction {
+        @Override
+        public AcceptableValue<Long> createNew() {
+            return new LatestFunctionInst();
+        }
     }
 }
diff --git a/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/max/MaxFunctionTest.java b/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/max/MaxFunctionTest.java
index 8a515683f940..31296dca7011 100644
--- a/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/max/MaxFunctionTest.java
+++ b/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/max/MaxFunctionTest.java
@@ -83,31 +83,39 @@ public void testCalculate() {
     @Test
     public void testToHour() {
         function.setTimeBucket(TimeBucket.getMinuteTimeBucket(System.currentTimeMillis()));
-        function.accept(MeterEntity.newService("service-test", Layer.GENERAL), LARGE_VALUE);
-        function.accept(MeterEntity.newService("service-test", Layer.GENERAL), SMALL_VALUE);
+        MeterEntity meterEntity = MeterEntity.newService("service-test", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, LARGE_VALUE);
+        function.accept(meterEntity, SMALL_VALUE);
         function.calculate();
 
         final MaxFunction hourFunction = (MaxFunction) function.toHour();
         hourFunction.calculate();
 
         assertThat(hourFunction.getValue()).isEqualTo(LARGE_VALUE);
+        assertThat(hourFunction.getAttr0()).isEqualTo(function.getAttr0());
     }
 
     @Test
     public void testToDay() {
         function.setTimeBucket(TimeBucket.getMinuteTimeBucket(System.currentTimeMillis()));
-        function.accept(MeterEntity.newService("service-test", Layer.GENERAL), LARGE_VALUE);
-        function.accept(MeterEntity.newService("service-test", Layer.GENERAL), SMALL_VALUE);
+        MeterEntity meterEntity = MeterEntity.newService("service-test", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, LARGE_VALUE);
+        function.accept(meterEntity, SMALL_VALUE);
         function.calculate();
 
         final MaxFunction dayFunction = (MaxFunction) function.toDay();
         dayFunction.calculate();
         assertThat(dayFunction.getValue()).isEqualTo(LARGE_VALUE);
+        assertThat(dayFunction.getAttr0()).isEqualTo(function.getAttr0());
     }
 
     @Test
     public void testSerialize() {
-        function.accept(MeterEntity.newService("service-test", Layer.GENERAL), LARGE_VALUE);
+        MeterEntity meterEntity = MeterEntity.newService("service-test", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, LARGE_VALUE);
         MaxFunction function2 = new MaxFunctionInst();
         function2.deserialize(function.serialize().build());
 
@@ -115,11 +123,14 @@ public void testSerialize() {
         assertThat(function2.getTimeBucket()).isEqualTo(function.getTimeBucket());
         assertThat(function2.getServiceId()).isEqualTo(function.getServiceId());
         assertThat(function2.getValue()).isEqualTo(function.getValue());
+        assertThat(function2.getAttr0()).isEqualTo(function.getAttr0());
     }
 
     @Test
     public void testBuilder() throws IllegalAccessException, InstantiationException {
-        function.accept(MeterEntity.newService("service-test", Layer.GENERAL), LARGE_VALUE);
+        MeterEntity meterEntity = MeterEntity.newService("service-test", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, LARGE_VALUE);
         function.calculate();
         StorageBuilder<MaxFunction> storageBuilder = function.builder().newInstance();
 
@@ -131,6 +142,7 @@ public void testBuilder() throws IllegalAccessException, InstantiationException
         MaxFunction function2 = storageBuilder.storage2Entity(new HashMapConverter.ToEntity(map));
 
         assertThat(function2.getValue()).isEqualTo(function.getValue());
+        assertThat(function2.getAttr0()).isEqualTo(function.getAttr0());
     }
 
     private static class MaxFunctionInst extends MaxFunction {
diff --git a/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/min/MinFunctionTest.java b/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/min/MinFunctionTest.java
index 47c271144709..1fd50d0c8118 100644
--- a/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/min/MinFunctionTest.java
+++ b/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/min/MinFunctionTest.java
@@ -84,31 +84,39 @@ public void testCalculate() {
     @Test
     public void testToHour() {
         function.setTimeBucket(TimeBucket.getMinuteTimeBucket(System.currentTimeMillis()));
-        function.accept(MeterEntity.newService("service-test", Layer.GENERAL), SMALL_VALUE);
-        function.accept(MeterEntity.newService("service-test", Layer.GENERAL), LARGE_VALUE);
+        MeterEntity meterEntity = MeterEntity.newService("service-test", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, LARGE_VALUE);
+        function.accept(meterEntity, SMALL_VALUE);
         function.calculate();
 
         final MinFunction hourFunction = (MinFunction) function.toHour();
         hourFunction.calculate();
 
         assertThat(hourFunction.getValue()).isEqualTo(SMALL_VALUE);
+        assertThat(hourFunction.getAttr0()).isEqualTo(function.getAttr0());
     }
 
     @Test
     public void testToDay() {
         function.setTimeBucket(TimeBucket.getMinuteTimeBucket(System.currentTimeMillis()));
-        function.accept(MeterEntity.newService("service-test", Layer.GENERAL), SMALL_VALUE);
-        function.accept(MeterEntity.newService("service-test", Layer.GENERAL), LARGE_VALUE);
+        MeterEntity meterEntity = MeterEntity.newService("service-test", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, LARGE_VALUE);
+        function.accept(meterEntity, SMALL_VALUE);
         function.calculate();
 
         final MinFunction dayFunction = (MinFunction) function.toDay();
         dayFunction.calculate();
         assertThat(dayFunction.getValue()).isEqualTo(SMALL_VALUE);
+        assertThat(dayFunction.getAttr0()).isEqualTo(function.getAttr0());
     }
 
     @Test
     public void testSerialize() {
-        function.accept(MeterEntity.newService("service-test", Layer.GENERAL), SMALL_VALUE);
+        MeterEntity meterEntity = MeterEntity.newService("service-test", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, SMALL_VALUE);
         MinFunction function2 = Mockito.spy(MinFunction.class);
         function2.deserialize(function.serialize().build());
 
@@ -116,11 +124,15 @@ public void testSerialize() {
         assertThat(function2.getTimeBucket()).isEqualTo(function.getTimeBucket());
         assertThat(function2.getServiceId()).isEqualTo(function.getServiceId());
         assertThat(function2.getValue()).isEqualTo(function.getValue());
+        assertThat(function2.getAttr0()).isEqualTo(function.getAttr0());
+
     }
 
     @Test
     public void testBuilder() throws IllegalAccessException, InstantiationException {
-        function.accept(MeterEntity.newService("service-test", Layer.GENERAL), SMALL_VALUE);
+        MeterEntity meterEntity = MeterEntity.newService("service-test", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, SMALL_VALUE);
         function.calculate();
         StorageBuilder<MinFunction> storageBuilder = function.builder().newInstance();
 
diff --git a/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sum/SumFunctionTest.java b/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sum/SumFunctionTest.java
new file mode 100644
index 000000000000..bc0af33b38dc
--- /dev/null
+++ b/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sum/SumFunctionTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.server.core.analysis.meter.function.sum;
+
+import java.util.Map;
+import org.apache.skywalking.oap.server.core.analysis.Layer;
+import org.apache.skywalking.oap.server.core.analysis.TimeBucket;
+import org.apache.skywalking.oap.server.core.analysis.meter.MeterEntity;
+import org.apache.skywalking.oap.server.core.analysis.meter.function.AcceptableValue;
+import org.apache.skywalking.oap.server.core.config.NamingControl;
+import org.apache.skywalking.oap.server.core.config.group.EndpointNameGrouping;
+import org.apache.skywalking.oap.server.core.storage.type.HashMapConverter;
+import org.apache.skywalking.oap.server.core.storage.type.StorageBuilder;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mockito;
+import org.mockito.Spy;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+
+@ExtendWith(MockitoExtension.class)
+public class SumFunctionTest {
+
+    @Spy
+    private SumFunction function;
+
+    @BeforeAll
+    public static void setup() {
+        MeterEntity.setNamingControl(
+            new NamingControl(512, 512, 512, new EndpointNameGrouping()));
+    }
+
+    @BeforeEach
+    public void before() {
+        function = new SumFunctionTest.SumFunctionInst();
+        function.setTimeBucket(TimeBucket.getMinuteTimeBucket(System.currentTimeMillis()));
+    }
+
+    @AfterAll
+    public static void tearDown() {
+        MeterEntity.setNamingControl(null);
+    }
+
+    @Test
+    public void testAccept() {
+        long time = 100;
+        function.accept(MeterEntity.newService("sum_resp_time", Layer.GENERAL), time);
+        assertThat(function.getValue()).isEqualTo(time);
+        time = 200;
+        function.accept(MeterEntity.newService("sum_resp_time", Layer.GENERAL), time);
+        assertThat(function.getValue()).isEqualTo(300);
+    }
+
+    @Test
+    public void testSerialize() {
+        long time = 1597113447737L;
+        MeterEntity meterEntity = MeterEntity.newService("sum_resp_time", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, time);
+        SumFunction function2 = Mockito.spy(SumFunction.class);
+        function2.deserialize(function.serialize().build());
+        assertThat(function2.getEntityId()).isEqualTo(function.getEntityId());
+        assertThat(function2.getTimeBucket()).isEqualTo(function.getTimeBucket());
+        assertThat(function2.getAttr0()).isEqualTo(function.getAttr0());
+    }
+
+    @Test
+    public void testBuilder() throws IllegalAccessException, InstantiationException {
+        long time = 1597113447737L;
+        MeterEntity meterEntity = MeterEntity.newService("sum_resp_time", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, time);
+        function.calculate();
+        StorageBuilder<SumFunction> storageBuilder = (StorageBuilder<SumFunction>) function.builder().newInstance();
+
+        final HashMapConverter.ToStorage toStorage = new HashMapConverter.ToStorage();
+        storageBuilder.entity2Storage(function, toStorage);
+        final Map<String, Object> map = toStorage.obtain();
+        map.put(SumFunction.VALUE, map.get(SumFunction.VALUE));
+
+        SumFunction function2 = storageBuilder.storage2Entity(new HashMapConverter.ToEntity(map));
+        assertThat(function2.getAttr0()).isEqualTo(function.getAttr0());
+    }
+
+    @Test
+    public void testToHour() {
+        long time1 = 100;
+        long time2 = 200;
+        function.setTimeBucket(TimeBucket.getMinuteTimeBucket(System.currentTimeMillis()));
+        MeterEntity meterEntity = MeterEntity.newService("sum_resp_time", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, time1);
+        function.accept(meterEntity, time2);
+        function.calculate();
+
+        final SumFunction hourFunction = (SumFunction) function.toHour();
+        hourFunction.calculate();
+
+        assertThat(hourFunction.getValue()).isEqualTo(300);
+        assertThat(hourFunction.getAttr0()).isEqualTo("testAttr");
+    }
+
+    @Test
+    public void testToDay() {
+        long time1 = 100;
+        long time2 = 200;
+        MeterEntity meterEntity = MeterEntity.newService("sum_resp_time", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, time1);
+        function.accept(meterEntity, time2);
+        function.calculate();
+
+        final SumFunction dayFunction = (SumFunction) function.toDay();
+        dayFunction.calculate();
+        assertThat(dayFunction.getValue()).isEqualTo(300);
+        assertThat(dayFunction.getAttr0()).isEqualTo("testAttr");
+    }
+
+    private static class SumFunctionInst extends SumFunction {
+        @Override
+        public AcceptableValue<Long> createNew() {
+            return new SumFunctionTest.SumFunctionInst();
+        }
+    }
+}
diff --git a/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sumpermin/SumPerMinFunctionTest.java b/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sumpermin/SumPerMinFunctionTest.java
index fe00209e33d0..e6737e2e0dd6 100644
--- a/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sumpermin/SumPerMinFunctionTest.java
+++ b/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sumpermin/SumPerMinFunctionTest.java
@@ -87,28 +87,36 @@ public void testHour() {
         long time1 = 1597113318673L;
         long time2 = 1597113447737L;
         function.setTimeBucket(TimeBucket.getMinuteTimeBucket(System.currentTimeMillis()));
-        function.accept(MeterEntity.newService("sum_sync_time", Layer.GENERAL), time1);
-        function.accept(MeterEntity.newService("sum_sync_time", Layer.GENERAL), time2);
+        MeterEntity meterEntity = MeterEntity.newService("sum_sync_time", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, time1);
+        function.accept(meterEntity, time2);
         function.calculate();
         final SumPerMinFunction hourFunction = (SumPerMinFunction) function.toHour();
         hourFunction.calculate();
         assertThat(hourFunction.getValue()).isEqualTo((time1 + time2) / 60);
+        assertThat(hourFunction.getAttr0()).isEqualTo(function.getAttr0());
     }
 
     @Test
     public void testSerialize() {
         long time = 1597113447737L;
-        function.accept(MeterEntity.newService("sum_sync_time", Layer.GENERAL), time);
+        MeterEntity meterEntity = MeterEntity.newService("sum_sync_time", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, time);
         SumPerMinFunction function2 = Mockito.spy(SumPerMinFunction.class);
         function2.deserialize(function.serialize().build());
         assertThat(function2.getEntityId()).isEqualTo(function.getEntityId());
         assertThat(function2.getTimeBucket()).isEqualTo(function.getTimeBucket());
+        assertThat(function2.getAttr0()).isEqualTo(function.getAttr0());
     }
 
     @Test
     public void testBuilder() throws IllegalAccessException, InstantiationException {
         long time = 1597113447737L;
-        function.accept(MeterEntity.newService("sum_sync_time", Layer.GENERAL), time);
+        MeterEntity meterEntity = MeterEntity.newService("sum_sync_time", Layer.GENERAL);
+        meterEntity.setAttr0("testAttr");
+        function.accept(meterEntity, time);
         function.calculate();
         StorageBuilder<SumPerMinFunction> storageBuilder = function.builder().newInstance();
 
@@ -119,6 +127,7 @@ public void testBuilder() throws IllegalAccessException, InstantiationException
 
         SumPerMinFunction function2 = storageBuilder.storage2Entity(new HashMapConverter.ToEntity(map));
         assertThat(function2.getValue()).isEqualTo(function.getValue());
+        assertThat(function2.getAttr0()).isEqualTo(function.getAttr0());
     }
 
     private static class SumPerMinFunctionInst extends SumPerMinFunction {
diff --git a/oap-server/server-query-plugin/promql-plugin/src/test/java/org/apache/skywalking/promql/rt/parser/PromQLExprQueryVisitorTest.java b/oap-server/server-query-plugin/promql-plugin/src/test/java/org/apache/skywalking/promql/rt/parser/PromQLExprQueryVisitorTest.java
index 8ced768f90ae..074ca04ce3c8 100644
--- a/oap-server/server-query-plugin/promql-plugin/src/test/java/org/apache/skywalking/promql/rt/parser/PromQLExprQueryVisitorTest.java
+++ b/oap-server/server-query-plugin/promql-plugin/src/test/java/org/apache/skywalking/promql/rt/parser/PromQLExprQueryVisitorTest.java
@@ -158,6 +158,23 @@ public static Collection<Object[]> aggregateData() {
                     )
                 )
             },
+            {
+                "MetricsAggregationOpSumParens",
+                PromQLApiHandler.QueryType.RANGE,
+                "(sum by(group) (http_requests_total{service='serviceA', layer='GENERAL'}) + 2) * 2",
+                List.of(
+                    List.of(
+                        new TimeValuePair(TIME_2023022010, "8"),
+                        new TimeValuePair(TIME_2023022011, "12"),
+                        new TimeValuePair(TIME_2023022012, "16")
+                    )
+                ),
+                List.of(
+                    List.of(
+                        new LabelValuePair("group", "g")
+                    )
+                )
+            },
             {
                 "MetricsAggregationOpAvg",
                 PromQLApiHandler.QueryType.RANGE,
diff --git a/test/e2e-v2/cases/mqe/mqe-cases.yaml b/test/e2e-v2/cases/mqe/mqe-cases.yaml
index 772bb756650c..f54b48b5893a 100644
--- a/test/e2e-v2/cases/mqe/mqe-cases.yaml
+++ b/test/e2e-v2/cases/mqe/mqe-cases.yaml
@@ -36,9 +36,12 @@ cases:
   - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression="abs(service_sla-10300)" --service-name=e2e-service-provider
     expected: expected/func-OP.yml
 
-  # topN-OP-service
+  # topN-OP-service Global
   - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression="top_n(service_sla,3,des)/100"
     expected: expected/topN-OP-service.yml
+    # topN-OP-service Global with attrs
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression="top_n(service_sla,3,des,attr0='GENERAL')/100"
+    expected: expected/topN-OP-service.yml
 
   # topN-OP-service
   - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression="top_n(service_instance_sla,3,des)/100" --service-name=e2e-service-provider
diff --git a/test/e2e-v2/cases/mqe/oal/core.oal b/test/e2e-v2/cases/mqe/oal/core.oal
index c40593d9bfae..d357f208fd29 100755
--- a/test/e2e-v2/cases/mqe/oal/core.oal
+++ b/test/e2e-v2/cases/mqe/oal/core.oal
@@ -17,8 +17,8 @@
  */
 
 // Service scope metrics
-service_resp_time = from(Service.latency).longAvg();
-service_sla = from(Service.*).percent(status == true);
+service_resp_time = from(Service.latency).longAvg().decorator("ServiceDecorator");
+service_sla = from(Service.*).percent(status == true).decorator("ServiceDecorator");
 service_cpm = from(Service.*).cpm();
 service_percentile = from(Service.latency).percentile2(10); // Multiple values including p50, p75, p90, p95, p99
 service_percentile_old = from(Service.latency).percentile(10); // old version, use percentile func, need compatible with old version
diff --git a/test/e2e-v2/cases/storage/expected/topN-OP-service.yml b/test/e2e-v2/cases/storage/expected/topN-OP-service.yml
new file mode 100644
index 000000000000..4ac5c7cbe5dd
--- /dev/null
+++ b/test/e2e-v2/cases/storage/expected/topN-OP-service.yml
@@ -0,0 +1,27 @@
+# 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.
+
+type: SORTED_LIST
+results:
+  - metric:
+      labels: []
+    values:
+      - id: e2e-service-consumer
+        value: "100"
+        traceid: null
+      - id: e2e-service-provider
+        value: "100"
+        traceid: null
+error: null
diff --git a/test/e2e-v2/cases/storage/storage-cases.yaml b/test/e2e-v2/cases/storage/storage-cases.yaml
index f7a06f398248..690a760ed0f2 100644
--- a/test/e2e-v2/cases/storage/storage-cases.yaml
+++ b/test/e2e-v2/cases/storage/storage-cases.yaml
@@ -160,3 +160,9 @@ cases:
           | yq e '.traces[0].traceids[0]' - \
       )
     expected: expected/trace-users-detail.yml
+  # topN-OP-service Global
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression="top_n(service_sla,3,des)/100"
+    expected: expected/topN-OP-service.yml
+    # topN-OP-service Global with attrs
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression="top_n(service_sla,3,des,attr0='GENERAL')/100"
+    expected: expected/topN-OP-service.yml
diff --git a/test/e2e-v2/cases/vm/expected/topN.yml b/test/e2e-v2/cases/vm/expected/topN.yml
new file mode 100644
index 000000000000..60713f8e815b
--- /dev/null
+++ b/test/e2e-v2/cases/vm/expected/topN.yml
@@ -0,0 +1,28 @@
+# 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.
+
+type: SORTED_LIST
+results:
+  {{- contains .results }}
+  - metric:
+      labels: []
+    {{- contains .values }}
+    values:
+      - id: vm-service
+        value: {{ .value }}
+        traceid: null
+    {{- end }}
+  {{- end }}
+error: null
diff --git a/test/e2e-v2/cases/vm/prometheus-node-exporter/docker-compose.yml b/test/e2e-v2/cases/vm/prometheus-node-exporter/docker-compose.yml
index cf1627700b2d..45d770b4da76 100644
--- a/test/e2e-v2/cases/vm/prometheus-node-exporter/docker-compose.yml
+++ b/test/e2e-v2/cases/vm/prometheus-node-exporter/docker-compose.yml
@@ -23,6 +23,8 @@ services:
     environment:
       SW_OTEL_RECEIVER: default
       SW_OTEL_RECEIVER_ENABLED_OTEL_METRICS_RULES: vm
+    volumes:
+      - ./otel-rules/vm.yaml:/skywalking/config/otel-rules/vm.yaml
     ports:
       - 12800
 
diff --git a/test/e2e-v2/cases/vm/prometheus-node-exporter/e2e.yaml b/test/e2e-v2/cases/vm/prometheus-node-exporter/e2e.yaml
index fddad37cc198..84d5105667e1 100644
--- a/test/e2e-v2/cases/vm/prometheus-node-exporter/e2e.yaml
+++ b/test/e2e-v2/cases/vm/prometheus-node-exporter/e2e.yaml
@@ -52,3 +52,9 @@ verify:
     # multiple-linear metrics
     - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_vm_cpu_average_used --service-name=vm-service
       expected: ../expected/metrics-has-average-value-label.yml
+    # topN
+    - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression="top_n(meter_vm_cpu_load1,3,des)" --service-name=vm-service
+      expected: ../expected/topN.yml
+    # topN with attrs
+    - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression="top_n(meter_vm_cpu_load1,3,des,attr0='OS_LINUX')" --service-name=vm-service
+      expected: ../expected/topN.yml
diff --git a/test/e2e-v2/cases/vm/prometheus-node-exporter/otel-rules/vm.yaml b/test/e2e-v2/cases/vm/prometheus-node-exporter/otel-rules/vm.yaml
new file mode 100644
index 000000000000..a0921ad99d91
--- /dev/null
+++ b/test/e2e-v2/cases/vm/prometheus-node-exporter/otel-rules/vm.yaml
@@ -0,0 +1,96 @@
+# 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.
+
+# This will parse a textual representation of a duration. The formats
+# accepted are based on the ISO-8601 duration format {@code PnDTnHnMn.nS}
+# with days considered to be exactly 24 hours.
+# <p>
+# Examples:
+# <pre>
+#    "PT20.345S" -- parses as "20.345 seconds"
+#    "PT15M"     -- parses as "15 minutes" (where a minute is 60 seconds)
+#    "PT10H"     -- parses as "10 hours" (where an hour is 3600 seconds)
+#    "P2D"       -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
+#    "P2DT3H4M"  -- parses as "2 days, 3 hours and 4 minutes"
+#    "P-6H3M"    -- parses as "-6 hours and +3 minutes"
+#    "-P6H3M"    -- parses as "-6 hours and -3 minutes"
+#    "-P-6H+3M"  -- parses as "+6 hours and -3 minutes"
+# </pre>
+filter: "{ tags -> tags.job_name == 'vm-monitoring' }" # The OpenTelemetry job name
+metricPrefix: meter_vm
+metricsRules:
+
+  #node cpu
+  - name: cpu_total_percentage
+    exp: (node_cpu_seconds_total * 100).tagNotEqual('mode' , 'idle').sum(['node_identifier_host_name']).rate('PT1M').service(['node_identifier_host_name'] , Layer.OS_LINUX)
+  - name: cpu_average_used
+    exp: (node_cpu_seconds_total * 100).sum(['node_identifier_host_name' , 'mode']).rate('PT1M').service(['node_identifier_host_name'] , Layer.OS_LINUX)
+  - name: cpu_load1
+    exp: (node_load1 * 100).service(['node_identifier_host_name'] , Layer.OS_LINUX).decorate({ me -> me.attr0 = me.layer.name()})
+  - name: cpu_load5
+    exp: (node_load5 * 100).service(['node_identifier_host_name'] , Layer.OS_LINUX)
+  - name: cpu_load15
+    exp: (node_load15 * 100).service(['node_identifier_host_name'] , Layer.OS_LINUX)
+
+    #node Memory
+  - name: memory_total
+    exp: node_memory_MemTotal_bytes.service(['node_identifier_host_name'] , Layer.OS_LINUX)
+  - name: memory_available
+    exp: node_memory_MemAvailable_bytes.service(['node_identifier_host_name'] , Layer.OS_LINUX)
+  - name: memory_used
+    exp: (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes).service(['node_identifier_host_name'] , Layer.OS_LINUX)
+  - name: memory_buff_cache
+    exp: (node_memory_Buffers_bytes + node_memory_Cached_bytes).service(['node_identifier_host_name'] , Layer.OS_LINUX)
+  - name: memory_swap_free
+    exp: node_memory_SwapFree_bytes.service(['node_identifier_host_name'] , Layer.OS_LINUX)
+  - name: memory_swap_total
+    exp: node_memory_SwapTotal_bytes.service(['node_identifier_host_name'] , Layer.OS_LINUX)
+  - name: memory_swap_percentage
+    exp: (100 - ((node_memory_SwapFree_bytes * 100) / node_memory_SwapTotal_bytes)).service(['node_identifier_host_name'] , Layer.OS_LINUX)
+
+    #node filesystem
+  - name: filesystem_percentage
+    exp: (100 - ((node_filesystem_avail_bytes * 100).sum(['node_identifier_host_name' , 'mountpoint']) / node_filesystem_size_bytes.sum(['node_identifier_host_name' , 'mountpoint']))).service(['node_identifier_host_name'] , Layer.OS_LINUX)
+
+    #node disk
+  - name: disk_read
+    exp: (node_disk_read_bytes_total.sum(['node_identifier_host_name']).rate('PT1M')).service(['node_identifier_host_name'] , Layer.OS_LINUX)
+  - name: disk_written
+    exp: (node_disk_written_bytes_total.sum(['node_identifier_host_name']).rate('PT1M')).service(['node_identifier_host_name'] , Layer.OS_LINUX)
+
+    #node network
+  - name: network_receive
+    exp: (node_network_receive_bytes_total.sum(['node_identifier_host_name']).irate()).service(['node_identifier_host_name'] , Layer.OS_LINUX)
+  - name: network_transmit
+    exp: (node_network_transmit_bytes_total.sum(['node_identifier_host_name']).irate()).service(['node_identifier_host_name'] , Layer.OS_LINUX)
+
+    #node netstat
+  - name: tcp_curr_estab
+    exp: node_netstat_Tcp_CurrEstab.service(['node_identifier_host_name'] , Layer.OS_LINUX)
+  - name: tcp_tw
+    exp: node_sockstat_TCP_tw.service(['node_identifier_host_name'] , Layer.OS_LINUX)
+  - name: tcp_alloc
+    exp: node_sockstat_TCP_alloc.service(['node_identifier_host_name'] , Layer.OS_LINUX)
+  - name: sockets_used
+    exp: node_sockstat_sockets_used.service(['node_identifier_host_name'] , Layer.OS_LINUX)
+  - name: udp_inuse
+    exp: node_sockstat_UDP_inuse.service(['node_identifier_host_name'] , Layer.OS_LINUX)
+
+    #node filefd
+  - name: filefd_allocated
+    exp: node_filefd_allocated.service(['node_identifier_host_name'] , Layer.OS_LINUX)
+
+
+