Repository: gardener/external-dns-management
Pull request #173 (2021-05-03 12:13:15)

Added requests_per_zone and zone_cache_discardings metrics

**What this PR does / why we need it**:
The metrics `external_dns_management_total_provider_requests` does not contain information about the zone and if there are multiple zones per account, the counts cannot distinguish between them.
This PR addes a metrics `external_dns_management_requests_per_zone` which provides a metrics for each zone and account hash.
Additionally a metrics `external_dns_management_zone_cache_discardings` was added to allow monitoring of zone cache discards. 
The request_test for updates was harmonized for all providers.

**Which issue(s) this PR fixes**:
Fixes #

**Special notes for your reviewer**:

**Release note**:
<!--  Write your release note:
1. Enter your release note in the below block.
2. If no release note is required, just write "NONE" within the block.

Format of block header: <category> <target_group>
Possible values:
- category:       breaking|feature|bugfix|doc|other
- target_group:   user|operator|developer|dependency
-->
```other operator
Added `external_dns_management_requests_per_zone` and `external_dns_management_zone_cache_discardings` metrics
```



Patch:
diff --git a/pkg/controller/provider/alicloud/access.go b/pkg/controller/provider/alicloud/access.go
index e3b9bfed..092ad60c 100644
--- a/pkg/controller/provider/alicloud/access.go
+++ b/pkg/controller/provider/alicloud/access.go
@@ -51,7 +51,7 @@ func GetRR(dnsname, domain string) string {
 
 type Access interface {
 	ListDomains(consume func(domain alidns.Domain) (bool, error)) error
-	ListRecords(domain string, consume func(record alidns.Record) (bool, error)) error
+	ListRecords(zoneID, domain string, consume func(record alidns.Record) (bool, error)) error
 
 	raw.Executor
 }
@@ -83,7 +83,7 @@ func (this *access) ListDomains(consume func(domain alidns.Domain) (bool, error)
 	nextPage := 1
 	rt := provider.M_LISTZONES
 	for {
-		this.metrics.AddRequests(rt, 1)
+		this.metrics.AddGenericRequests(rt, 1)
 		rt = provider.M_PLISTZONES
 		request.PageNumber = requests.NewInteger(nextPage)
 		this.rateLimiter.Accept()
@@ -102,14 +102,14 @@ func (this *access) ListDomains(consume func(domain alidns.Domain) (bool, error)
 	}
 }
 
-func (this *access) ListRecords(domain string, consume func(record alidns.Record) (bool, error)) error {
+func (this *access) ListRecords(zoneID, domain string, consume func(record alidns.Record) (bool, error)) error {
 	request := alidns.CreateDescribeDomainRecordsRequest()
 	request.DomainName = domain
 	request.PageSize = requests.NewInteger(defaultPageSize)
 	nextPage := 1
 	rt := provider.M_LISTRECORDS
 	for {
-		this.metrics.AddRequests(rt, 1)
+		this.metrics.AddZoneRequests(zoneID, rt, 1)
 		rt = provider.M_PLISTRECORDS
 		request.PageNumber = requests.NewInteger(nextPage)
 		this.rateLimiter.Accept()
@@ -128,7 +128,7 @@ func (this *access) ListRecords(domain string, consume func(record alidns.Record
 	}
 }
 
-func (this *access) CreateRecord(r raw.Record) error {
+func (this *access) CreateRecord(r raw.Record, zone provider.DNSHostedZone) error {
 	a := r.(*Record)
 	req := alidns.CreateAddDomainRecordRequest()
 	req.DomainName = a.DomainName
@@ -136,13 +136,13 @@ func (this *access) CreateRecord(r raw.Record) error {
 	req.Type = a.Type
 	req.TTL = requests.NewInteger(a.TTL)
 	req.Value = a.Value
-	this.metrics.AddRequests(provider.M_UPDATERECORDS, 1)
+	this.metrics.AddZoneRequests(zone.Id(), provider.M_UPDATERECORDS, 1)
 	this.rateLimiter.Accept()
 	_, err := this.client.AddDomainRecord(req)
 	return err
 }
 
-func (this *access) UpdateRecord(r raw.Record) error {
+func (this *access) UpdateRecord(r raw.Record, zone provider.DNSHostedZone) error {
 	a := r.(*Record)
 	req := alidns.CreateUpdateDomainRecordRequest()
 	req.RecordId = a.RecordId
@@ -150,16 +150,16 @@ func (this *access) UpdateRecord(r raw.Record) error {
 	req.Type = a.Type
 	req.TTL = requests.NewInteger(a.TTL)
 	req.Value = a.Value
-	this.metrics.AddRequests(provider.M_UPDATERECORDS, 1)
+	this.metrics.AddZoneRequests(zone.Id(), provider.M_UPDATERECORDS, 1)
 	this.rateLimiter.Accept()
 	_, err := this.client.UpdateDomainRecord(req)
 	return err
 }
 
-func (this *access) DeleteRecord(r raw.Record) error {
+func (this *access) DeleteRecord(r raw.Record, zone provider.DNSHostedZone) error {
 	req := alidns.CreateDeleteDomainRecordRequest()
 	req.RecordId = r.GetId()
-	this.metrics.AddRequests(provider.M_UPDATERECORDS, 1)
+	this.metrics.AddZoneRequests(zone.Id(), provider.M_UPDATERECORDS, 1)
 	this.rateLimiter.Accept()
 	_, err := this.client.DeleteDomainRecord(req)
 	return err
diff --git a/pkg/controller/provider/alicloud/handler.go b/pkg/controller/provider/alicloud/handler.go
index 9a20b4b2..b92b1626 100644
--- a/pkg/controller/provider/alicloud/handler.go
+++ b/pkg/controller/provider/alicloud/handler.go
@@ -102,7 +102,7 @@ func (h *Handler) getZones(cache provider.ZoneCache) (provider.DNSHostedZones, e
 				}
 				return true, nil
 			}
-			err := h.access.ListRecords(z.DomainName, f)
+			err := h.access.ListRecords(z.DomainId, z.DomainName, f)
 			if err != nil {
 				if checkAccessForbidden(err) {
 					// It is reasonable for some RAM user, it is only allowed to access certain domain's records detail
@@ -132,7 +132,7 @@ func (h *Handler) getZoneState(zone provider.DNSHostedZone, cache provider.ZoneC
 		//fmt.Printf("**** found %s %s: %s\n", a.GetType(), a.GetDNSName(), a.GetValue() )
 		return true, nil
 	}
-	err := h.access.ListRecords(zone.Key(), f)
+	err := h.access.ListRecords(zone.Id(), zone.Key(), f)
 	if err != nil {
 		return nil, perrs.WrapAsHandlerError(err, "list records failed")
 	}
diff --git a/pkg/controller/provider/aws/execution.go b/pkg/controller/provider/aws/execution.go
index 514659cd..907eaed0 100644
--- a/pkg/controller/provider/aws/execution.go
+++ b/pkg/controller/provider/aws/execution.go
@@ -124,7 +124,7 @@ func (this *Execution) submitChanges(metrics provider.Metrics) error {
 			},
 		}
 
-		metrics.AddRequests(provider.M_UPDATERECORDS, 1)
+		metrics.AddZoneRequests(this.zone.Id(), provider.M_UPDATERECORDS, 1)
 		this.rateLimiter.Accept()
 		if _, err := this.r53.ChangeResourceRecordSets(params); err != nil {
 			this.Errorf("%d records in zone %s fail: %s", len(changes), this.zone.Id(), err)
diff --git a/pkg/controller/provider/aws/handler.go b/pkg/controller/provider/aws/handler.go
index fbf29b90..525082c2 100644
--- a/pkg/controller/provider/aws/handler.go
+++ b/pkg/controller/provider/aws/handler.go
@@ -132,7 +132,7 @@ func (h *Handler) getZones(cache provider.ZoneCache) (provider.DNSHostedZones, e
 		for _, zone := range resp.HostedZones {
 			raw = append(raw, zone)
 		}
-		h.config.Metrics.AddRequests(rt, 1)
+		h.config.Metrics.AddGenericRequests(rt, 1)
 		rt = provider.M_PLISTZONES
 		return true
 	}
@@ -224,7 +224,7 @@ func (h *Handler) handleRecordSets(zone provider.DNSHostedZone, f func(rs *route
 	inp := (&route53.ListResourceRecordSetsInput{}).SetHostedZoneId(zone.Id())
 	forwarded := []string{}
 	aggr := func(resp *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool) {
-		h.config.Metrics.AddRequests(rt, 1)
+		h.config.Metrics.AddZoneRequests(zone.Id(), rt, 1)
 		for _, r := range resp.ResourceRecordSets {
 			f(r)
 			if aws.StringValue(r.Type) == dns.RS_NS {
diff --git a/pkg/controller/provider/azure/execution.go b/pkg/controller/provider/azure/execution.go
index 0b0e53fa..a29abd66 100644
--- a/pkg/controller/provider/azure/execution.go
+++ b/pkg/controller/provider/azure/execution.go
@@ -137,13 +137,15 @@ func (exec *Execution) update(recordType azure.RecordType, rset *azure.RecordSet
 	exec.handler.config.RateLimiter.Accept()
 	_, err := exec.handler.recordsClient.CreateOrUpdate(exec.handler.ctx, exec.resourceGroup, exec.zoneName, *rset.Name,
 		recordType, *rset, "", "")
-	metrics.AddRequests("RecordSetsClient_CreateOrUpdate", 1)
+	zoneID := makeZoneID(exec.resourceGroup, exec.zoneName)
+	metrics.AddZoneRequests(zoneID, provider.M_UPDATERECORDS, 1)
 	return err
 }
 
 func (exec *Execution) delete(recordType azure.RecordType, rset *azure.RecordSet, metrics provider.Metrics) error {
 	exec.handler.config.RateLimiter.Accept()
 	_, err := exec.handler.recordsClient.Delete(exec.handler.ctx, exec.resourceGroup, exec.zoneName, *rset.Name, recordType, "")
-	metrics.AddRequests("RecordSetsClient_Delete", 1)
+	zoneID := makeZoneID(exec.resourceGroup, exec.zoneName)
+	metrics.AddZoneRequests(zoneID, provider.M_DELETERECORDS, 1)
 	return err
 }
diff --git a/pkg/controller/provider/azure/handler.go b/pkg/controller/provider/azure/handler.go
index 3b18ee8d..dbd04162 100644
--- a/pkg/controller/provider/azure/handler.go
+++ b/pkg/controller/provider/azure/handler.go
@@ -115,7 +115,7 @@ func (h *Handler) getZones(cache provider.ZoneCache) (provider.DNSHostedZones, e
 	zones := provider.DNSHostedZones{}
 	h.config.RateLimiter.Accept()
 	results, err := h.zonesClient.ListComplete(h.ctx, nil)
-	h.config.Metrics.AddRequests("ZonesClient_ListComplete", 1)
+	h.config.Metrics.AddGenericRequests(provider.M_LISTZONES, 1)
 	if err != nil {
 		return nil, perrs.WrapAsHandlerError(err, "Listing DNS zones failed")
 	}
@@ -133,7 +133,7 @@ func (h *Handler) getZones(cache provider.ZoneCache) (provider.DNSHostedZones, e
 		forwarded := h.collectForwardedSubzones(resourceGroup, *item.Name)
 
 		// ResourceGroup needed for requests to Azure. Remember by adding to Id. Split by calling splitZoneid().
-		hostedZone := provider.NewDNSHostedZone(h.ProviderType(), resourceGroup+"/"+*item.Name, dns.NormalizeHostname(*item.Name), "", forwarded, false)
+		hostedZone := provider.NewDNSHostedZone(h.ProviderType(), makeZoneID(resourceGroup, *item.Name), dns.NormalizeHostname(*item.Name), "", forwarded, false)
 
 		zones = append(zones, hostedZone)
 	}
@@ -147,7 +147,8 @@ func (h *Handler) collectForwardedSubzones(resourceGroup, zoneName string) []str
 	var top int32 = 1000
 	h.config.RateLimiter.Accept()
 	result, err := h.recordsClient.ListByType(h.ctx, resourceGroup, zoneName, azure.NS, &top, "")
-	h.config.Metrics.AddRequests("RecordSetsClient_ListByType_NS", 1)
+	zoneID := makeZoneID(resourceGroup, zoneName)
+	h.config.Metrics.AddZoneRequests(zoneID, provider.M_LISTRECORDS, 1)
 	if err != nil {
 		logger.Infof("Failed fetching NS records for %s: %s", zoneName, err.Error())
 		// just ignoring it
@@ -163,6 +164,10 @@ func (h *Handler) collectForwardedSubzones(resourceGroup, zoneName string) []str
 	return forwarded
 }
 
+func makeZoneID(resourceGroup, zoneName string) string {
+	return resourceGroup + "/" + zoneName
+}
+
 func splitZoneid(zoneid string) (string, string) {
 	parts := strings.Split(zoneid, "/")
 	if len(parts) != 2 {
@@ -181,12 +186,14 @@ func (h *Handler) getZoneState(zone provider.DNSHostedZone, cache provider.ZoneC
 	resourceGroup, zoneName := splitZoneid(zone.Id())
 	h.config.RateLimiter.Accept()
 	results, err := h.recordsClient.ListAllByDNSZoneComplete(h.ctx, resourceGroup, zoneName, nil, "")
-	h.config.Metrics.AddRequests("RecordSetsClient_ListAllByDNSZoneComplete", 1)
+	h.config.Metrics.AddZoneRequests(zone.Id(), provider.M_LISTRECORDS, 1)
 	if err != nil {
 		return nil, perrs.WrapfAsHandlerError(err, "Listing DNS zone state for zone %s failed", zoneName)
 	}
 
+	count := 0
 	for ; results.NotDone(); results.Next() {
+		count++
 		item := results.Value()
 		// We expect recordName.DNSZone. However Azure only return recordName . Reverse is dropZoneName() needed for calls to Azure
 		fullName := fmt.Sprintf("%s.%s", *item.Name, zoneName)
@@ -218,6 +225,10 @@ func (h *Handler) getZoneState(zone provider.DNSHostedZone, cache provider.ZoneC
 			dnssets.AddRecordSetFromProvider(fullName, rs)
 		}
 	}
+	pages := count / 100
+	if pages > 0 {
+		h.config.Metrics.AddZoneRequests(zone.Id(), provider.M_PLISTRECORDS, count/100)
+	}
 	return provider.NewDNSZoneState(dnssets), nil
 }
 
diff --git a/pkg/controller/provider/cloudflare/access.go b/pkg/controller/provider/cloudflare/access.go
index edaca13e..0daa98ad 100644
--- a/pkg/controller/provider/cloudflare/access.go
+++ b/pkg/controller/provider/cloudflare/access.go
@@ -46,7 +46,7 @@ func NewAccess(apiToken string, metrics provider.Metrics, rateLimiter flowcontro
 }
 
 func (this *access) ListZones(consume func(zone cloudflare.Zone) (bool, error)) error {
-	this.metrics.AddRequests(provider.M_LISTZONES, 1)
+	this.metrics.AddGenericRequests(provider.M_LISTZONES, 1)
 	this.rateLimiter.Accept()
 	results, err := this.API.ListZones()
 	if err != nil {
@@ -61,7 +61,7 @@ func (this *access) ListZones(consume func(zone cloudflare.Zone) (bool, error))
 }
 
 func (this *access) ListRecords(zoneId string, consume func(record cloudflare.DNSRecord) (bool, error)) error {
-	this.metrics.AddRequests(provider.M_LISTRECORDS, 1)
+	this.metrics.AddZoneRequests(zoneId, provider.M_LISTRECORDS, 1)
 	this.rateLimiter.Accept()
 	results, err := this.DNSRecords(zoneId, cloudflare.DNSRecord{})
 	if err != nil {
@@ -75,7 +75,7 @@ func (this *access) ListRecords(zoneId string, consume func(record cloudflare.DN
 	return nil
 }
 
-func (this *access) CreateRecord(r raw.Record) error {
+func (this *access) CreateRecord(r raw.Record, zone provider.DNSHostedZone) error {
 	a := r.(*Record)
 	ttl := r.GetTTL()
 	testTTL(&ttl)
@@ -86,13 +86,13 @@ func (this *access) CreateRecord(r raw.Record) error {
 		TTL:     ttl,
 		ZoneID:  a.ZoneID,
 	}
-	this.metrics.AddRequests(provider.M_CREATERECORDS, 1)
+	this.metrics.AddZoneRequests(zone.Id(), provider.M_CREATERECORDS, 1)
 	this.rateLimiter.Accept()
 	_, err := this.CreateDNSRecord(a.ZoneID, dnsRecord)
 	return err
 }
 
-func (this *access) UpdateRecord(r raw.Record) error {
+func (this *access) UpdateRecord(r raw.Record, zone provider.DNSHostedZone) error {
 	a := r.(*Record)
 	ttl := r.GetTTL()
 	testTTL(&ttl)
@@ -103,15 +103,15 @@ func (this *access) UpdateRecord(r raw.Record) error {
 		TTL:     ttl,
 		ZoneID:  a.ZoneID,
 	}
-	this.metrics.AddRequests(provider.M_UPDATERECORDS, 1)
+	this.metrics.AddZoneRequests(zone.Id(), provider.M_UPDATERECORDS, 1)
 	this.rateLimiter.Accept()
 	err := this.UpdateDNSRecord(a.ZoneID, r.GetId(), dnsRecord)
 	return err
 }
 
-func (this *access) DeleteRecord(r raw.Record) error {
+func (this *access) DeleteRecord(r raw.Record, zone provider.DNSHostedZone) error {
 	a := r.(*Record)
-	this.metrics.AddRequests(provider.M_DELETERECORDS, 1)
+	this.metrics.AddZoneRequests(zone.Id(), provider.M_DELETERECORDS, 1)
 	this.rateLimiter.Accept()
 	err := this.DeleteDNSRecord(a.ZoneID, r.GetId())
 	return err
diff --git a/pkg/controller/provider/google/execution.go b/pkg/controller/provider/google/execution.go
index e31a1ce1..53d53cc2 100644
--- a/pkg/controller/provider/google/execution.go
+++ b/pkg/controller/provider/google/execution.go
@@ -94,7 +94,7 @@ func (this *Execution) submitChanges(metrics provider.Metrics) error {
 		this.Infof("desired change: Addition %s %s: %s", c.Name, c.Type, utils.Strings(c.Rrdatas...))
 	}
 
-	metrics.AddRequests(provider.M_UPDATERECORDS, 1)
+	metrics.AddZoneRequests(this.zone.Id(), provider.M_UPDATERECORDS, 1)
 	this.handler.config.RateLimiter.Accept()
 	projectID, zoneName := SplitZoneID(this.zone.Id())
 	if _, err := this.handler.service.Changes.Create(projectID, zoneName, this.change).Do(); err != nil {
diff --git a/pkg/controller/provider/google/handler.go b/pkg/controller/provider/google/handler.go
index 27e27917..c6dfff69 100644
--- a/pkg/controller/provider/google/handler.go
+++ b/pkg/controller/provider/google/handler.go
@@ -110,7 +110,7 @@ func (h *Handler) getZones(cache provider.ZoneCache) (provider.DNSHostedZones, e
 		for _, zone := range resp.ManagedZones {
 			raw = append(raw, zone)
 		}
-		h.config.Metrics.AddRequests(rt, 1)
+		h.config.Metrics.AddGenericRequests(rt, 1)
 		rt = provider.M_PLISTZONES
 		return nil
 	}
@@ -153,7 +153,7 @@ func (h *Handler) handleRecordSets(zone provider.DNSHostedZone, f func(r *google
 				}
 			}
 		}
-		h.config.Metrics.AddRequests(rt, 1)
+		h.config.Metrics.AddZoneRequests(zone.Id(), rt, 1)
 		rt = provider.M_PLISTRECORDS
 		return nil
 	}
diff --git a/pkg/controller/provider/infoblox/access.go b/pkg/controller/provider/infoblox/access.go
index d36192df..e5e9eef4 100644
--- a/pkg/controller/provider/infoblox/access.go
+++ b/pkg/controller/provider/infoblox/access.go
@@ -44,20 +44,20 @@ func NewAccess(client ibclient.IBConnector, view string, metrics provider.Metric
 	}
 }
 
-func (this *access) CreateRecord(r raw.Record) error {
-	this.metrics.AddRequests(provider.M_CREATERECORDS, 1)
+func (this *access) CreateRecord(r raw.Record, zone provider.DNSHostedZone) error {
+	this.metrics.AddZoneRequests(zone.Id(), provider.M_CREATERECORDS, 1)
 	_, err := this.CreateObject(r.(ibclient.IBObject))
 	return err
 }
 
-func (this *access) UpdateRecord(r raw.Record) error {
-	this.metrics.AddRequests(provider.M_CREATERECORDS, 1)
+func (this *access) UpdateRecord(r raw.Record, zone provider.DNSHostedZone) error {
+	this.metrics.AddZoneRequests(zone.Id(), provider.M_CREATERECORDS, 1)
 	_, err := this.UpdateObject(r.(Record).PrepareUpdate().(ibclient.IBObject), r.GetId())
 	return err
 }
 
-func (this *access) DeleteRecord(r raw.Record) error {
-	this.metrics.AddRequests(provider.M_DELETERECORDS, 1)
+func (this *access) DeleteRecord(r raw.Record, zone provider.DNSHostedZone) error {
+	this.metrics.AddZoneRequests(zone.Id(), provider.M_DELETERECORDS, 1)
 	_, err := this.DeleteObject(r.GetId())
 	return err
 }
diff --git a/pkg/controller/provider/infoblox/handler.go b/pkg/controller/provider/infoblox/handler.go
index 9d65a3b1..1a7960dd 100644
--- a/pkg/controller/provider/infoblox/handler.go
+++ b/pkg/controller/provider/infoblox/handler.go
@@ -181,7 +181,7 @@ func NewHandler(config *provider.DNSHandlerConfig) (provider.DNSHandler, error)
 
 func (h *Handler) getZones(cache provider.ZoneCache) (provider.DNSHostedZones, error) {
 	var raw []ibclient.ZoneAuth
-	h.config.Metrics.AddRequests(provider.M_LISTZONES, 1)
+	h.config.Metrics.AddGenericRequests(provider.M_LISTZONES, 1)
 	obj := ibclient.NewZoneAuth(ibclient.ZoneAuth{})
 	err := h.access.GetObject(obj, "", &raw)
 	if err != nil {
@@ -190,7 +190,7 @@ func (h *Handler) getZones(cache provider.ZoneCache) (provider.DNSHostedZones, e
 
 	zones := provider.DNSHostedZones{}
 	for _, z := range raw {
-		h.config.Metrics.AddRequests(provider.M_LISTRECORDS, 1)
+		h.config.Metrics.AddZoneRequests(z.Ref, provider.M_LISTRECORDS, 1)
 		var resN []RecordNS
 		objN := ibclient.NewRecordNS(
 			ibclient.RecordNS{
@@ -218,7 +218,7 @@ func (h *Handler) getZoneState(zone provider.DNSHostedZone, cache provider.ZoneC
 	state := raw.NewState()
 	rt := provider.M_LISTRECORDS
 
-	h.config.Metrics.AddRequests(rt, 1)
+	h.config.Metrics.AddZoneRequests(zone.Id(), rt, 1)
 	var resA []RecordA
 	objA := ibclient.NewRecordA(
 		ibclient.RecordA{
@@ -234,7 +234,7 @@ func (h *Handler) getZoneState(zone provider.DNSHostedZone, cache provider.ZoneC
 		state.AddRecord((*RecordA)(&res).Copy())
 	}
 
-	h.config.Metrics.AddRequests(rt, 1)
+	h.config.Metrics.AddZoneRequests(zone.Id(), rt, 1)
 	var resC []RecordCNAME
 	objC := ibclient.NewRecordCNAME(
 		ibclient.RecordCNAME{
@@ -250,7 +250,7 @@ func (h *Handler) getZoneState(zone provider.DNSHostedZone, cache provider.ZoneC
 		state.AddRecord((*RecordCNAME)(&res).Copy())
 	}
 
-	h.config.Metrics.AddRequests(rt, 1)
+	h.config.Metrics.AddZoneRequests(zone.Id(), rt, 1)
 	var resT []RecordTXT
 	objT := ibclient.NewRecordTXT(
 		ibclient.RecordTXT{
diff --git a/pkg/controller/provider/netlify/access.go b/pkg/controller/provider/netlify/access.go
index 4307fd6d..c5724484 100644
--- a/pkg/controller/provider/netlify/access.go
+++ b/pkg/controller/provider/netlify/access.go
@@ -57,7 +57,7 @@ func NewAccess(apiToken string, metrics provider.Metrics, rateLimiter flowcontro
 }
 
 func (this *access) ListZones(consume func(zone models.DNSZone) (bool, error)) error {
-	this.metrics.AddRequests(provider.M_LISTZONES, 1)
+	this.metrics.AddGenericRequests(provider.M_LISTZONES, 1)
 	this.rateLimiter.Accept()
 	results, err := this.client.GetDNSZones(nil, this.authInfo)
 	if err != nil {
@@ -71,11 +71,11 @@ func (this *access) ListZones(consume func(zone models.DNSZone) (bool, error)) e
 	return nil
 }
 
-func (this *access) ListRecords(zoneId string, consume func(record models.DNSRecord) (bool, error)) error {
-	this.metrics.AddRequests(provider.M_LISTRECORDS, 1)
+func (this *access) ListRecords(zoneID string, consume func(record models.DNSRecord) (bool, error)) error {
+	this.metrics.AddZoneRequests(zoneID, provider.M_LISTRECORDS, 1)
 	this.rateLimiter.Accept()
 	params := operations.NewGetDNSRecordsParams()
-	params.ZoneID = zoneId
+	params.ZoneID = zoneID
 	results, err := this.client.GetDNSRecords(params, this.authInfo)
 	if err != nil {
 		return err
@@ -88,7 +88,7 @@ func (this *access) ListRecords(zoneId string, consume func(record models.DNSRec
 	return nil
 }
 
-func (this *access) CreateRecord(r raw.Record) error {
+func (this *access) CreateRecord(r raw.Record, zone provider.DNSHostedZone) error {
 	a := r.(*Record)
 	ttl := r.GetTTL()
 	testTTL(&ttl)
@@ -98,7 +98,7 @@ func (this *access) CreateRecord(r raw.Record) error {
 		Value:    r.GetValue(),
 		TTL:      int64(ttl),
 	}
-	this.metrics.AddRequests(provider.M_CREATERECORDS, 1)
+	this.metrics.AddZoneRequests(zone.Id(), provider.M_CREATERECORDS, 1)
 	this.rateLimiter.Accept()
 	createParams := operations.NewCreateDNSRecordParams()
 	createParams.SetZoneID(a.DNSZoneID)
@@ -107,22 +107,22 @@ func (this *access) CreateRecord(r raw.Record) error {
 	return err
 }
 
-func (this *access) UpdateRecord(r raw.Record) error {
+func (this *access) UpdateRecord(r raw.Record, zone provider.DNSHostedZone) error {
 	// Netlify does not support updating a record
 	// Delete the existing record and re-create it
-	err := this.DeleteRecord(r)
+	err := this.DeleteRecord(r, zone)
 	if err != nil {
 		return err
 	}
-	return this.CreateRecord(r)
+	return this.CreateRecord(r, zone)
 }
 
-func (this *access) DeleteRecord(r raw.Record) error {
+func (this *access) DeleteRecord(r raw.Record, zone provider.DNSHostedZone) error {
 	a := r.(*Record)
 	deleteParams := operations.NewDeleteDNSRecordParams()
 	deleteParams.SetZoneID(a.DNSZoneID)
 	deleteParams.SetDNSRecordID(a.ID)
-	this.metrics.AddRequests(provider.M_DELETERECORDS, 1)
+	this.metrics.AddZoneRequests(zone.Id(), provider.M_DELETERECORDS, 1)
 	this.rateLimiter.Accept()
 	_, err := this.client.DeleteDNSRecord(deleteParams, this.authInfo)
 	return err
diff --git a/pkg/controller/provider/openstack/designateclient.go b/pkg/controller/provider/openstack/designateclient.go
index 0cf8a2df..bcfb240e 100644
--- a/pkg/controller/provider/openstack/designateclient.go
+++ b/pkg/controller/provider/openstack/designateclient.go
@@ -53,9 +53,6 @@ type designateClientInterface interface {
 
 	// DeleteRecordSet deletes recordset in the given DNS zone
 	DeleteRecordSet(zoneID, recordSetID string) error
-
-	// GetRecordSet gets recordset by its ID
-	GetRecordSet(zoneID, recordSetID string, handler func(recordSet *recordsets.RecordSet) error) error
 }
 
 // implementation of the designateClientInterface
@@ -148,7 +145,7 @@ func (c designateClient) ForEachZone(handler func(zone *zones.Zone) error) error
 	rt := provider.M_LISTZONES
 	return pager.EachPage(
 		func(page pagination.Page) (bool, error) {
-			c.metrics.AddRequests(rt, 1)
+			c.metrics.AddGenericRequests(rt, 1)
 			rt = provider.M_PLISTZONES
 			list, err := zones.ExtractZones(page)
 			if err != nil {
@@ -176,7 +173,7 @@ func (c designateClient) ForEachRecordSetFilterByTypeAndName(zoneID string, rrty
 	rt := provider.M_LISTRECORDS
 	return pager.EachPage(
 		func(page pagination.Page) (bool, error) {
-			c.metrics.AddRequests(rt, 1)
+			c.metrics.AddZoneRequests(zoneID, rt, 1)
 			rt = provider.M_PLISTRECORDS
 			list, err := recordsets.ExtractRecordSets(page)
 			if err != nil {
@@ -196,7 +193,7 @@ func (c designateClient) ForEachRecordSetFilterByTypeAndName(zoneID string, rrty
 // CreateRecordSet creates recordset in the given DNS zone
 func (c designateClient) CreateRecordSet(zoneID string, opts recordsets.CreateOpts) (string, error) {
 	r, err := recordsets.Create(c.serviceClient, zoneID, opts).Extract()
-	c.metrics.AddRequests("RecordSets_Create", 1)
+	c.metrics.AddZoneRequests(zoneID, provider.M_CREATERECORDS, 1)
 	if err != nil {
 		return "", err
 	}
@@ -206,23 +203,13 @@ func (c designateClient) CreateRecordSet(zoneID string, opts recordsets.CreateOp
 // UpdateRecordSet updates recordset in the given DNS zone
 func (c designateClient) UpdateRecordSet(zoneID, recordSetID string, opts recordsets.UpdateOpts) error {
 	_, err := recordsets.Update(c.serviceClient, zoneID, recordSetID, opts).Extract()
-	c.metrics.AddRequests("RecordSets_Update", 1)
+	c.metrics.AddZoneRequests(zoneID, provider.M_UPDATERECORDS, 1)
 	return err
 }
 
 // DeleteRecordSet deletes recordset in the given DNS zone
 func (c designateClient) DeleteRecordSet(zoneID, recordSetID string) error {
 	err := recordsets.Delete(c.serviceClient, zoneID, recordSetID).ExtractErr()
-	c.metrics.AddRequests("RecordSets_Delete", 1)
+	c.metrics.AddZoneRequests(zoneID, provider.M_DELETERECORDS, 1)
 	return err
 }
-
-// GetRecordSet gets single recordset by its ID
-func (c designateClient) GetRecordSet(zoneID, recordSetID string, handler func(recordSet *recordsets.RecordSet) error) error {
-	rs, err := recordsets.Get(c.serviceClient, zoneID, recordSetID).Extract()
-	c.metrics.AddRequests("RecordSets_Get", 1)
-	if err != nil {
-		return err
-	}
-	return handler(rs)
-}
diff --git a/pkg/dns/provider/inmemory.go b/pkg/dns/provider/inmemory.go
index b55dd03f..499e8c86 100644
--- a/pkg/dns/provider/inmemory.go
+++ b/pkg/dns/provider/inmemory.go
@@ -99,13 +99,13 @@ func (m *InMemory) AddZone(zone DNSHostedZone) bool {
 	return true
 }
 
-func (m *InMemory) Apply(zoneId string, request *ChangeRequest, metrics Metrics) error {
+func (m *InMemory) Apply(zoneID string, request *ChangeRequest, metrics Metrics) error {
 	m.lock.Lock()
 	defer m.lock.Unlock()
 
-	data, ok := m.zones[zoneId]
+	data, ok := m.zones[zoneID]
 	if !ok {
-		return fmt.Errorf("DNSZone %s not hosted", zoneId)
+		return fmt.Errorf("DNSZone %s not hosted", zoneID)
 	}
 
 	name, rset := buildRecordSet(request)
@@ -113,10 +113,10 @@ func (m *InMemory) Apply(zoneId string, request *ChangeRequest, metrics Metrics)
 	switch request.Action {
 	case R_CREATE, R_UPDATE:
 		data.dnssets.AddRecordSet(name, rset)
-		metrics.AddRequests("AddRecordSet", 1)
+		metrics.AddZoneRequests(zoneID, M_UPDATERECORDS, 1)
 	case R_DELETE:
 		data.dnssets.RemoveRecordSet(name, rset.Type)
-		metrics.AddRequests("RemoveRecordSet", 1)
+		metrics.AddZoneRequests(zoneID, M_DELETERECORDS, 1)
 	}
 	return nil
 }
diff --git a/pkg/dns/provider/interface.go b/pkg/dns/provider/interface.go
index 66b17d68..9c22a664 100644
--- a/pkg/dns/provider/interface.go
+++ b/pkg/dns/provider/interface.go
@@ -174,7 +174,8 @@ const (
 )
 
 type Metrics interface {
-	AddRequests(request_type string, n int)
+	AddGenericRequests(request_type string, n int)
+	AddZoneRequests(zoneID, request_type string, n int)
 }
 
 type Finalizers interface {
diff --git a/pkg/dns/provider/provider.go b/pkg/dns/provider/provider.go
index c551deb5..ea0e7faa 100644
--- a/pkg/dns/provider/provider.go
+++ b/pkg/dns/provider/provider.go
@@ -84,8 +84,12 @@ func NewDNSAccount(config utils.Properties, handler DNSHandler, hash string) *DN
 	}
 }
 
-func (this *DNSAccount) AddRequests(requestType string, n int) {
-	metrics.AddRequests(this.handler.ProviderType(), this.hash, requestType, n)
+func (this *DNSAccount) AddGenericRequests(requestType string, n int) {
+	metrics.AddRequests(this.handler.ProviderType(), this.hash, requestType, n, nil)
+}
+
+func (this *DNSAccount) AddZoneRequests(zoneID, requestType string, n int) {
+	metrics.AddRequests(this.handler.ProviderType(), this.hash, requestType, n, &zoneID)
 }
 
 func (this *DNSAccount) ProviderType() string {
diff --git a/pkg/dns/provider/raw/execution.go b/pkg/dns/provider/raw/execution.go
index 2e5f88dc..d61130af 100644
--- a/pkg/dns/provider/raw/execution.go
+++ b/pkg/dns/provider/raw/execution.go
@@ -26,9 +26,9 @@ import (
 )
 
 type Executor interface {
-	CreateRecord(r Record) error
-	UpdateRecord(r Record) error
-	DeleteRecord(r Record) error
+	CreateRecord(r Record, zone provider.DNSHostedZone) error
+	UpdateRecord(r Record, zone provider.DNSHostedZone) error
+	DeleteRecord(r Record, zone provider.DNSHostedZone) error
 
 	NewRecord(fqdn, rtype, value string, zone provider.DNSHostedZone, ttl int64) Record
 }
@@ -171,8 +171,8 @@ func (this *Execution) SubmitChanges() error {
 	return nil
 }
 
-func (this *Execution) submit(f func(record Record) error, r Record) {
-	err := f(r)
+func (this *Execution) submit(f func(record Record, zone provider.DNSHostedZone) error, r Record) {
+	err := f(r, this.zone)
 	if err != nil {
 		res := this.results[r.GetDNSName()]
 		if res != nil {
diff --git a/pkg/dns/provider/utils.go b/pkg/dns/provider/utils.go
index 3d973de5..ff9bbb2f 100644
--- a/pkg/dns/provider/utils.go
+++ b/pkg/dns/provider/utils.go
@@ -24,7 +24,10 @@ type NullMetrics struct{}
 
 var _ Metrics = &NullMetrics{}
 
-func (m *NullMetrics) AddRequests(request_type string, n int) {
+func (m *NullMetrics) AddGenericRequests(request_type string, n int) {
+}
+
+func (m *NullMetrics) AddZoneRequests(zon, request_type string, n int) {
 }
 
 func copyZones(src map[string]*dnsHostedZone) dnsHostedZones {
diff --git a/pkg/dns/provider/zonecache.go b/pkg/dns/provider/zonecache.go
index 5650fbd4..853c12a0 100644
--- a/pkg/dns/provider/zonecache.go
+++ b/pkg/dns/provider/zonecache.go
@@ -29,6 +29,7 @@ import (
 	"time"
 
 	"github.com/gardener/controller-manager-library/pkg/logger"
+	"github.com/gardener/external-dns-management/pkg/server/metrics"
 	"k8s.io/apimachinery/pkg/runtime"
 
 	"github.com/gardener/external-dns-management/pkg/dns"
@@ -250,7 +251,7 @@ func (c *defaultZoneCache) GetZones() (DNSHostedZones, error) {
 		}
 		c.state.RestrictCacheToZones(c.zones)
 	} else {
-		c.metrics.AddRequests(M_CACHED_GETZONES, 1)
+		c.metrics.AddGenericRequests(M_CACHED_GETZONES, 1)
 	}
 	return c.zones, c.zonesErr
 }
@@ -272,7 +273,7 @@ func (c *defaultZoneCache) clearBackoff() {
 func (c *defaultZoneCache) GetZoneState(zone DNSHostedZone) (DNSZoneState, error) {
 	state, cached, err := c.state.GetZoneState(zone, c)
 	if cached {
-		c.metrics.AddRequests(M_CACHED_GETZONESTATE, 1)
+		c.metrics.AddZoneRequests(zone.Id(), M_CACHED_GETZONESTATE, 1)
 	} else {
 		c.persistZone(zone)
 	}
@@ -302,6 +303,7 @@ func (c *defaultZoneCache) ApplyRequests(logctx logger.LogContext, err error, zo
 		if !errors.IsThrottlingError(err) {
 			logctx.Infof("zone cache discarded because of error during ExecuteRequests")
 			c.deleteZoneState(zone)
+			metrics.AddZoneCacheDiscarding(zone.ProviderType(), zone.Id())
 		} else {
 			logctx.Infof("zone cache untouched (only throttling during ExecuteRequests)")
 		}
diff --git a/pkg/server/metrics/metrics.go b/pkg/server/metrics/metrics.go
index 8d0be424..bea64318 100644
--- a/pkg/server/metrics/metrics.go
+++ b/pkg/server/metrics/metrics.go
@@ -31,6 +31,8 @@ import (
 
 func init() {
 	prometheus.MustRegister(Requests)
+	prometheus.MustRegister(ZoneRequests)
+	prometheus.MustRegister(ZoneCacheDiscardings)
 	prometheus.MustRegister(Accounts)
 	prometheus.MustRegister(Entries)
 	prometheus.MustRegister(StaleEntries)
@@ -48,6 +50,22 @@ var (
 		[]string{"providertype", "accounthash", "requesttype"},
 	)
 
+	ZoneRequests = prometheus.NewCounterVec(
+		prometheus.CounterOpts{
+			Name: "external_dns_management_requests_per_zone",
+			Help: "Requests per provider type, credential set, and zone",
+		},
+		[]string{"providertype", "accounthash", "requesttype", "zone"},
+	)
+
+	ZoneCacheDiscardings = prometheus.NewCounterVec(
+		prometheus.CounterOpts{
+			Name: "external_dns_management_zone_cache_discardings",
+			Help: "Discardings of zone cache per provider type and zone",
+		},
+		[]string{"providertype", "zone"},
+	)
+
 	Accounts = prometheus.NewGaugeVec(
 		prometheus.GaugeOpts{
 			Name: "external_dns_management_account_providers",
@@ -129,9 +147,16 @@ func ReportAccountProviders(ptype, account string, amount int) {
 	Accounts.WithLabelValues(ptype, account).Set(float64(amount))
 }
 
-func AddRequests(ptype, account, requestType string, no int) {
+func AddRequests(ptype, account, requestType string, no int, zone *string) {
 	theRequestLabels.AddRequestLabel(ptype, account, requestType)
 	Requests.WithLabelValues(ptype, account, requestType).Add(float64(no))
+	if zone != nil {
+		ZoneRequests.WithLabelValues(ptype, account, requestType, *zone).Add(float64(no))
+	}
+}
+
+func AddZoneCacheDiscarding(ptype, zone string) {
+	ZoneCacheDiscardings.WithLabelValues(ptype, zone).Add(float64(1))
 }
 
 type ZoneProviderTypes struct {


Test patch:
diff --git a/pkg/controller/provider/openstack/handler_test.go b/pkg/controller/provider/openstack/handler_test.go
index c9d5ef0c..fce1f276 100644
--- a/pkg/controller/provider/openstack/handler_test.go
+++ b/pkg/controller/provider/openstack/handler_test.go
@@ -178,14 +178,6 @@ func (c *designateMockClient) DeleteRecordSet(zoneID, recordSetID string) error
 	return nil
 }
 
-func (c *designateMockClient) GetRecordSet(zoneID, recordSetID string, handler func(recordSet *recordsets.RecordSet) error) error {
-	_, rs, err := c.getRecordSet(zoneID, recordSetID)
-	if err != nil {
-		return err
-	}
-	return handler(rs)
-}
-
 func newMockHandler(mockZones ...*zones.Zone) *Handler {
 	c := designateMockClient{
 		tzmap: map[string]*testzone{},