Monitoring Harbor with Azure Monitor and Azure Managed Grafana

Monitoring Harbor with Azure Monitor and Azure Managed Grafana

Collect Harbor 2.15 metrics with an Azure-specific ServiceMonitor, verify ingestion in Azure Monitor managed Prometheus, and build a Harbor dashboard in Azure Managed Grafana.

Harbor already exposes the Prometheus metrics you need to answer practical operational questions: Are all components healthy? Are registry requests slowing down? Is a project close to its quota? Are background jobs backing up?

If Harbor runs on Kubernetes, Azure Monitor managed service for Prometheus can collect those metrics without another Prometheus server. Azure Managed Grafana can then query the Azure Monitor workspace and turn the data into dashboards and alerts.

There is one Azure-specific detail that is easy to miss:

Azure Monitor uses azmonitoring.coreos.com/v1 for its ServiceMonitor and PodMonitor resources. It does not scrape resources created with the Prometheus Operator’s monitoring.coreos.com/v1 API group.

That matters because the official Harbor Helm chart can create a ServiceMonitor, but it uses the OSS API group. In this post, I will enable Harbor’s metrics, leave that monitor disabled, and create an Azure-compatible ServiceMonitor instead.


What We Are Building

The metrics path looks like this:

flowchart TB
    subgraph Harbor["Harbor on Kubernetes"]
        Exporter["exporter Service"]
        Core["core Service"]
        Registry["registry Service"]
        Jobservice["jobservice Service"]
    end

    ServiceMonitor["Azure ServiceMonitor<br/>azmonitoring.coreos.com/v1"]
    Collector["ama-metrics collector"]
    Workspace["Azure Monitor workspace"]
    Grafana["Azure Managed Grafana"]

    Exporter --> ServiceMonitor
    Core --> ServiceMonitor
    Registry --> ServiceMonitor
    Jobservice --> ServiceMonitor
    ServiceMonitor --> Collector --> Workspace --> Grafana

The walkthrough assumes you already have:

  • Harbor 2.15 running on Kubernetes
  • kubectl and, for a Helm deployment, helm access to the cluster
  • Azure Monitor managed service for Prometheus enabled on the cluster
  • An Azure Monitor workspace receiving metrics from the cluster
  • An Azure Managed Grafana workspace
  • Permission to manage Grafana data sources and Azure role assignments

The cluster can be AKS or another supported Kubernetes cluster connected to Azure Monitor. Azure resource provisioning is outside the scope of this post.


The Metrics Harbor Exposes

Harbor exposes Prometheus metrics from four components:

ComponentWhat it tells youExample metrics
ExporterOverall health, projects, repositories, artifacts, quotas, and task queuesharbor_health, harbor_up, harbor_project_repo_total, harbor_project_quota_usage_byte
CoreAPI request volume, in-flight requests, and request durationharbor_core_http_request_total, harbor_core_http_request_duration_seconds
RegistryRegistry HTTP traffic, latency, response sizes, storage actions, and cache activityregistry_http_requests_total, registry_http_request_duration_seconds_bucket, registry_storage_action_seconds_bucket
JobserviceBackground job throughput and processing timeharbor_jobservice_task_total, harbor_jobservice_task_process_time_seconds

The Harbor 2.15 metrics reference lists the complete set, labels, and metric types.

Harbor’s documentation describes component queries such as /metrics?comp=core. The Helm chart takes a Kubernetes-friendly approach: when metrics are enabled, the exporter, core, registry, and jobservice Services each expose a named metrics port. The ServiceMonitor can discover all four Services and scrape /metrics directly.


Enable Harbor Metrics

For the official Helm chart, add these values to the values file used for your Harbor release:

metrics:
  enabled: true
  serviceMonitor:
    enabled: false

metrics.enabled adds the metric endpoints and Service ports. Keep metrics.serviceMonitor.enabled set to false: the chart-generated resource uses monitoring.coreos.com/v1, which Azure Monitor does not watch.

Apply the values through your normal Harbor upgrade process and pin the same chart version you already operate. For example:

helm get values harbor -n harbor > harbor-current-values.yaml

# Add the metrics block to harbor-current-values.yaml, then use your pinned version.
helm upgrade harbor harbor/harbor \
  --namespace harbor \
  --version <YOUR_HARBOR_CHART_VERSION> \
  -f harbor-current-values.yaml

