diff --git a/go.mod b/go.mod index 5de812fa87..3bc74feab5 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/onsi/ginkgo v1.16.1 github.com/onsi/gomega v1.11.0 github.com/pkg/errors v0.9.1 + github.com/prometheus/client_golang v1.9.0 github.com/spf13/pflag v1.0.5 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 gopkg.in/ini.v1 v1.62.0 diff --git a/main.go b/main.go index 4151600274..2eb7e5f144 100644 --- a/main.go +++ b/main.go @@ -40,6 +40,7 @@ import ( infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha4" "sigs.k8s.io/cluster-api-provider-openstack/controllers" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics" "sigs.k8s.io/cluster-api-provider-openstack/pkg/record" "sigs.k8s.io/cluster-api-provider-openstack/version" ) @@ -72,6 +73,8 @@ func init() { _ = clusterv1.AddToScheme(scheme) _ = infrav1.AddToScheme(scheme) // +kubebuilder:scaffold:scheme + + metrics.RegisterAPIPrometheusMetrics() } // InitFlags initializes the flags. diff --git a/pkg/cloud/services/compute/instance.go b/pkg/cloud/services/compute/instance.go index d550356baf..4e96ea1b57 100644 --- a/pkg/cloud/services/compute/instance.go +++ b/pkg/cloud/services/compute/instance.go @@ -45,6 +45,7 @@ import ( "sigs.k8s.io/cluster-api/util" infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha4" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics" "sigs.k8s.io/cluster-api-provider-openstack/pkg/record" capoerrors "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/errors" ) @@ -217,11 +218,14 @@ func (s *Service) createInstance(eventObject runtime.Object, clusterName string, serverCreateOpts = applyServerGroupID(serverCreateOpts, i.ServerGroupID) + mc := metrics.NewMetricPrometheusContext("server", "create") + server, err := servers.Create(s.computeClient, keypairs.CreateOptsExt{ CreateOptsBuilder: serverCreateOpts, KeyName: i.SSHKeyName, }).Extract() - if err != nil { + + if mc.ObserveRequest(err) != nil { if err = s.deletePorts(eventObject, portList); err != nil { return nil, err } @@ -400,8 +404,10 @@ func (s *Service) getOrCreatePort(eventObject runtime.Object, clusterName string if net.Subnet.ID != "" { portCreateOpts.FixedIPs = []ports.IP{{SubnetID: net.Subnet.ID}} } + mc := metrics.NewMetricPrometheusContext("port", "create") + port, err := ports.Create(s.networkClient, portCreateOpts).Extract() - if err != nil { + if mc.ObserveRequest(err) != nil { record.Warnf(eventObject, "FailedCreatePort", "Failed to create port %s: %v", portName, err) return nil, err } @@ -431,8 +437,10 @@ func (s *Service) getOrCreateTrunk(eventObject runtime.Object, trunkName, portID Name: trunkName, PortID: portID, } + mc := metrics.NewMetricPrometheusContext("trunk", "create") + trunk, err := trunks.Create(s.networkClient, trunkCreateOpts).Extract() - if err != nil { + if mc.ObserveRequest(err) != nil { record.Warnf(eventObject, "FailedCreateTrunk", "Failed to create trunk %s: %v", trunkName, err) return nil, err } @@ -442,10 +450,11 @@ func (s *Service) getOrCreateTrunk(eventObject runtime.Object, trunkName, portID } func (s *Service) replaceAllAttributesTags(eventObject runtime.Object, trunkID string, tags []string) error { + mc := metrics.NewMetricPrometheusContext("trunk", "update") _, err := attributestags.ReplaceAll(s.networkClient, "trunks", trunkID, attributestags.ReplaceAllOpts{ Tags: tags, }).Extract() - if err != nil { + if mc.ObserveRequest(err) != nil { record.Warnf(eventObject, "FailedReplaceAllAttributesTags", "Failed to replace all attributestags, trunk %s: %v", trunkID, err) return err } @@ -488,8 +497,9 @@ func (s *Service) AssociateFloatingIP(instanceID, floatingIP string) error { opts := floatingips.AssociateOpts{ FloatingIP: floatingIP, } + mc := metrics.NewMetricPrometheusContext("floating_ip", "update") err := floatingips.AssociateInstance(s.computeClient, instanceID, opts).ExtractErr() - if err != nil { + if mc.ObserveRequest(err) != nil { return err } return nil @@ -551,8 +561,9 @@ func (s *Service) deletePort(eventObject runtime.Object, portID string) error { } err = util.PollImmediate(RetryIntervalPortDelete, TimeoutPortDelete, func() (bool, error) { + mc := metrics.NewMetricPrometheusContext("port", "delete") err := ports.Delete(s.networkClient, port.ID).ExtractErr() - if err != nil { + if mc.ObserveRequest(err) != nil { if capoerrors.IsRetryable(err) { return false, nil } @@ -595,8 +606,9 @@ func (s *Service) deleteAttachInterface(eventObject runtime.Object, instanceID, return nil } + mc := metrics.NewMetricPrometheusContext("router_interface", "delete") err = attachinterfaces.Delete(s.computeClient, instanceID, portID).ExtractErr() - if err != nil { + if mc.ObserveRequest(err) != nil { if capoerrors.IsNotFound(err) { return nil } @@ -673,8 +685,9 @@ func (s *Service) deleteInstance(eventObject runtime.Object, instanceID string) if instance == nil || instance.ID == "" { return nil } - - if err = servers.Delete(s.computeClient, instance.ID).ExtractErr(); err != nil { + mc := metrics.NewMetricPrometheusContext("server", "delete") + err = servers.Delete(s.computeClient, instance.ID).ExtractErr() + if mc.ObserveRequest(err) != nil { record.Warnf(eventObject, "FailedDeleteServer", "Failed to deleted server %s with id %s: %v", instance.Name, instance.ID, err) return err } diff --git a/pkg/cloud/services/loadbalancer/loadbalancer.go b/pkg/cloud/services/loadbalancer/loadbalancer.go index 3156c8d114..fd798ba98f 100644 --- a/pkg/cloud/services/loadbalancer/loadbalancer.go +++ b/pkg/cloud/services/loadbalancer/loadbalancer.go @@ -30,6 +30,7 @@ import ( "sigs.k8s.io/cluster-api/util" infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha4" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics" "sigs.k8s.io/cluster-api-provider-openstack/pkg/record" ) @@ -54,8 +55,10 @@ func (s *Service) ReconcileLoadBalancer(openStackCluster *infrav1.OpenStackClust VipSubnetID: openStackCluster.Status.Network.Subnet.ID, } + mc := metrics.NewMetricPrometheusContext("loadbalancer", "create") lb, err = loadbalancers.Create(s.loadbalancerClient, lbCreateOpts).Extract() - if err != nil { + + if mc.ObserveRequest(err) != nil { record.Warnf(openStackCluster, "FailedCreateLoadBalancer", "Failed to create load balancer %s: %v", loadBalancerName, err) return err } @@ -95,8 +98,9 @@ func (s *Service) ReconcileLoadBalancer(openStackCluster *infrav1.OpenStackClust ProtocolPort: port, LoadbalancerID: lb.ID, } + mc := metrics.NewMetricPrometheusContext("loadbalancer_listener", "create") listener, err = listeners.Create(s.loadbalancerClient, listenerCreateOpts).Extract() - if err != nil { + if mc.ObserveRequest(err) != nil { return fmt.Errorf("error creating listener: %s", err) } } @@ -121,8 +125,9 @@ func (s *Service) ReconcileLoadBalancer(openStackCluster *infrav1.OpenStackClust LBMethod: pools.LBMethodRoundRobin, ListenerID: listener.ID, } + mc := metrics.NewMetricPrometheusContext("loadbalancer_pool", "create") pool, err = pools.Create(s.loadbalancerClient, poolCreateOpts).Extract() - if err != nil { + if mc.ObserveRequest(err) != nil { return fmt.Errorf("error creating pool: %s", err) } } @@ -145,8 +150,9 @@ func (s *Service) ReconcileLoadBalancer(openStackCluster *infrav1.OpenStackClust Timeout: 5, MaxRetries: 3, } + mc := metrics.NewMetricPrometheusContext("loadbalancer_healthmonitor", "create") _, err = monitors.Create(s.loadbalancerClient, monitorCreateOpts).Extract() - if err != nil { + if mc.ObserveRequest(err) != nil { return fmt.Errorf("error creating monitor: %s", err) } } @@ -216,8 +222,9 @@ func (s *Service) ReconcileLoadBalancerMember(openStackCluster *infrav1.OpenStac if err != nil { return err } + mc := metrics.NewMetricPrometheusContext("loadbalancer_member", "delete") err = pools.DeleteMember(s.loadbalancerClient, pool.ID, lbMember.ID).ExtractErr() - if err != nil { + if mc.ObserveRequest(err) != nil { return fmt.Errorf("error deleting lbmember: %s", err) } err = s.waitForLoadBalancerActive(lbID) @@ -238,7 +245,9 @@ func (s *Service) ReconcileLoadBalancerMember(openStackCluster *infrav1.OpenStac if err := s.waitForLoadBalancerActive(lbID); err != nil { return err } - if _, err := pools.CreateMember(s.loadbalancerClient, pool.ID, lbMemberOpts).Extract(); err != nil { + mc := metrics.NewMetricPrometheusContext("loadbalancer_member", "create") + _, err = pools.CreateMember(s.loadbalancerClient, pool.ID, lbMemberOpts).Extract() + if mc.ObserveRequest(err) != nil { return fmt.Errorf("error create lbmember: %s", err) } if err := s.waitForLoadBalancerActive(lbID); err != nil { @@ -279,8 +288,9 @@ func (s *Service) DeleteLoadBalancer(openStackCluster *infrav1.OpenStackCluster, Cascade: true, } s.logger.Info("Deleting load balancer", "name", loadBalancerName, "cascade", deleteOpts.Cascade) + mc := metrics.NewMetricPrometheusContext("loadbalancer", "delete") err = loadbalancers.Delete(s.loadbalancerClient, lb.ID, deleteOpts).ExtractErr() - if err != nil { + if mc.ObserveRequest(err) != nil { record.Warnf(openStackCluster, "FailedDeleteLoadBalancer", "Failed to delete load balancer %s with id %s: %v", lb.Name, lb.ID, err) return err } @@ -333,8 +343,9 @@ func (s *Service) DeleteLoadBalancerMember(openStackCluster *infrav1.OpenStackCl if err != nil { return err } + mc := metrics.NewMetricPrometheusContext("loadbalancer_member", "delete") err = pools.DeleteMember(s.loadbalancerClient, pool.ID, lbMember.ID).ExtractErr() - if err != nil { + if mc.ObserveRequest(err) != nil { return fmt.Errorf("error deleting load balancer member: %s", err) } err = s.waitForLoadBalancerActive(lbID) diff --git a/pkg/cloud/services/networking/floatingip.go b/pkg/cloud/services/networking/floatingip.go index 2acfe87bdc..606f0521c6 100644 --- a/pkg/cloud/services/networking/floatingip.go +++ b/pkg/cloud/services/networking/floatingip.go @@ -23,6 +23,7 @@ import ( "k8s.io/apimachinery/pkg/util/wait" infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha4" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics" "sigs.k8s.io/cluster-api-provider-openstack/pkg/record" ) @@ -45,8 +46,9 @@ func (s *Service) GetOrCreateFloatingIP(openStackCluster *infrav1.OpenStackClust fpCreateOpts.FloatingNetworkID = openStackCluster.Status.ExternalNetwork.ID + mc := metrics.NewMetricPrometheusContext("floating_ip", "create") fp, err = floatingips.Create(s.client, fpCreateOpts).Extract() - if err != nil { + if mc.ObserveRequest(err) != nil { record.Warnf(openStackCluster, "FailedCreateFloatingIP", "Failed to create floating IP %s: %v", ip, err) return nil, err } @@ -95,7 +97,9 @@ func (s *Service) DeleteFloatingIP(openStackCluster *infrav1.OpenStackCluster, i return nil } - if err = floatingips.Delete(s.client, fip.ID).ExtractErr(); err != nil { + mc := metrics.NewMetricPrometheusContext("floating_ip", "delete") + err = floatingips.Delete(s.client, fip.ID).ExtractErr() + if mc.ObserveRequest(err) != nil { record.Warnf(openStackCluster, "FailedDeleteFloatingIP", "Failed to delete floating IP %s: %v", ip, err) return err } @@ -116,8 +120,9 @@ func (s *Service) AssociateFloatingIP(openStackCluster *infrav1.OpenStackCluster fpUpdateOpts := &floatingips.UpdateOpts{ PortID: &portID, } + mc := metrics.NewMetricPrometheusContext("floating_ip", "update") _, err := floatingips.Update(s.client, fp.ID, fpUpdateOpts).Extract() - if err != nil { + if mc.ObserveRequest(err) != nil { record.Warnf(openStackCluster, "FailedAssociateFloatingIP", "Failed to associate floating IP %s with port %s: %v", fp.FloatingIP, portID, err) return err } @@ -149,8 +154,9 @@ func (s *Service) DisassociateFloatingIP(openStackCluster *infrav1.OpenStackClus fpUpdateOpts := &floatingips.UpdateOpts{ PortID: nil, } + mc := metrics.NewMetricPrometheusContext("floating_ip", "update") _, err = floatingips.Update(s.client, fip.ID, fpUpdateOpts).Extract() - if err != nil { + if mc.ObserveRequest(err) != nil { record.Warnf(openStackCluster, "FailedDisassociateFloatingIP", "Failed to disassociate floating IP %s: %v", fip.FloatingIP, err) return err } diff --git a/pkg/cloud/services/networking/network.go b/pkg/cloud/services/networking/network.go index add1ac5845..9eafd3945d 100644 --- a/pkg/cloud/services/networking/network.go +++ b/pkg/cloud/services/networking/network.go @@ -27,6 +27,7 @@ import ( "github.com/gophercloud/gophercloud/pagination" infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha4" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics" "sigs.k8s.io/cluster-api-provider-openstack/pkg/record" ) @@ -122,8 +123,10 @@ func (s *Service) ReconcileNetwork(openStackCluster *infrav1.OpenStackCluster, c Name: networkName, } } + + mc := metrics.NewMetricPrometheusContext("network", "create") network, err := networks.Create(s.client, opts).Extract() - if err != nil { + if mc.ObserveRequest(err) != nil { record.Warnf(openStackCluster, "FailedCreateNetwork", "Failed to create network %s: %v", networkName, err) return err } @@ -156,7 +159,9 @@ func (s *Service) DeleteNetwork(openStackCluster *infrav1.OpenStackCluster, clus return nil } - if err = networks.Delete(s.client, network.ID).ExtractErr(); err != nil { + mc := metrics.NewMetricPrometheusContext("network", "delete") + err = networks.Delete(s.client, network.ID).ExtractErr() + if mc.ObserveRequest(err) != nil { record.Warnf(openStackCluster, "FailedDeleteNetwork", "Failed to delete network %s with id %s: %v", network.Name, network.ID, err) return err } @@ -220,18 +225,22 @@ func (s *Service) createSubnet(openStackCluster *infrav1.OpenStackCluster, name CIDR: openStackCluster.Spec.NodeCIDR, DNSNameservers: openStackCluster.Spec.DNSNameservers, } + mc := metrics.NewMetricPrometheusContext("subnet", "create") + subnet, err := subnets.Create(s.client, opts).Extract() - if err != nil { + + if mc.ObserveRequest(err) != nil { record.Warnf(openStackCluster, "FailedCreateSubnet", "Failed to create subnet %s: %v", name, err) return nil, err } record.Eventf(openStackCluster, "SuccessfulCreateSubnet", "Created subnet %s with id %s", name, subnet.ID) if len(openStackCluster.Spec.Tags) > 0 { + mc := metrics.NewMetricPrometheusContext("subnet", "update") _, err = attributestags.ReplaceAll(s.client, "subnets", subnet.ID, attributestags.ReplaceAllOpts{ Tags: openStackCluster.Spec.Tags, }).Extract() - if err != nil { + if mc.ObserveRequest(err) != nil { return nil, err } } diff --git a/pkg/cloud/services/networking/router.go b/pkg/cloud/services/networking/router.go index d52692c814..80622246e2 100644 --- a/pkg/cloud/services/networking/router.go +++ b/pkg/cloud/services/networking/router.go @@ -25,6 +25,7 @@ import ( "github.com/gophercloud/gophercloud/openstack/networking/v2/subnets" infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha4" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics" "sigs.k8s.io/cluster-api-provider-openstack/pkg/record" capoerrors "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/errors" ) @@ -106,10 +107,11 @@ INTERFACE_LOOP: // ... and create a router interface for our subnet. if createInterface { s.logger.V(4).Info("Creating RouterInterface", "routerID", router.ID, "subnetID", openStackCluster.Status.Network.Subnet.ID) + mc := metrics.NewMetricPrometheusContext("router_interface", "create") routerInterface, err := routers.AddInterface(s.client, router.ID, routers.AddInterfaceOpts{ SubnetID: openStackCluster.Status.Network.Subnet.ID, }).Extract() - if err != nil { + if mc.ObserveRequest(err) != nil { return fmt.Errorf("unable to create router interface: %v", err) } s.logger.V(4).Info("Created RouterInterface", "id", routerInterface.ID) @@ -130,8 +132,12 @@ func (s *Service) createRouter(openStackCluster *infrav1.OpenStackCluster, name NetworkID: openStackCluster.Status.ExternalNetwork.ID, } } + + mc := metrics.NewMetricPrometheusContext("router", "create") + router, err := routers.Create(s.client, opts).Extract() - if err != nil { + + if mc.ObserveRequest(err) != nil { record.Warnf(openStackCluster, "FailedCreateRouter", "Failed to create router %s: %v", name, err) return nil, err } @@ -175,7 +181,9 @@ func (s *Service) setRouterExternalIPs(openStackCluster *infrav1.OpenStackCluste }) } - if _, err := routers.Update(s.client, router.ID, updateOpts).Extract(); err != nil { + mc := metrics.NewMetricPrometheusContext("router", "update") + _, err := routers.Update(s.client, router.ID, updateOpts).Extract() + if mc.ObserveRequest(err) != nil { record.Warnf(openStackCluster, "FailedUpdateRouter", "Failed to update router %s with id %s: %v", router.Name, router.ID, err) return err } @@ -195,10 +203,11 @@ func (s *Service) DeleteRouter(openStackCluster *infrav1.OpenStackCluster, clust } if subnet.ID != "" { + mc := metrics.NewMetricPrometheusContext("router_interface", "delete") _, err = routers.RemoveInterface(s.client, router.ID, routers.RemoveInterfaceOpts{ SubnetID: subnet.ID, }).Extract() - if err != nil { + if mc.ObserveRequest(err) != nil { if !capoerrors.IsNotFound(err) { return fmt.Errorf("unable to remove router interface: %v", err) } @@ -208,7 +217,9 @@ func (s *Service) DeleteRouter(openStackCluster *infrav1.OpenStackCluster, clust } } - if err = routers.Delete(s.client, router.ID).ExtractErr(); err != nil { + mc := metrics.NewMetricPrometheusContext("router", "delete") + err = routers.Delete(s.client, router.ID).ExtractErr() + if mc.ObserveRequest(err) != nil { record.Warnf(openStackCluster, "FailedDeleteRouter", "Failed to delete router %s with id %s: %v", router.Name, router.ID, err) return err } diff --git a/pkg/cloud/services/networking/securitygroups.go b/pkg/cloud/services/networking/securitygroups.go index 09c77fa52f..d1986eb085 100644 --- a/pkg/cloud/services/networking/securitygroups.go +++ b/pkg/cloud/services/networking/securitygroups.go @@ -23,6 +23,7 @@ import ( "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules" infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha4" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics" "sigs.k8s.io/cluster-api-provider-openstack/pkg/record" ) @@ -361,7 +362,9 @@ func (s *Service) deleteSecurityGroup(openStackCluster *infrav1.OpenStackCluster // nothing to do return nil } - if err = groups.Delete(s.client, group.ID).ExtractErr(); err != nil { + mc := metrics.NewMetricPrometheusContext("security_group", "delete") + err = groups.Delete(s.client, group.ID).ExtractErr() + if mc.ObserveRequest(err) != nil { record.Warnf(openStackCluster, "FailedDeleteSecurityGroup", "Failed to delete security group %s with id %s: %v", group.Name, group.ID, err) return err } @@ -418,8 +421,9 @@ func (s *Service) reconcileGroupRules(desired, observed infrav1.SecurityGroup) ( s.logger.V(4).Info("Deleting rules not needed anymore for group", "name", observed.Name, "amount", len(rulesToDelete)) for _, rule := range rulesToDelete { s.logger.V(6).Info("Deleting rule", "ruleID", rule.ID, "groupName", observed.Name) + mc := metrics.NewMetricPrometheusContext("security_group_rule", "delete") err := rules.Delete(s.client, rule.ID).ExtractErr() - if err != nil { + if mc.ObserveRequest(err) != nil { return infrav1.SecurityGroup{}, err } } @@ -455,8 +459,10 @@ func (s *Service) createSecurityGroupIfNotExists(openStackCluster *infrav1.OpenS Description: "Cluster API managed group", } s.logger.V(6).Info("Creating group", "name", groupName) + + mc := metrics.NewMetricPrometheusContext("security_group", "create") group, err := groups.Create(s.client, createOpts).Extract() - if err != nil { + if mc.ObserveRequest(err) != nil { record.Warnf(openStackCluster, "FailedCreateSecurityGroup", "Failed to create security group %s: %v", groupName, err) return err } @@ -514,8 +520,9 @@ func (s *Service) createRule(r infrav1.SecurityGroupRule) (infrav1.SecurityGroup SecGroupID: r.SecurityGroupID, } s.logger.V(6).Info("Creating rule", "Description", r.Description, "Direction", dir, "PortRangeMin", r.PortRangeMin, "PortRangeMax", r.PortRangeMax, "Proto", proto, "etherType", etherType, "RemoteGroupID", r.RemoteGroupID, "RemoteIPPrefix", r.RemoteIPPrefix, "SecurityGroupID", r.SecurityGroupID) + mc := metrics.NewMetricPrometheusContext("security_group_rule", "create") rule, err := rules.Create(s.client, createOpts).Extract() - if err != nil { + if mc.ObserveRequest(err) != nil { return infrav1.SecurityGroupRule{}, err } return convertOSSecGroupRuleToConfigSecGroupRule(*rule), nil diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go new file mode 100644 index 0000000000..7635f970a6 --- /dev/null +++ b/pkg/metrics/metrics.go @@ -0,0 +1,98 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed 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 metrics + +import ( + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + "sigs.k8s.io/controller-runtime/pkg/metrics" +) + +type OpenstackPrometheusMetrics struct { + Duration *prometheus.HistogramVec + Total *prometheus.CounterVec + Errors *prometheus.CounterVec +} + +// MetricPrometheusContext indicates the context for OpenStack metrics. +type MetricPrometheusContext struct { + Start time.Time + Attributes []string + Metrics *OpenstackPrometheusMetrics +} + +// NewMetricPrometheusContext creates a new MetricContext. +func NewMetricPrometheusContext(resource string, request string) *MetricPrometheusContext { + return &MetricPrometheusContext{ + Start: time.Now(), + Attributes: []string{resource + "_" + request}, + } +} + +// ObserveRequest records the request latency and counts the errors. +func (mc *MetricPrometheusContext) ObserveRequest(err error) error { + return mc.Observe(APIRequestPrometheusMetrics, err) +} + +// Observe records the request latency and counts the errors. +func (mc *MetricPrometheusContext) Observe(om *OpenstackPrometheusMetrics, err error) error { + if om == nil { + // mc.RequestMetrics not set, ignore this request + return err + } + + om.Duration.WithLabelValues(mc.Attributes...).Observe( + time.Since(mc.Start).Seconds()) + om.Total.WithLabelValues(mc.Attributes...).Inc() + if err != nil { + om.Errors.WithLabelValues(mc.Attributes...).Inc() + } + return err +} + +var APIRequestPrometheusMetrics = &OpenstackPrometheusMetrics{ + Duration: prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: "capo", + Name: "openstack_api_request_duration_seconds", + Help: "Latency of an OpenStack API call", + }, []string{"request"}), + Total: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "capo", + Name: "openstack_api_requests_total", + Help: "Total number of OpenStack API calls", + }, []string{"request"}), + Errors: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "capo", + Name: "openstack_api_request_errors_total", + Help: "Total number of errors for an OpenStack API call", + }, []string{"request"}), +} + +var registerAPIPrometheusMetrics sync.Once + +func RegisterAPIPrometheusMetrics() { + registerAPIPrometheusMetrics.Do(func() { + metrics.Registry.MustRegister(APIRequestPrometheusMetrics.Duration) + metrics.Registry.MustRegister(APIRequestPrometheusMetrics.Total) + metrics.Registry.MustRegister(APIRequestPrometheusMetrics.Errors) + }) +}