Skip to content

Commit 0ee8928

Browse files
authored
add azure advisor recommendations (#159)
* add azure advisor recommendations * fix messed up diff on README.md
1 parent 72049c6 commit 0ee8928

File tree

7 files changed

+176
-1
lines changed

7 files changed

+176
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ This exporter needs `Reader` permissions on subscription level.
102102
| `azurerm_publicip_info` | Portscan | Azure PublicIP information |
103103
| `azurerm_publicip_portscan_status` | Portscan | Status of scanned ports (finished scan, elapsed time, updated timestamp) |
104104
| `azurerm_publicip_portscan_port` | Portscan | List of opened ports per IP |
105+
| `azurerm_advisor_recommendation` | Advisor | Azure Advisor recommendation |
105106

106107
### ResourceTags handling
107108

@@ -114,4 +115,3 @@ see [armclient tracing documentation](https://github.com/webdevops/go-common/blo
114115
### Caching
115116

116117
see [prometheus collector cache documentation](https://github.com/webdevops/go-common/blob/main/prometheus/README.md#caching)
117-

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type (
1212
General CollectorBase `json:"general"`
1313
Resource CollectorBase `json:"resource"`
1414
Quota CollectorBase `json:"quota"`
15+
Advisor CollectorAdvisor `json:"advisor"`
1516
Defender CollectorBase `json:"defender"`
1617
ResourceHealth CollectorResourceHealth `json:"resourceHealth"`
1718
Iam CollectorBase `json:"iam"`

config/config_advisor.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package config
2+
3+
type (
4+
CollectorAdvisor struct {
5+
*CollectorBase `yaml:",inline"`
6+
7+
ProblemMaxLength int `json:"problemMaxLength"`
8+
SolutionMaxLength int `json:"solutionMaxLength"`
9+
}
10+
)

default.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ collectors:
1313

1414
quota: {}
1515

16+
advisor: {}
17+
1618
defender: {}
1719

1820
resourceHealth: {}

example.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ collectors:
3434
quota:
3535
scrapeTime: 5m
3636

37+
# Azure Advisor recommendations
38+
advisor:
39+
scrapeTime: 5m
40+
# Optional: truncate problem/solution text to avoid excessively long labels
41+
problemMaxLength: 0 # 0 = no truncation
42+
solutionMaxLength: 0 # 0 = no truncation
43+
3744
# Defender (security) metrics
3845
# score, recommendations, ...
3946
defender:

main.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,21 @@ func initMetricCollector() {
281281
logger.With(slog.String("collector", collectorName)).Infof("collector disabled")
282282
}
283283

284+
collectorName = "advisor"
285+
if Config.Collectors.Advisor.IsEnabled() {
286+
c := collector.New(collectorName, &MetricsCollectorAzureRmAdvisor{}, logger)
287+
c.SetScapeTime(*Config.Collectors.Advisor.ScrapeTime)
288+
c.SetCache(
289+
Opts.GetCachePath(collectorName+".json"),
290+
collector.BuildCacheTag(cacheTag, Config.Azure, Config.Collectors.Advisor),
291+
)
292+
if err := c.Start(); err != nil {
293+
logger.Fatal(err.Error())
294+
}
295+
} else {
296+
logger.With(zap.String("collector", collectorName)).Infof("collector disabled")
297+
}
298+
284299
collectorName = "defender"
285300
if Config.Collectors.Defender.IsEnabled() {
286301
c := collector.New(collectorName, &MetricsCollectorAzureRmDefender{}, logger.Slog())

metrics_azurerm_advisor.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package main
2+
3+
import (
4+
"strings"
5+
6+
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/advisor/armadvisor"
7+
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions"
8+
"github.com/prometheus/client_golang/prometheus"
9+
"github.com/webdevops/go-common/prometheus/collector"
10+
"github.com/webdevops/go-common/utils/to"
11+
"go.uber.org/zap"
12+
)
13+
14+
type MetricsCollectorAzureRmAdvisor struct {
15+
collector.Processor
16+
17+
prometheus struct {
18+
advisorRecommendation *prometheus.GaugeVec
19+
}
20+
}
21+
22+
func (m *MetricsCollectorAzureRmAdvisor) Setup(collector *collector.Collector) {
23+
m.Processor.Setup(collector)
24+
25+
m.prometheus.advisorRecommendation = prometheus.NewGaugeVec(
26+
prometheus.GaugeOpts{
27+
Name: "azurerm_advisor_recommendation",
28+
Help: "Azure Advisor recommendation",
29+
},
30+
[]string{
31+
"recommendationID",
32+
"resourceID",
33+
"resourceType",
34+
"category",
35+
"impact",
36+
"risk",
37+
"recommendationSubCategory",
38+
"problem",
39+
"solution",
40+
},
41+
)
42+
m.Collector.RegisterMetricList("advisorRecommendation", m.prometheus.advisorRecommendation, true)
43+
}
44+
45+
func (m *MetricsCollectorAzureRmAdvisor) Reset() {}
46+
47+
func (m *MetricsCollectorAzureRmAdvisor) Collect(callback chan<- func()) {
48+
err := AzureSubscriptionsIterator.ForEachAsync(m.Logger(), func(subscription *armsubscriptions.Subscription, logger *zap.SugaredLogger) {
49+
m.collectAzureAdvisorRecommendations(subscription, logger)
50+
})
51+
if err != nil {
52+
m.Logger().Panic(err)
53+
}
54+
}
55+
56+
func (m *MetricsCollectorAzureRmAdvisor) collectAzureAdvisorRecommendations(subscription *armsubscriptions.Subscription, logger *zap.SugaredLogger) {
57+
client, err := armadvisor.NewRecommendationsClient(*subscription.SubscriptionID, AzureClient.GetCred(), AzureClient.NewArmClientOptions())
58+
if err != nil {
59+
logger.Panic(err)
60+
}
61+
62+
// Generate recommendations first (async operation)
63+
// Note: This is a fire-and-forget operation. The generation is asynchronous,
64+
// so newly generated recommendations will be available in the next scrape cycle.
65+
_, err = client.Generate(m.Context(), nil)
66+
if err != nil {
67+
logger.Warnf("failed to generate recommendations for subscription %s: %v", to.StringLower(subscription.SubscriptionID), err)
68+
}
69+
70+
recommendationMetrics := m.Collector.GetMetricList("advisorRecommendation")
71+
72+
pager := client.NewListPager(nil)
73+
for pager.More() {
74+
result, err := pager.NextPage(m.Context())
75+
if err != nil {
76+
logger.Panic(err)
77+
}
78+
79+
for _, recommendation := range result.Value {
80+
category := ""
81+
if recommendation.Properties.Category != nil {
82+
category = strings.ToLower(string(*recommendation.Properties.Category))
83+
}
84+
85+
risk := ""
86+
if recommendation.Properties.Risk != nil {
87+
risk = strings.ToLower(string(*recommendation.Properties.Risk))
88+
}
89+
90+
impact := ""
91+
if recommendation.Properties.Impact != nil {
92+
impact = strings.ToLower(string(*recommendation.Properties.Impact))
93+
}
94+
95+
problem := ""
96+
solution := ""
97+
if recommendation.Properties.ShortDescription != nil {
98+
problem = to.String(recommendation.Properties.ShortDescription.Problem)
99+
solution = to.String(recommendation.Properties.ShortDescription.Solution)
100+
}
101+
102+
// Truncate problem and solution if configured
103+
if Config.Collectors.Advisor.ProblemMaxLength > 0 {
104+
problem = truncateStrings(problem, Config.Collectors.Advisor.ProblemMaxLength, "...")
105+
}
106+
if Config.Collectors.Advisor.SolutionMaxLength > 0 {
107+
solution = truncateStrings(solution, Config.Collectors.Advisor.SolutionMaxLength, "...")
108+
}
109+
110+
recommendationSubCategory := ""
111+
if recommendation.Properties.ExtendedProperties != nil {
112+
if subCat, ok := recommendation.Properties.ExtendedProperties["recommendationSubCategory"]; ok && subCat != nil {
113+
recommendationSubCategory = to.String(subCat)
114+
}
115+
}
116+
117+
resourceID := ""
118+
if recommendation.Properties.ResourceMetadata != nil && recommendation.Properties.ResourceMetadata.ResourceID != nil {
119+
resourceID = to.String(recommendation.Properties.ResourceMetadata.ResourceID)
120+
}
121+
122+
resourceType := to.StringLower(recommendation.Properties.ImpactedField)
123+
124+
recommendationID := to.StringLower(recommendation.Name)
125+
126+
infoLabels := prometheus.Labels{
127+
"recommendationID": recommendationID,
128+
"resourceID": resourceID,
129+
"resourceType": resourceType,
130+
"category": category,
131+
"impact": impact,
132+
"risk": risk,
133+
"recommendationSubCategory": recommendationSubCategory,
134+
"problem": problem,
135+
"solution": solution,
136+
}
137+
recommendationMetrics.Add(infoLabels, 1)
138+
}
139+
}
140+
}

0 commit comments

Comments
 (0)