argoproj/argo-workflows · error
found unwanted type %s for custom metric %s of type %s
Error message
found unwanted type %s for custom metric %s of type %s
What it means
matchExistingMetric's default branch: an instrument with the metric's name exists in the registry but its underlying OpenTelemetry type is neither Float64ObservableGauge, Float64ObservableCounter, nor Float64Histogram, so the existing-type check cannot be performed and the upsert is rejected. It reports the actual Go type via reflect for diagnosis. In practice this means a base/built-in instrument (or an unexpected wrapper) already owns that name.
Source
Thrown at workflow/metrics/metrics_custom.go:161
if inst.GetDescription() != metricSpec.Help {
return nil, fmt.Errorf("help for metric %s is already set to %s, it cannot be changed", metricSpec.Name, inst.GetDescription())
}
wantedType := metricSpec.GetMetricType()
switch inst.GetOtel().(type) {
case *metric.Float64ObservableGauge:
if wantedType != wfv1.MetricTypeGauge && !metricSpec.IsRealtime() {
return nil, fmt.Errorf("found existing gauge for custom metric %s of type %s", metricSpec.Name, wantedType)
}
case *metric.Float64ObservableCounter:
if wantedType != wfv1.MetricTypeCounter {
return nil, fmt.Errorf("found existing counter for custom metric %s of type %s", metricSpec.Name, wantedType)
}
case *metric.Float64Histogram:
if wantedType != wfv1.MetricTypeHistogram {
return nil, fmt.Errorf("found existing histogram for custom metric %s of type %s", metricSpec.Name, wantedType)
}
default:
return nil, fmt.Errorf("found unwanted type %s for custom metric %s of type %s", reflect.TypeOf(inst.GetOtel()), metricSpec.Name, wantedType)
}
return inst, nil
}
return nil, nil
}
func (m *Metrics) ensureBaseMetric(metricSpec *wfv1.Prometheus, ownerKey string) (*telemetry.Instrument, error) {
// Fast path: check if metric already exists and is fully initialized (double-checked locking).
metric, err := m.matchExistingMetric(metricSpec)
if err != nil {
return nil, err
}
if metric != nil {
if customUserData(metric, false) != nil {
m.attachCustomMetricToWorkflow(metricSpec, ownerKey)
return metric, nil
}
}View on GitHub (pinned to 35bff19146)
Solutions
- Rename the custom metric to a unique name that does not collide with existing instruments
- Check the logged Go type in the error to identify what already registered the name and align usage accordingly
- Avoid using names of Argo built-in/base metrics for custom workflow metrics
Defensive patterns
Strategy: validation
Validate before calling
// reject custom metric names that shadow Argo built-in metric names
var builtinMetrics = map[string]bool{"operation_count": true, "k8s_request_duration": true}
func isReservedName(name string) bool { return builtinMetrics[name] } Prevention
- Never reuse built-in Argo metric names for custom metrics
- Prefix custom metrics with a workflow/team-specific prefix
- Read the reflect type in the error to identify what owns the name
When it happens
Trigger: UpsertCustomMetric is called with a metricSpec.Name that collides with a registered instrument of an unexpected OTel kind (e.g. a non-observable/base instrument created elsewhere in the controller) — the switch in matchExistingMetric falls through to default and returns this error immediately.
Common situations: A custom metric name shadowing a built-in Argo metric name (e.g. same name as a system instrument); upgrades where instrument kinds changed; copying an existing name from another metrics scope.
Related errors
- found existing histogram for custom metric %s of type %s
- help for metric %s is already set to %s, it cannot be change
- found existing gauge for custom metric %s of type %s
- found existing counter for custom metric %s of type %s
- failed to create new metric %s
AI-assisted analysis of argoproj/argo-workflows@35bff19146 (2026-09-03).
Data as JSON: /api/errors/15b928b346f86a29.
Report an issue: GitHub.