Do not copy the example command without checking your release name, namespace, chart repository, and current values first.

For a non-Helm deployment, enable the equivalent metric settings in your Harbor configuration. The endpoint still needs to be reachable from the Azure Monitor collector inside the cluster.

Inspect the generated Services

Do not guess the labels or port names. Inspect what your release created:

kubectl get services -n harbor --show-labels

kubectl get services -n harbor \
  -l release=harbor,app=harbor \
  -o custom-columns='NAME:.metadata.name,PORT_NAMES:.spec.ports[*].name,PORTS:.spec.ports[*].port'

With the default release name and HTTP between Harbor components, the four metric Services share these labels:

release=harbor
app=harbor

They also expose a port named http-metrics. If Harbor internal TLS is enabled, the Helm chart names it https-metrics instead. Change the ServiceMonitor endpoint and TLS settings to match your deployment.

Test a source endpoint

Choose the exporter Service name from the previous output and port-forward its metrics port:

kubectl port-forward -n harbor service/<HARBOR_EXPORTER_SERVICE> 8001:8001

In another terminal:

curl -fsS http://127.0.0.1:8001/metrics | grep -E '^harbor_(health|up)'

Fix an empty, refused, or TLS-failing endpoint here. A ServiceMonitor cannot make an unreachable endpoint scrapeable.


Create the Azure ServiceMonitor

Enabling managed Prometheus installs Azure’s PodMonitor and ServiceMonitor CRDs. Confirm they are present:

kubectl api-resources --api-group=azmonitoring.coreos.com

Create harbor-azure-monitor.yaml:

apiVersion: azmonitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: harbor-azure-monitor
  namespace: harbor
spec:
  labelLimit: 63
  labelNameLengthLimit: 511
  labelValueLengthLimit: 1023
  selector:
    matchLabels:
      release: harbor
      app: harbor
  endpoints:
    - port: http-metrics
      path: /metrics
      interval: 30s
      scrapeTimeout: 10s
      honorLabels: true
      relabelings:
        - sourceLabels: [__meta_kubernetes_service_name]
          targetLabel: harbor_service

Apply it:

kubectl apply -f harbor-azure-monitor.yaml
kubectl get servicemonitor.azmonitoring.coreos.com -n harbor

This single selector matches the Harbor component Services. Prometheus service discovery discards a selected Service when it does not contain the named http-metrics port, so Harbor’s non-metric Services do not become scrape targets.

Replace release: harbor, app: harbor, the namespace, and the port name with the values you inspected. Because the ServiceMonitor is in the same namespace as the Services, it does not need a namespaceSelector.

Microsoft recommends the three label-limit fields shown above. Without them, a custom monitor can be dropped during Azure’s processing.

If Harbor uses internal TLS

Harbor supports TLS for communication between its internal components. When it is enabled, the Helm chart exposes the named https-metrics port instead of http-metrics. Configure the ServiceMonitor to use that port and trust the CA that signed Harbor’s internal certificates.

See Configure Internal TLS Communication Between Harbor Components for Harbor’s certificate requirements and setup instructions.

Why not a ConfigMap or PodMonitor?

A ServiceMonitor CRD is enough for these Service-backed targets. The ama-metrics-settings-configmap is useful when you need global scrape settings, default-target changes, metric filtering, or secret-access configuration, but it is not required for this path.

A PodMonitor is a valid alternative when an application has no suitable Service. It must still use azmonitoring.coreos.com/v1. For Harbor Helm, the component Services make a ServiceMonitor the cleaner option.


Verify the Collector

First, confirm the monitor’s selector matches the Services you expect:

kubectl get services -n harbor \
  -l release=harbor,app=harbor \
  -o custom-columns='NAME:.metadata.name,METRICS_PORT:.spec.ports[?(@.name=="http-metrics")].port'

kubectl describe servicemonitor.azmonitoring.coreos.com \
  harbor-azure-monitor \
  -n harbor

Then check the Azure Monitor collector:

kubectl get pods -n kube-system | grep ama-metrics

The collector pods should be running. Review the target allocator when discovery fails:

kubectl logs -n kube-system <AMA_METRICS_OPERATOR_TARGETS_POD> \
  -c targetallocator \
  --tail=100

