Repository: grafana/alloy
Pull request #3397: foreach: Add id field to use collection item fields for child component identification
Author: login: harshrai654
Labels: name: frozen-due-to-age
Created: 2025-04-20T10:59:02Z | Merged: 2025-05-05T09:32:37Z

<!--

CONTRIBUTORS GUIDE: https://github.com/grafana/alloy/blob/main/docs/developer/contributing.md#updating-the-changelog

If this is your first PR or you have not contributed in a while, we recommend
taking the time to review the guide. It gives helpful instructions for
contributors around things like how to update the changelog.

-->

#### PR Description

Add a new optional `id` field to the `foreach` block which allows specifying a field name from collection items to be used for child component identification. When set and the collection contains objects, the value of this field will be used to generate more meaningful component paths in metrics, making it easier to identify and debug components in large deployments.

For example:
```alloy
foreach "pods" {
    collection = [
        { namespace = "prod", name = "app-1" },
        { namespace = "dev", name = "app-1" }
    ]
    var = "each"
    id = "name"
    enable_metrics = true
    template { ... }
}
```

Will result in component paths like:
```
/foreach.pods/foreach_app-1_1/component.default
/foreach.pods/foreach_app-1_2/component.default
```

#### Which issue(s) this PR fixes

<!-- Uncomment the following line if you want that GitHub issue gets automatically closed after merging the PR -->
Fixes #3267 

#### Notes to the Reviewer

- The id field is optional and defaults to using the entire collection item for identification
- If the collection item is not a map or the specified field doesn't exist, it falls back to using the entire item
- Added tests to verify the behaviour with use of collection object's field as `id` along with collision scenario.

#### PR Checklist

<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [X] CHANGELOG.md updated
- [X] Documentation added
- [X] Tests updated


Linked issue #3267: foreach: add a way to specify an ID instead of the generated uid for objects
The children created by the foreach will get a UID associated with their instance when the collection contains objects. When the metrics are enabled, the component metrics will have a component path that looks like this:

`component_path="/foreach.retention/foreach_<some_uid>/aws_log_retention.default"`

The problem is that it does not give meaningful information about the foreach child.

One option to solve this could be to add an optional "id" argument to be used instead of a UID:

```
foreach "default" {
  collection           = []
  var                      = "each"
  enable_metrics = true
  id                        = "namespace" // or each["namespace"]?
  template {}
}
```

In this scenario, the namespace field of the objects will be used instead of the UID and the component path in the metrics will look like this:

`component_path="/foreach.retention/foreach_<namespace>/aws_log_retention.default"`

Collision can be handled by simply adding a suffix with an increment:

`component_path="/foreach.retention/foreach_<namespace>_1/aws_log_retention.default"`
Linked issue comments: user: login: harshrai654
created_at: 2025-04-16T18:31:18Z
body: Hey @wildum , Can I pick up this issue? I am very new to open source contributions but I am using grafana stack for a while so I am familiar with the ecosystem and the workings of alloy. It will be helpful if you can point in the direction to look at to uderstand what code path is related to this issue.
Thanks.
author_association: CONTRIBUTOR
user: login: harshrai654
created_at: 2025-04-18T16:54:08Z
body: Just out of curiosity, I started with a general walkthrough of the codebase. From what I understand, we’d need to update ForEachArguments to introduce an optional id field, and modify the objectFingerprint function to use this id value (if present) for generating the customComponentId.
It looks like the collision scenario is already handled via customComponentHashCounts, so that part may not need changes.

I'd love to take this on — let me know if it's okay for me to pick this up!
author_association: CONTRIBUTOR
user: login: harshrai654
created_at: 2025-04-24T16:38:25Z
body: Hey! Just a quick update — I’ve completed the task and opened a PR #3397 
Looking forward to feedback once someone gets a chance to review. 😊
author_association: CONTRIBUTOR
user: login: wildum
created_at: 2025-05-02T07:55:03Z
body: hey thanks for picking this up, I was on vacation the last 3 weeks, I will review your PR once I'm done catching up :)
author_association: CONTRIBUTOR

Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b57723bceb9..9e20fa1b528 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -41,6 +41,8 @@ Main (unreleased)
 
 - Pretty print diagnostic errors when using `alloy run` (@kalleep)
 
