Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions prometheus_client/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ def __init__(self: T,
registry: Optional[CollectorRegistry] = REGISTRY,
_labelvalues: Optional[Sequence[str]] = None,
) -> None:

self._original_name = name
self._namespace = namespace
self._subsystem = subsystem
self._name = _build_full_name(self._type, name, namespace, subsystem, unit)
self._labelnames = _validate_labelnames(self, labelnames)
self._labelvalues = tuple(_labelvalues or ())
Expand Down Expand Up @@ -176,13 +180,25 @@ def labels(self: T, *labelvalues: Any, **labelkwargs: Any) -> T:
labelvalues = tuple(str(l) for l in labelvalues)
with self._lock:
if labelvalues not in self._metrics:

original_name = getattr(self, '_original_name', self._name)
namespace = getattr(self, '_namespace', '')
subsystem = getattr(self, '_subsystem', '')
unit = getattr(self, '_unit', '')

child_kwargs = dict(self._kwargs) if self._kwargs else {}
for k in ('namespace', 'subsystem', 'unit'):
child_kwargs.pop(k, None)

self._metrics[labelvalues] = self.__class__(
self._name,
original_name,
documentation=self._documentation,
labelnames=self._labelnames,
unit=self._unit,
namespace=namespace,
subsystem=subsystem,
unit=unit,
_labelvalues=labelvalues,
**self._kwargs
**child_kwargs
)
return self._metrics[labelvalues]

Expand Down
110 changes: 110 additions & 0 deletions tests/test_multiprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,116 @@ def test_remove_clear_warning(self):
assert "Removal of labels has not been implemented" in str(w[0].message)
assert issubclass(w[-1].category, UserWarning)
assert "Clearing labels has not been implemented" in str(w[-1].message)

def test_child_name_is_built_once_with_namespace_subsystem_unit(self):
"""
Repro for #1035:
In multiprocess mode, child metrics must NOT rebuild the full name
(namespace/subsystem/unit) a second time. The exported family name should
be built once, and Counter samples should use "<family>_total".
"""
from prometheus_client import Counter

class CustomCounter(Counter):
def __init__(
self,
name,
documentation,
labelnames=(),
namespace="mydefaultnamespace",
subsystem="mydefaultsubsystem",
unit="",
registry=None,
_labelvalues=None
):
# Intentionally provide non-empty defaults to trigger the bug path.
super().__init__(
name=name,
documentation=documentation,
labelnames=labelnames,
namespace=namespace,
subsystem=subsystem,
unit=unit,
registry=registry,
_labelvalues=_labelvalues)

# Create a Counter with explicit namespace/subsystem/unit
c = CustomCounter(
name='m',
documentation='help',
labelnames=('status', 'method'),
namespace='ns',
subsystem='ss',
unit='seconds', # avoid '_total_total' confusion
registry=None, # not registered in local registry in multiprocess mode
)

# Create two labeled children
c.labels(status='200', method='GET').inc()
c.labels(status='404', method='POST').inc()

# Collect from the multiprocess collector initialized in setUp()
metrics = {m.name: m for m in self.collector.collect()}

# Family name should be built once (no '_total' in family name)
expected_family = 'ns_ss_m_seconds'
self.assertIn(expected_family, metrics, f"missing family {expected_family}")

# Counter samples must use '<family>_total'
mf = metrics[expected_family]
sample_names = {s.name for s in mf.samples}
self.assertTrue(
all(name == expected_family + '_total' for name in sample_names),
f"unexpected sample names: {sample_names}"
)

# Ensure no double-built prefix sneaks in (the original bug)
bad_prefix = 'mydefaultnamespace_mydefaultsubsystem_'
all_names = {mf.name, *sample_names}
self.assertTrue(
all(not n.startswith(bad_prefix) for n in all_names),
f"found double-built name(s): {[n for n in all_names if n.startswith(bad_prefix)]}"
)

def test_child_preserves_parent_context_for_subclasses(self):
"""
Ensure child metrics preserve parent's namespace/subsystem/unit information
so that subclasses can correctly use these parameters in their logic.
"""
class ContextAwareCounter(Counter):
def __init__(self,
name,
documentation,
labelnames=(),
namespace="",
subsystem="",
unit="",
**kwargs):
self.context = {
'namespace': namespace,
'subsystem': subsystem,
'unit': unit
}
super().__init__(name, documentation,
labelnames=labelnames,
namespace=namespace,
subsystem=subsystem,
unit=unit,
**kwargs)

parent = ContextAwareCounter('m', 'help',
labelnames=['status'],
namespace='prod',
subsystem='api',
unit='seconds',
registry=None)

child = parent.labels(status='200')

# Verify that child retains parent's context
self.assertEqual(child.context['namespace'], 'prod')
self.assertEqual(child.context['subsystem'], 'api')
self.assertEqual(child.context['unit'], 'seconds')


class TestMmapedDict(unittest.TestCase):
Expand Down