The Prometheus Agent UI is the fastest way to follow a target through discovery. Select an ama-metrics-... collector pod from the pod list, then run:

kubectl port-forward -n kube-system pod/<AMA_METRICS_COLLECTOR_POD> 9090:9090

Open these pages:

Look for a pool named like serviceMonitor/harbor/harbor-azure-monitor. The Harbor endpoints should be UP.

Prometheus Agent configuration showing the Harbor ServiceMonitor scrape job, 30-second interval, metrics path, and configured label limits


Confirm the Metrics in Azure Monitor

Open the Azure Monitor workspace in the Azure portal, select Prometheus explorer, and start with:

harbor_up

You should see one series for each Harbor component reported by the exporter. Continue with:

harbor_health
sum by (method, operation) (
  rate(harbor_core_http_request_total[5m])
)
sum by (status, type) (
  rate(harbor_jobservice_task_total[5m])
)

If harbor_up exists but core, registry, or jobservice metrics do not, return to the collector targets page. That usually means only the exporter Service was discovered or the other named ports are not reachable.


Connect Azure Managed Grafana

Azure Managed Grafana uses its managed identity to query the Azure Monitor workspace.

  1. Open the Azure Monitor workspace and copy its Query endpoint.
  2. Open the Grafana workspace.
  3. Go to Connections → Data sources → Add data source.
  4. Select Azure Monitor Managed Service for Prometheus on Grafana 13 or later. Grafana 11 and 12 can use the Prometheus data source with Azure authentication.
  5. Paste the workspace query endpoint into Prometheus server URL.
  6. Select Azure Auth and Managed Identity.
  7. Select Save & test.

The Grafana managed identity needs Monitoring Data Reader for the Azure Monitor workspace. Azure Managed Grafana commonly receives this role at subscription scope when it is created. For least privilege, verify the existing assignment and scope it to the resource group or workspace that Grafana actually needs.

Use Explore and run harbor_up. Do not start building a dashboard until this query returns the same series you saw in Prometheus explorer.


Build the Harbor Dashboard

You can start with the community Harbor dashboard in the Grafana dashboard gallery or import this Azure Monitor Managed Prometheus Harbor dashboard. Neither is an Azure-specific guaranteed drop-in: select your Azure managed Prometheus data source and review every imported query and variable.

Grafana Harbor Overview dashboard showing Harbor health, component status, project quotas, repositories, and artifacts

Start with a dashboard variable for the labels that exist in your data. cluster, namespace, and the harbor_service label added by the ServiceMonitor are useful candidates. Do not copy variables from another dashboard before confirming the label names in Explore.

These panels form a useful first dashboard.

Component availability

min by (component) (harbor_up)

Use a state timeline or status history panel. A value of 0 means the component is unavailable.

Overall Harbor health

min(harbor_health)

Use a stat panel with value mappings for healthy and unhealthy states.

Project quota utilization

100 *
harbor_project_quota_usage_byte
/
clamp_min(harbor_project_quota_byte, 1)

Use a table or bar gauge grouped by project_name. Set the unit to percent.

Core request rate

sum by (method, operation) (
  rate(harbor_core_http_request_total[5m])
)

Use a time series panel. Filter low-value operations or move them into a table if the legend becomes too busy.

Core p99 request duration

Harbor exposes core duration as a Prometheus summary, so select its exported quantile directly:

max by (operation) (
  harbor_core_http_request_duration_seconds{quantile="0.99"}
)

This Harbor deployment exposes 0.5, 0.9, and 0.99 summary quantiles. Confirm the available quantile label values in your own metrics before choosing a percentile. Do not apply histogram_quantile to summary quantiles.

Registry 5xx percentage

100 *
sum(rate(registry_http_requests_total{code=~"5.."}[5m]))
/
clamp_min(sum(rate(registry_http_requests_total[5m])), 1)

Use a stat and time series panel. A low-traffic registry can produce short spikes, so alert only after a sustained window.

Registry p95 request duration

Registry duration is a histogram, so calculate its quantile from buckets:

histogram_quantile(
  0.95,
  sum by (le, handler) (
    rate(registry_http_request_duration_seconds_bucket[5m])
  )
)

Job queue depth

max by (type) (harbor_task_queue_size)

Pair queue depth with queue latency and job processing status to distinguish a brief burst from a stuck worker pool.

Job results