+-  Add optional `id` field to `foreach` block to generate more meaningful component paths in metrics by using a specific field from collection items. (@harshrai654)
+  
 ### Bugfixes
 
 - Fix `otelcol.receiver.filelog` documentation's default value for `start_at`. (@petewall)
diff --git a/docs/sources/reference/config-blocks/foreach.md b/docs/sources/reference/config-blocks/foreach.md
index 4eee6b99b23..07c612c0e92 100644
--- a/docs/sources/reference/config-blocks/foreach.md
+++ b/docs/sources/reference/config-blocks/foreach.md
@@ -7,7 +7,6 @@ menuTitle: foreach
 title: foreach
 ---
 
-
 # foreach
 
 {{< docs/shared lookup="stability/experimental_feature.md" source="alloy" version="<ALLOY_VERSION>" >}}
@@ -30,11 +29,12 @@ foreach "<LABEL>" {
 
 You can use the following arguments with `foreach`:
 
-Name             | Type        | Description                                                                              | Default | Required
------------------|-------------|------------------------------------------------------------------------------------------|---------|---------
-`collection`     | `list(any)` | A list of items to loop over.                                                            |         | yes
-`var`            | `string`    | Name of the variable referring to the current item in the collection.                    |         | yes
-`enable_metrics` | `bool`      | Whether to expose debug metrics in the {{< param "PRODUCT_NAME" >}} `/metrics` endpoint. | `false` | no
+| Name             | Type        | Description                                                                              | Default | Required |
+| ---------------- | ----------- | ---------------------------------------------------------------------------------------- | ------- | -------- |
+| `collection`     | `list(any)` | A list of items to loop over.                                                            |         | yes      |
+| `var`            | `string`    | Name of the variable referring to the current item in the collection.                    |         | yes      |
+| `enable_metrics` | `bool`      | Whether to expose debug metrics in the {{< param "PRODUCT_NAME" >}} `/metrics` endpoint. | `false` | no       |
+| `id`             | `string`    | Name of the field to use from collection items for child component's identification.     | `""`    | no       |
 
 The items in the `collection` list can be of any type [type][types], such as a bool, a string, a list, or a map.
 
@@ -42,15 +42,44 @@ The items in the `collection` list can be of any type [type][types], such as a b
 Setting `enable_metrics` to `true` when `collection` has lots of elements may cause a large number of metrics to appear on the {{< param "PRODUCT_NAME" >}} `/metric` endpoint.
 {{< /admonition >}}
 
+{{< admonition type="note" >}}
+When `id` is set and `enable_metrics` is `true`, component paths in metrics use the value of the specified field from collection items instead of a hash of the entire item.
+For example:
+
+```alloy
+foreach "pods" {
+    collection = [
+        { namespace = "prod", name = "app-1" },
+        { namespace = "dev", name = "app-1" },
+        { namespace = "prod", name = "app-2" }
+    ]
+    var = "each"
+    id = "name"
+    enable_metrics = true
+    template { ... }
+}
+```
+
+This results in component paths like the following:
+
+```text
+/foreach.pods/foreach_app-1_1/component.default
+/foreach.pods/foreach_app-1_2/component.default
+/foreach.pods/foreach_app-2_1/component.default
+```
+
+If the collection item is not a object or the specified field doesn't exist, it falls back to using the entire item for identification.
+{{< /admonition >}}
+
 [types]: ../../../get-started/configuration-syntax/expressions/types_and_values
 
 ## Blocks
 
 You can use the following blocks with `foreach`:
 
-Block        | Description                  | Required
---------------|------------------------------|---------
-[template][] | A component pipeline to run. | yes
+| Block        | Description                  | Required |
+| ------------ | ---------------------------- | -------- |
+| [template][] | A component pipeline to run. | yes      |
 
 [template]: #template
 
@@ -72,6 +101,7 @@ For example, `prometheus.exporter.redis` has a `redis_addr` attribute for the Re
 On the other hand, `discovery.*` components such as `discovery.kubernetes` output a list of targets such as this:
 
 {{< collapse title="Example targets output by `discovery.kubernetes`" >}}
+
 ```json
 [
     {
@@ -124,6 +154,7 @@ On the other hand, `discovery.*` components such as `discovery.kubernetes` outpu
     }
 ]
 ```
+
 {{< /collapse >}}
 
 You can use a `foreach` to loop over each target and start a separate component pipeline for it.
@@ -195,6 +226,5 @@ prometheus.remote_write "mimir" {
 
 Replace the following:
 
-* _`<PROMETHEUS_USERNAME>`_: Your Prometheus username.
-* _`<GRAFANA_CLOUD_API_KEY>`_: Your Grafana Cloud API key.
-
+- _`<PROMETHEUS_USERNAME>`_: Your Prometheus username.
+- _`<GRAFANA_CLOUD_API_KEY>`_: Your Grafana Cloud API key.
diff --git a/internal/runtime/foreach_test.go b/internal/runtime/foreach_test.go
index 83332a4bd66..834c043e26a 100644
--- a/internal/runtime/foreach_test.go
+++ b/internal/runtime/foreach_test.go
@@ -151,13 +151,22 @@ func testConfigForEach(t *testing.T, config string, reloadConfig string, update
 	}
 
 	if expectedMetrics != nil {
+		metricsToCheck := []string{}
+
 		// These metrics have fixed values.
 		// Hence, we can compare their values from run to run.
-		metricsToCheck := []string{
-			"alloy_component_controller_evaluating",
-			"alloy_component_controller_running_components",
-			// "alloy_component_evaluation_queue_size", // TODO - metric value is inconsistent depending on timing
-			"pulse_count",
+		metrics := map[string]bool{
+			"alloy_component_controller_running_components": true,
+			"alloy_component_controller_evaluating":         true,
+			"pulse_count":                                   true,
+			// "alloy_component_evaluation_queue_size": true, // TODO - metric value is inconsistent
+		}
+
+		// Only check metrics that are present in the expected output
+		for metric := range metrics {
+			if strings.Contains(*expectedMetrics, metric) {
+				metricsToCheck = append(metricsToCheck, metric)
+			}
 		}
 
 		err := testutil.GatherAndCompare(reg, strings.NewReader(*expectedMetrics), metricsToCheck...)
diff --git a/internal/runtime/internal/controller/node_config_foreach.go b/internal/runtime/internal/controller/node_config_foreach.go
index 0d61d03a07c..7ed8c2e43b1 100644
--- a/internal/runtime/internal/controller/node_config_foreach.go
+++ b/internal/runtime/internal/controller/node_config_foreach.go
@@ -126,6 +126,7 @@ func (fn *ForeachConfigNode) ID() ComponentID {
 type ForEachArguments struct {
 	Collection []any  `alloy:"collection,attr"`
 	Var        string `alloy:"var,attr"`
+	Id         string `alloy:"id,attr,optional"`
 
 	// enable_metrics should be false by default.
 	// That way users are protected from an explosion of debug metrics
@@ -189,10 +190,25 @@ func (fn *ForeachConfigNode) evaluate(scope *vm.Scope) error {
 	newCustomComponentIds := make(map[string]bool, len(args.Collection))
 	fn.customComponentHashCounts = make(map[string]int)
 	for i := 0; i < len(args.Collection); i++ {
+		// Using default value for id as whole collection object
+		id := args.Collection[i]
+
+		// Extract Id from collection if exists
+		if args.Id != "" {
+			if m, ok := args.Collection[i].(map[string]any); ok {
+				if val, exists := m[args.Id]; exists {
+					// Use the field's value for fingerprinting
+					id = val
+				} else {
+					level.Warn(fn.logger).Log("msg", "specified id not found in collection item", "id", args.Id)
+				}
+			}
+		}
+
 		// We must create an ID from the collection entries to avoid recreating all components on every updates.
 		// We track the hash counts because the collection might contain duplicates ([1, 1, 1] would result in the same ids
 		// so we handle it by adding the count at the end -> [11, 12, 13]
-		customComponentID := fmt.Sprintf("foreach_%s", objectFingerprint(args.Collection[i]))
+		customComponentID := fmt.Sprintf("foreach_%s", objectFingerprint(id))
 		count := fn.customComponentHashCounts[customComponentID] // count = 0 if the key is not found
 		fn.customComponentHashCounts[customComponentID] = count + 1
 		customComponentID += fmt.Sprintf("_%d", count+1)
@@ -400,9 +416,9 @@ func computeHash(s string) string {
 	return hex.EncodeToString(hasher.Sum(nil))
 }
 
-func objectFingerprint(obj any) string {
+func objectFingerprint(id any) string {
 	// TODO: Test what happens if there is a "true" string and a true bool in the collection.
-	switch v := obj.(type) {
+	switch v := id.(type) {
 	case string:
 		return replaceNonAlphaNumeric(v)
 	case int, bool:
diff --git a/internal/runtime/testdata/foreach_metrics/foreach_4.txtar b/internal/runtime/testdata/foreach_metrics/foreach_4.txtar
new file mode 100644
index 00000000000..0252e0a4085
--- /dev/null
+++ b/internal/runtime/testdata/foreach_metrics/foreach_4.txtar
@@ -0,0 +1,30 @@
+This test uses objects in collection with id attribute using object's field.
+
+-- main.alloy --
+foreach "testForeach" {
+  collection = [{"namespace" = "dev", "b" = 3}, {"namespace" = "prod", "b" = 3}, {"namespace" = "dev", "b" = 4}]
+  var = "item"
+  id = "namespace"
+  enable_metrics = true
+
+  template {
+    testcomponents.pulse "pt" {
+      max = item["b"]
+      frequency = "10ms"
+      forward_to = [testcomponents.summation_receiver.sum.receiver]
+    }
+  }
+}
+
+// Similar to testcomponents.summation, but with a "receiver" export
+testcomponents.summation_receiver "sum" {
+}
+
+-- expected_metrics.prom --
+
+# HELP alloy_component_controller_running_components Total number of running components.
+# TYPE alloy_component_controller_running_components gauge
+alloy_component_controller_running_components{controller_id="",controller_path="/",health_type="healthy"} 2
+alloy_component_controller_running_components{controller_id="foreach_dev_1",controller_path="/foreach.testForeach",health_type="healthy"} 1
+alloy_component_controller_running_components{controller_id="foreach_dev_2",controller_path="/foreach.testForeach",health_type="healthy"} 1
+alloy_component_controller_running_components{controller_id="foreach_prod_1",controller_path="/foreach.testForeach",health_type="healthy"} 1
\ No newline at end of file
diff --git a/internal/runtime/testdata/foreach_metrics/foreach_5.txtar b/internal/runtime/testdata/foreach_metrics/foreach_5.txtar
new file mode 100644
index 00000000000..359750b9bd4
--- /dev/null
+++ b/internal/runtime/testdata/foreach_metrics/foreach_5.txtar
@@ -0,0 +1,29 @@
+This test verifies id field behavior with invalid/missing field names and non-object collection items.
+
+-- main.alloy --
+foreach "testForeach" {
+  collection = ["plain-string",{"namespace" = "prod"},42]
+  var = "item"
+  id = "namespace"
+  enable_metrics = true
+
+  template {
+    testcomponents.pulse "pt" {
+      max = 10
+      frequency = "10ms"
+      forward_to = [testcomponents.summation_receiver.sum.receiver]
+    }
+  }
+}
+
+testcomponents.summation_receiver "sum" {
+}
+
+-- expected_metrics.prom --
+
+# HELP alloy_component_controller_running_components Total number of running components.
+# TYPE alloy_component_controller_running_components gauge
+alloy_component_controller_running_components{controller_id="",controller_path="/",health_type="healthy"} 2
+alloy_component_controller_running_components{controller_id="foreach_42_1",controller_path="/foreach.testForeach",health_type="healthy"} 1
+alloy_component_controller_running_components{controller_id="foreach_plain_string_1",controller_path="/foreach.testForeach",health_type="healthy"} 1
+alloy_component_controller_running_components{controller_id="foreach_prod_1",controller_path="/foreach.testForeach",health_type="healthy"} 1


Reviews:
user: login: clayton-cornell
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T18:10:24Z
user: login: wildum
body: Great work, thanks!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T07:24:40Z
user: login: wildum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T08:09:44Z

Review comments:
diff_hunk: @@ -30,27 +29,56 @@ foreach "<LABEL>" {
 
 You can use the following arguments with `foreach`:
 
-Name             | Type        | Description                                                                              | Default | Required
------------------|-------------|------------------------------------------------------------------------------------------|---------|---------
-`collection`     | `list(any)` | A list of items to loop over.                                                            |         | yes
-`var`            | `string`    | Name of the variable referring to the current item in the collection.                    |         | yes
-`enable_metrics` | `bool`      | Whether to expose debug metrics in the {{< param "PRODUCT_NAME" >}} `/metrics` endpoint. | `false` | no
+| Name             | Type        | Description                                                                              | Default | Required |
+| ---------------- | ----------- | ---------------------------------------------------------------------------------------- | ------- | -------- |
+| `collection`     | `list(any)` | A list of items to loop over.                                                            |         | yes      |
+| `var`            | `string`    | Name of the variable referring to the current item in the collection.                    |         | yes      |
+| `enable_metrics` | `bool`      | Whether to expose debug metrics in the {{< param "PRODUCT_NAME" >}} `/metrics` endpoint. | `false` | no       |
+| `id`             | `string`    | Name of the field to use from collection items for child component's identification.     | `""`    | no       |
 
 The items in the `collection` list can be of any type [type][types], such as a bool, a string, a list, or a map.
 
 {{< admonition type="warning" >}}
 Setting `enable_metrics` to `true` when `collection` has lots of elements may cause a large number of metrics to appear on the {{< param "PRODUCT_NAME" >}} `/metric` endpoint.
 {{< /admonition >}}
 
+{{< admonition type="note" >}}
+When `id` is set and `enable_metrics` is true, component paths in metrics will use the value of the specified field from collection items instead of a hash of the entire item. For example:
path: docs/sources/reference/config-blocks/foreach.md
user: login: clayton-cornell
body: ```suggestion
{{< admonition type="note" >}}
When `id` is set and `enable_metrics` is `true`, component paths in metrics use the value of the specified field from collection items instead of a hash of the entire item.
For example:
```
created_at: 2025-05-01T18:08:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,27 +29,56 @@ foreach "<LABEL>" {
 
 You can use the following arguments with `foreach`:
 
-Name             | Type        | Description                                                                              | Default | Required
------------------|-------------|------------------------------------------------------------------------------------------|---------|---------
-`collection`     | `list(any)` | A list of items to loop over.                                                            |         | yes
-`var`            | `string`    | Name of the variable referring to the current item in the collection.                    |         | yes
-`enable_metrics` | `bool`      | Whether to expose debug metrics in the {{< param "PRODUCT_NAME" >}} `/metrics` endpoint. | `false` | no
+| Name             | Type        | Description                                                                              | Default | Required |
+| ---------------- | ----------- | ---------------------------------------------------------------------------------------- | ------- | -------- |
+| `collection`     | `list(any)` | A list of items to loop over.                                                            |         | yes      |
+| `var`            | `string`    | Name of the variable referring to the current item in the collection.                    |         | yes      |
+| `enable_metrics` | `bool`      | Whether to expose debug metrics in the {{< param "PRODUCT_NAME" >}} `/metrics` endpoint. | `false` | no       |
+| `id`             | `string`    | Name of the field to use from collection items for child component's identification.     | `""`    | no       |
 
 The items in the `collection` list can be of any type [type][types], such as a bool, a string, a list, or a map.
 
 {{< admonition type="warning" >}}
 Setting `enable_metrics` to `true` when `collection` has lots of elements may cause a large number of metrics to appear on the {{< param "PRODUCT_NAME" >}} `/metric` endpoint.
 {{< /admonition >}}
 
+{{< admonition type="note" >}}
+When `id` is set and `enable_metrics` is true, component paths in metrics will use the value of the specified field from collection items instead of a hash of the entire item. For example:
+
+```alloy
+foreach "pods" {
+    collection = [
+        { namespace = "prod", name = "app-1" },
+        { namespace = "dev", name = "app-1" },
+        { namespace = "prod", name = "app-2" }
+    ]
+    var = "each"
+    id = "name"
+    enable_metrics = true
+    template { ... }
+}
+```
+
+This will result in component paths like:
path: docs/sources/reference/config-blocks/foreach.md
user: login: clayton-cornell
body: ```suggestion
This results in component paths like the following:
```
created_at: 2025-05-01T18:09:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,27 +29,56 @@ foreach "<LABEL>" {
 
 You can use the following arguments with `foreach`:
 
-Name             | Type        | Description                                                                              | Default | Required
------------------|-------------|------------------------------------------------------------------------------------------|---------|---------
-`collection`     | `list(any)` | A list of items to loop over.                                                            |         | yes
-`var`            | `string`    | Name of the variable referring to the current item in the collection.                    |         | yes
-`enable_metrics` | `bool`      | Whether to expose debug metrics in the {{< param "PRODUCT_NAME" >}} `/metrics` endpoint. | `false` | no
+| Name             | Type        | Description                                                                              | Default | Required |
+| ---------------- | ----------- | ---------------------------------------------------------------------------------------- | ------- | -------- |
+| `collection`     | `list(any)` | A list of items to loop over.                                                            |         | yes      |
+| `var`            | `string`    | Name of the variable referring to the current item in the collection.                    |         | yes      |
+| `enable_metrics` | `bool`      | Whether to expose debug metrics in the {{< param "PRODUCT_NAME" >}} `/metrics` endpoint. | `false` | no       |
+| `id`             | `string`    | Name of the field to use from collection items for child component's identification.     | `""`    | no       |
 
 The items in the `collection` list can be of any type [type][types], such as a bool, a string, a list, or a map.
 
 {{< admonition type="warning" >}}
 Setting `enable_metrics` to `true` when `collection` has lots of elements may cause a large number of metrics to appear on the {{< param "PRODUCT_NAME" >}} `/metric` endpoint.
 {{< /admonition >}}
 
+{{< admonition type="note" >}}
+When `id` is set and `enable_metrics` is true, component paths in metrics will use the value of the specified field from collection items instead of a hash of the entire item. For example:
+
+```alloy
+foreach "pods" {
+    collection = [
+        { namespace = "prod", name = "app-1" },
+        { namespace = "dev", name = "app-1" },
+        { namespace = "prod", name = "app-2" }
+    ]
+    var = "each"
+    id = "name"
+    enable_metrics = true
+    template { ... }
+}
+```
+
+This will result in component paths like:
+
+```
path: docs/sources/reference/config-blocks/foreach.md
user: login: clayton-cornell
body: ````suggestion
```text
````
created_at: 2025-05-01T18:09:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,52 @@
+This test uses objects in collection with id attribute using object's field.
+
+-- main.alloy --
+foreach "testForeach" {
+  collection = [{"namespace" = "dev", "b" = 3}, {"namespace" = "prod", "b" = 3}, {"namespace" = "dev", "b" = 4}]
+  var = "item"
+  id = "namespace"
+  enable_metrics = true
+
+  template {
+    testcomponents.pulse "pt" {
+      max = item["b"]
+      frequency = "10ms"
+      forward_to = [testcomponents.summation_receiver.sum.receiver]
+    }
+  }
+}
+
+// Similar to testcomponents.summation, but with a "receiver" export
+testcomponents.summation_receiver "sum" {
+}
+
+-- expected_metrics.prom --
+
+# HELP alloy_component_controller_evaluating Tracks if the controller is currently in the middle of a graph evaluation
+# TYPE alloy_component_controller_evaluating gauge
+alloy_component_controller_evaluating{controller_id="",controller_path="/"} 0
+alloy_component_controller_evaluating{controller_id="foreach_dev_1",controller_path="/foreach.testForeach"} 0
+alloy_component_controller_evaluating{controller_id="foreach_dev_2",controller_path="/foreach.testForeach"} 0
+alloy_component_controller_evaluating{controller_id="foreach_prod_1",controller_path="/foreach.testForeach"} 0
+# HELP alloy_component_controller_running_components Total number of running components.
+# TYPE alloy_component_controller_running_components gauge
+alloy_component_controller_running_components{controller_id="",controller_path="/",health_type="healthy"} 2
+alloy_component_controller_running_components{controller_id="foreach_dev_1",controller_path="/foreach.testForeach",health_type="healthy"} 1
+alloy_component_controller_running_components{controller_id="foreach_dev_2",controller_path="/foreach.testForeach",health_type="healthy"} 1
+alloy_component_controller_running_components{controller_id="foreach_prod_1",controller_path="/foreach.testForeach",health_type="healthy"} 1
+# HELP alloy_component_evaluation_queue_size Tracks the number of components waiting to be evaluated in the worker pool
+# TYPE alloy_component_evaluation_queue_size gauge
+alloy_component_evaluation_queue_size{controller_id="",controller_path="/"} 1
+alloy_component_evaluation_queue_size{controller_id="foreach_dev_1",controller_path="/foreach.testForeach"} 0
+alloy_component_evaluation_queue_size{controller_id="foreach_dev_2",controller_path="/foreach.testForeach"} 0
+alloy_component_evaluation_queue_size{controller_id="foreach_prod_1",controller_path="/foreach.testForeach"} 0
+# HELP pulse_count
+# TYPE pulse_count counter
+pulse_count{component_id="testcomponents.pulse.pt",component_path="/foreach.testForeach/foreach_dev_1"} 3
+pulse_count{component_id="testcomponents.pulse.pt",component_path="/foreach.testForeach/foreach_dev_2"} 4
+pulse_count{component_id="testcomponents.pulse.pt",component_path="/foreach.testForeach/foreach_prod_1"} 3
path: internal/runtime/testdata/foreach_metrics/foreach_4.txtar
user: login: wildum
body: looks like the pulse_count is a bit flaky, probably a race condition in the tests. This is not important for your two tests because it should only validate the component_path. I would recommend just checking one metric to simplify it ("alloy_component_controller_running_components") in both tests
created_at: 2025-05-05T08:09:44Z
author_association: CONTRIBUTOR

PR comments:
user: login: wildum
created_at: 2025-05-05T07:30:24Z
body: Could you rebase with main please?
author_association: CONTRIBUTOR
user: login: harshrai654
created_at: 2025-05-05T07:40:53Z
body: Rebased with [grafana:main](https://github.com/grafana/alloy/tree/main).
author_association: CONTRIBUTOR