Repository: prometheus/snmp_exporter (go)

Issue:
Support chained lookups in the generator
https://github.com/prometheus/snmp_exporter/pull/489 added support for chained lookups, but the generator doesn't support this configuration. Consider this list of lookups:
```
    lookups:
      - source_indexes: [cbQosObjectsIndex, cbQosPolicyIndex]
        lookup: cbQosConfigIndex
      - source_indexes: [cbQosConfigIndex]
        lookup: cbQosCMName
```

The order here matters, first it uses [cbQosObjectsIndex, cbQosPolicyIndex] to lookup cbQosConfigIndex, then it uses cbQosConfigIndex to lookup cbQosCMName.

This is already supported (see https://github.com/prometheus/snmp_exporter/issues/405), but the generator doesn't seem to be able to generate a snmp.yml file that would work.

Pull request description:
Support chained lookups in the generator
This PR fixes the generator tree code to add lookups as index if the lookup label is used for another lookup. Example configuration to use it for Cisco's Class based QOS (CISCO-CLASS-BASED-QOS-MIB)

```
modules:
  cisco:
    auth:
      community: public
    walk:
      - interfaces
      - ifXTable
      - 1.3.6.1.4.1.9.9.42
      - 1.3.6.1.4.1.9.9.166
    lookups:
      - source_indexes: [cbQosPolicyIndex, cbQosObjectsIndex]
        lookup: cbQosConfigIndex
      - source_indexes: [cbQosConfigIndex]
        lookup: cbQosCMName
      - source_indexes: [cbQosPolicyIndex]
        lookup: cbQosIfIndex
      - source_indexes: [cbQosIfIndex]
        lookup: ifName
    overrides:
      cbQosConfigIndex:
        ignore: true
      cbQosPolicyIndex:
        ignore: true
      cbQosObjectsIndex:
        ignore: true
      cbQosIfIndex:
        ignore: true
``` 

Fixes: #678 

Patch:
diff --git a/generator/README.md b/generator/README.md
index 1f57f16..adf7d85 100644
--- a/generator/README.md
+++ b/generator/README.md
@@ -99,6 +99,17 @@ modules:
         drop_source_indexes: false  # If true, delete source index labels for this lookup.
                                     # This avoids label clutter when the new index is unique.
 
+      # It is also possible to chain lookups or use multiple labels to gather label values.
+      # This might be helpful to resolve multiple index labels to a proper human readable label.
+      # Please be aware that ordering matters here.
+
+      # In this example, we first do a lookup to get the `cbQosConfigIndex` as another label.
+      - source_indexes: [cbQosPolicyIndex, cbQosObjectsIndex]
+        lookup: cbQosConfigIndex
+      # Using the newly added label, we have another lookup to fetch the `cbQosCMName` based on `cbQosConfigIndex`.
+      - source_indexes: [cbQosConfigIndex]
+        lookup: cbQosCMName
+
      overrides: # Allows for per-module overrides of bits of MIBs
        metricName:
          ignore: true # Drops the metric from the output.
diff --git a/generator/tree.go b/generator/tree.go
index f596c99..9498ee3 100644
--- a/generator/tree.go
+++ b/generator/tree.go
@@ -379,6 +379,13 @@ func generateConfigModule(cfg *ModuleConfig, node *Node, nameToNode map[string]*
 	// Apply lookups.
 	for _, metric := range out.Metrics {
 		toDelete := []string{}
+
+		// Build a list of lookup labels which are required as index.
+		requiredAsIndex := []string{}
+		for _, lookup := range cfg.Lookups {
+			requiredAsIndex = append(requiredAsIndex, lookup.SourceIndexes...)
+		}
+
 		for _, lookup := range cfg.Lookups {
 			foundIndexes := 0
 			// See if all lookup indexes are present.
@@ -407,6 +414,17 @@ func generateConfigModule(cfg *ModuleConfig, node *Node, nameToNode map[string]*
 					l.Labels = append(l.Labels, sanitizeLabelName(oldIndex))
 				}
 				metric.Lookups = append(metric.Lookups, l)
+
+				// If lookup label is used as source index in another lookup,
+				// we need to add this new label as another index.
+				for _, sourceIndex := range requiredAsIndex {
+					if sourceIndex == l.Labelname {
+						idx := &config.Index{Labelname: l.Labelname, Type: l.Type}
+						metric.Indexes = append(metric.Indexes, idx)
+						break
+					}
+				}
+
 				// Make sure we walk the lookup OID(s).
 				if len(tableInstances[metric.Oid]) > 0 {
 					for _, index := range tableInstances[metric.Oid] {


Test patch:
diff --git a/generator/tree_test.go b/generator/tree_test.go
index ec7145a..0846d5d 100644
--- a/generator/tree_test.go
+++ b/generator/tree_test.go
@@ -1240,6 +1240,75 @@ func TestGenerateConfigModule(t *testing.T) {
 				},
 			},
 		},
+		// Multi-index table lookup, chained lookup
+		{
+			node: &Node{Oid: "1", Label: "root",
+				Children: []*Node{
+					{Oid: "1.1", Label: "octet",
+						Children: []*Node{
+							{Oid: "1.1.1", Label: "octetEntry", Indexes: []string{"octetIndex", "octetIndex2"},
+								Children: []*Node{
+									{Oid: "1.1.1.1", Access: "ACCESS_READONLY", Label: "octetIndex", Type: "INTEGER"},
+									{Oid: "1.1.1.2", Access: "ACCESS_READONLY", Label: "octetIndex2", Type: "INTEGER"},
+									{Oid: "1.1.1.3", Access: "ACCESS_READONLY", Label: "octetFoo", Type: "INTEGER"}}},
+							{Oid: "1.1.2", Label: "octetOtherEntry", Indexes: []string{"octetIndex3"},
+								Children: []*Node{
+									{Oid: "1.1.2.1", Access: "ACCESS_READONLY", Label: "octetIndex3", Type: "INTEGER"},
+									{Oid: "1.1.2.2", Access: "ACCESS_READONLY", Label: "octetDesc", Type: "OCTETSTR"}}}}}}},
+			cfg: &ModuleConfig{
+				Walk: []string{"octetFoo"},
+				Lookups: []*Lookup{
+					{
+						SourceIndexes: []string{"octetIndex", "octetIndex2"},
+						Lookup:        "octetIndex3",
+					},
+					{
+						SourceIndexes: []string{"octetIndex3"},
+						Lookup:        "octetDesc",
+					},
+				},
+			},
+			out: &config.Module{
+				// Walk is expanded to include the lookup OID.
+				Walk: []string{"1.1.1.3", "1.1.2.1", "1.1.2.2"},
+				Metrics: []*config.Metric{
+					{
+						Name: "octetFoo",
+						Oid:  "1.1.1.3",
+						Help: " - 1.1.1.3",
+						Type: "gauge",
+						Indexes: []*config.Index{
+							{
+								Labelname: "octetIndex",
+								Type:      "gauge",
+							},
+							{
+								Labelname: "octetIndex2",
+								Type:      "gauge",
+							},
+							{
+								Labelname: "octetIndex3",
+								Type:      "gauge",
+							},
+						},
+						Lookups: []*config.Lookup{
+							{
+								Labels:    []string{"octetIndex", "octetIndex2"},
+								Labelname: "octetIndex3",
+								Type:      "gauge",
+								Oid:       "1.1.2.1",
+							},
+							{
+								Labels:    []string{"octetIndex3"},
+								Labelname: "octetDesc",
+								Type:      "OctetString",
+								Oid:       "1.1.2.2",
+							},
+						},
+					},
+				},
+			},
+		},
 		// Validate metric names.
 		{
 			node: &Node{Oid: "1", Label: "root",