sum by (status, type) (
  rate(harbor_jobservice_task_total[5m])
)

Inspect the status values your Harbor release exports before creating a failure-only filter.

Suggested alerts

SignalStarting conditionWhy it matters
Component downharbor_up == 0 for 5 minutesA Harbor dependency or service is unavailable
Harbor unhealthyharbor_health == 0 for 5 minutesOverall health checks are failing
Registry errors5xx percentage above your baseline for 10 minutesPushes and pulls might be failing
High registry latencyp95 above the user-facing SLO for 10 minutesRegistry operations are degraded
Queue backlogQueue size and latency both grow for 15 minutesJobservice might not be keeping up
Project quotaQuota utilization above 85 percentA project is approaching its storage limit

Treat those durations and thresholds as starting points. Tune them against normal traffic, maintenance windows, and your own SLOs.


Control Cardinality and Cost

Harbor metrics include labels such as project_name, operation, handler, status, and type. They are useful, but every unique label combination creates another time series in the Azure Monitor workspace.

Start by collecting the full Harbor metric set. Measure ingestion and confirm the dashboard and alert queries. Only then add metricRelabelings to keep a smaller set.

For example:

metricRelabelings:
  - sourceLabels: [__name__]
    action: keep
    regex: '(harbor_health|harbor_up|harbor_project_quota_.*|harbor_core_http_.*|harbor_task_.*|harbor_jobservice_.*|registry_http_.*|registry_storage_.*)'

Keep the regular expression on one logical YAML value, and retest every dashboard panel after changing it. A restrictive allowlist can silently remove a metric that an alert needs.


Secure the Metrics Path

Harbor metric endpoints should remain cluster-internal unless there is a deliberate reason to expose them. Use ClusterIP Services, Kubernetes network policy, and private cluster networking to limit reachability.

The metrics include project names, Harbor configuration labels, traffic patterns, and health state. They do not belong on a public ingress.

When TLS or basic authentication protects the endpoint:

  • Keep credentials in Kubernetes Secrets.
  • Give the collector only the secret access it requires.
  • Trust the issuing CA instead of disabling certificate verification.
  • Follow Azure Monitor’s namespace-scoped secret access guidance for your Kubernetes version.

Troubleshooting

SymptomLikely causeCheck
ServiceMonitor exists but no scrape job appearsOSS API group was usedConfirm apiVersion: azmonitoring.coreos.com/v1
Scrape job has no targetsService labels do not matchCompare spec.selector with kubectl get svc --show-labels
Selected Service is missingMonitor and Service are in different namespacesCo-locate them or add the correct namespaceSelector
Target is discovered but droppedNamed metrics port does not matchInspect .spec.ports[*].name and use http-metrics or https-metrics
Custom monitor is rejected or ignoredAzure label limits are missingAdd the three Microsoft-recommended limit fields
Target reports connection refusedHarbor metrics are not enabled or the Service port is wrongPort-forward the Service and test /metrics
Target reports an x509 errorInternal TLS CA is not trustedConfigure scheme: https and tlsConfig.ca
Metrics exist in Azure but not GrafanaWrong data source, query endpoint, or RBACTest harbor_up in Explore and verify Monitoring Data Reader
Ingestion becomes expensiveHigh-cardinality labels or unnecessary metricsReview workspace ingestion and add a tested metric allowlist

For collector-level failures, use Microsoft’s Prometheus collection troubleshooting guide. It covers collector logs, service discovery, targets, authentication, and ingestion limits.


Cleanup

Remove only the monitor created for this walkthrough:

kubectl delete servicemonitor.azmonitoring.coreos.com \
  harbor-azure-monitor \
  -n harbor

Delete the imported Grafana dashboard if it was only for testing. Do not disable Harbor metrics, remove the Azure Monitor add-on, or delete shared Azure resources unless that is part of your own change plan.


Closing Thoughts

The difficult part is not exposing Harbor’s Prometheus metrics. It is making sure the right collector discovers them.

With Azure Monitor managed Prometheus, that boundary is explicit: use azmonitoring.coreos.com/v1, match the real Harbor Service labels and named ports, and verify each hop before moving on. Once harbor_up reaches the Azure Monitor workspace, Azure Managed Grafana gives you the familiar PromQL and dashboard workflow without operating another Prometheus stack.


Further Reading

Found this helpful?
Back to all posts