Skip to content
Open
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
199 changes: 199 additions & 0 deletions qa/1744
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#!/bin/sh
# PCP QA Test No. 1744
# Test pmproxy / pmseries label storing mechanism
#
# Copyright (c) 2026 Red Hat. All Rights Reserved.
#
seq=`basename $0`
echo "QA output created by $seq"

# get standard environment, filters and checks
. ./common.openmetrics
. ./common.python
. ./common.keys

_pmdaopenmetrics_check || _notrun "openmetrics pmda not installed"

test -x $PCP_BINADM_DIR/pmseries_import || _notrun "No pmseries_import script"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

pmseries_import is checked here but never used.

The script loads archives directly via pmseries $options --load "{source.path: ...}" (line 160), and never invokes pmseries_import. This _notrun gate will spuriously skip the test on systems that ship pmseries but not pmseries_import (or are renamed in a future packaging change), without any actual dependency on that tool. Drop the check, or replace it with whatever the test actually depends on (e.g., pmseries itself).

🧹 Suggested fix
-test -x $PCP_BINADM_DIR/pmseries_import || _notrun "No pmseries_import script"
-_check_series
+_check_series
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qa/1744` at line 17, The test currently gates execution on presence of
pmseries_import (checked via the test invoking pmseries_import and calling
_notrun), but the test never calls pmseries_import; instead it uses pmseries to
load archives, so remove the pmseries_import existence check and either drop
that _notrun gate entirely or replace it with a check for pmseries (the binary
actually used by the test). Update the check that calls _notrun to reference
pmseries (or remove it) and keep the rest of the test logic unchanged.

_check_series

status=1 # failure is the default!

_cleanup()
{
cd $here
[ -n "$pmloggerpid" ] && $sudo kill -TERM $pmloggerpid 2>/dev/null
if [ -n "$key_server_port" ]; then
echo "Shutting down key server on port $key_server_port..." >>$here/$seq.full
$keys_cli -p $key_server_port shutdown >>$here/$seq.full 2>&1
sleep 1
# Verify it's actually dead
if $keys_cli -p $key_server_port PING >>$here/$seq.full 2>&1; then
echo "WARNING: Key server still running after shutdown!" >>$here/$seq.full
else
echo "Key server shutdown confirmed" >>$here/$seq.full
fi
fi
$sudo rm -rf $PCP_ETC_DIR/pcp/labels/*
_restore_config $PCP_ETC_DIR/pcp/labels
_sighup_pmcd
_pmdaopenmetrics_cleanup
_restore_config $PCP_SYSCONF_DIR/pmseries
$sudo rm -rf $tmp $tmp.*
}
Comment on lines +22 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Cleanup deletes $PCP_ETC_DIR/pcp/labels/* even if _save_config failed.

Line 37 unconditionally rm -rfs everything under $PCP_ETC_DIR/pcp/labels/ before line 38 attempts _restore_config. If _save_config on line 91 was never reached (e.g., earlier _notrun / failure), the cleanup will still wipe a system directory it never backed up. Guard the destructive rm with a flag set after _save_config succeeds, or move the rm inside the same conditional that runs the test body.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qa/1744` around lines 22 - 43, The cleanup function unconditionally removes
$PCP_ETC_DIR/pcp/labels/* which can delete real system files if _save_config
never ran; modify the test so that _save_config sets a guard flag (e.g.,
labels_saved=1) after it successfully completes (reference the _save_config call
and the _restore_config function) and change _cleanup to only perform the
destructive "$sudo rm -rf $PCP_ETC_DIR/pcp/labels/*" when that guard is set (or
alternatively move the rm into the same conditional block that runs the test
body), ensuring _restore_config is only relied on when a prior backup was made.


_prepare_pmda openmetrics
trap "_cleanup; exit \$status" 0 1 2 3 15
_stop_auto_restart pmcd

_filter()
{
# Replace machine-specific label values with placeholders
hostname=`hostname`
machineid=`_machine_id`
domainid=`_domain_name`
sed \
-e "s/\"hostname\":\"$hostname\"/\"hostname\":\"HOSTNAME\"/g" \
-e "s/\"domainname\":\"$domainid\"/\"domainname\":\"DOMAINNAME\"/g" \
-e "s/\"machineid\":\"$machineid\"/\"machineid\":\"MACHINEID\"/g" \
-e 's/"groupid":[0-9]*/"groupid":GROUPID/g' \
-e 's/"userid":[0-9]*/"userid":USERID/g'
}

_filter_series()
{
sed \
-e 's/[0-9a-z]\{40\}/TIMESERIES/g' \
#end
}

# real QA test starts here

key_server_port=`_find_free_port`
_save_config $PCP_SYSCONF_DIR/pmseries
$sudo rm -f $PCP_SYSCONF_DIR/pmseries/*

# Create pmseries config pointing to our test key server
$sudo tee $PCP_SYSCONF_DIR/pmseries/pmseries.conf > /dev/null <<EOF
[pmseries]
servers = localhost:$key_server_port
EOF

echo "Start test key server on port $key_server_port..." >>$seq.full
$key_server --port $key_server_port --save "" > $tmp.keys 2>&1 &
_check_key_server_ping $key_server_port
_check_key_server $key_server_port
echo

_check_key_server_version $key_server_port

_pmdaopenmetrics_save_config
_save_config $PCP_ETC_DIR/pcp/labels
$sudo rm -rf $PCP_ETC_DIR/pcp/labels/*

# add the URL for this test
# need to be a place the user $PCP_USER (pmcd) can read
#
file=pmseries_label_test.txt
cp $here/openmetrics/samples/$file $tmp.$file
urlbase=`basename "$file" .txt | tr .- _`
echo 'file://'$tmp.$file >$tmp.tmp

# add a bunch of label filters to exercise various filtering options
cat <<EOF >>$tmp.tmp

FILTER: OPTIONAL LABEL some_optional_label

EOF
$sudo cp $tmp.tmp $PCP_PMDAS_DIR/openmetrics/config.d/$urlbase.url
Comment on lines +102 to +108
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate openmetrics PMDA FILTER parsing to confirm whether the argument is a label name, value, or regex.
fd -t f 'pmda(openmetrics|.*\.python)' src/pmdas/openmetrics 2>/dev/null
rg -nP -C5 '\bFILTER\s*:?\s*OPTIONAL\b' --type=py --type=sh --type=md
rg -nP -C5 'OPTIONAL\s+LABEL' 
rg -nP -C3 'PM_LABEL_OPTIONAL' --type=c --type=py

Repository: performancecopilot/pcp

Length of output: 20928


🏁 Script executed:

# Find and examine the label test file to confirm label name and value
find . -name "*pmseries_label_test*" -o -name "*label_test*" | head -20

Repository: performancecopilot/pcp

Length of output: 115


🏁 Script executed:

# Search for the actual label name and value in openmetrics samples
rg -n "test_metric2|optional_label" --type=txt --type=py src/pmdas/openmetrics/ qa/

Repository: performancecopilot/pcp

Length of output: 447


🏁 Script executed:

# Look for the filter parsing code in the openmetrics PMDA
fd -t f "openmetrics" src/pmdas/openmetrics/ | grep -E "\.(py|c)$"

Repository: performancecopilot/pcp

Length of output: 48


🏁 Script executed:

# Find the openmetrics PMDA Python implementation
find . -path "*/pmdas/openmetrics*.python" -type f

Repository: performancecopilot/pcp

Length of output: 113


🏁 Script executed:

# Search for the OPTIONAL LABEL filter parsing in Python code
rg -n "OPTIONAL|LABEL" src/pmdas/openmetrics/pmdaopenmetrics.python -A 5 -B 5

Repository: performancecopilot/pcp

Length of output: 6815


🏁 Script executed:

# Check the entire config parsing section
rg -n "FILTER" src/pmdas/openmetrics/pmdaopenmetrics.python -A 3 -B 3 | head -100

Repository: performancecopilot/pcp

Length of output: 1636


Fix filter to use label name instead of value: change some_optional_label to optional_label

The filter incorrectly uses the label value instead of the label name. The label is defined as optional_label="some_optional_label", where optional_label is the name and some_optional_label is the value. The PMDA filter code (pmdaopenmetrics.python line 632) matches against label names, not values, so the filter FILTER: OPTIONAL LABEL some_optional_label will never match. It should be FILTER: OPTIONAL LABEL optional_label.

Note: The test files contain a comment stating "*** NOTE: optional labels are not implemented yet" — confirm whether this feature is active before merging.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qa/1744` around lines 102 - 108, The FILTER line in the test config uses the
label value instead of the label name so pmdaopenmetrics (pmdaopenmetrics.python
matching logic) will never match; update the here-doc content that adds "FILTER:
OPTIONAL LABEL some_optional_label" to use the label name "optional_label"
instead (i.e., replace some_optional_label with optional_label in the cat <<EOF
block that writes $tmp.tmp) and verify the optional-label feature is enabled
(the test comment "*** NOTE: optional labels are not implemented yet") before
merging so the test will exercise the behavior.


_pmdaopenmetrics_install

if ! _pmdaopenmetrics_wait_for_metric openmetrics.control.calls
then
status=1
exit
fi

# Verify metrics are available from PMCD
echo "Verify openmetrics metrics are available ..."
pminfo -f openmetrics.pmseries_label_test.test_metric1 #>>$seq_full 2>&1
pminfo -f openmetrics.pmseries_label_test.test_metric2 #>>$seq_full 2>&1
echo

# Create a test archive with pmlogger
echo "Creating test archive with pmlogger ..."
cat > $tmp.pmlogger.config <<EOF
log mandatory on once {
openmetrics.pmseries_label_test
}
EOF
pmlogger -c $tmp.pmlogger.config -l $tmp.pmlogger.log -t 1sec $tmp.archive &
pmloggerpid=$!
echo "=== pmlogger started, PID=$pmloggerpid ===" >>$seq_full

# Wait for pmlogger to capture some data
pmsleep 3

# Stop pmlogger
$sudo kill -TERM $pmloggerpid 2>/dev/null
pmsleep 1

# Verify archive was created
echo "Archive files created:" >>$seq_full
ls -l $tmp.archive* >>$seq_full 2>&1

# Check key server port and flush
echo "Using key server on port $key_server_port" >>$seq_full
echo "Flushing key server on port $key_server_port ..."
flush_result=`echo "FLUSHALL" | $keys_cli -p $key_server_port 2>&1`
echo "Flush result: $flush_result" >>$seq_full
if [ "$flush_result" != "OK" ]; then
echo "ERROR: Failed to flush key server, got: $flush_result"
status=1
exit
fi
echo "Key server flushed successfully"

# Load the archive into pmseries
echo "Loading test archive into pmseries ..."
pmseries $options --load "{source.path: \"$tmp.archive\"}" 2>&1 | \
sed "s|$tmp.archive|ARCHIVE|g" | tee -a $seq_full

# Wait for metrics to be indexed
echo "Waiting for metrics to be indexed ..."
pmsleep 2

# Query for the first metric (identifying labels only)
echo
echo "=== test_metric1 (identifying labels only) ==="
series1=`pmseries $options 'openmetrics.pmseries_label_test.test_metric1'`
if [ -z "$series1" ]; then
echo "ERROR: No series found for test_metric1"
status=1
else
pmseries $series1 | _filter_series | _filter
fi

# Query for the second metric (with optional label)
echo
echo "=== test_metric2 (with optional label) ==="
series2=`pmseries $options 'openmetrics.pmseries_label_test.test_metric2'`
if [ -z "$series2" ]; then
echo "ERROR: No series found for test_metric2"
status=1
else
pmseries $series2 | _filter_series | _filter
fi
Comment on lines +170 to +187
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Test sets status=1 on missing series but continues to next check.

If series1 is empty on line 171, the test prints ERROR: and sets status=1 but falls through to query test_metric2. That's fine for debugging, but the trailing status=0 on line 198 unconditionally clears the failure flag, so a missed-series error on test_metric1/test_metric2 is silently masked at exit time.

🐛 Suggested fix
-# success, all done
-status=0
-exit
+# success, all done — but only if no earlier ERROR set status=1
+[ "$status" = 1 ] || status=0
+exit

Or simpler: track failures with a separate flag and only clear it at the end if no failure occurred.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qa/1744` around lines 170 - 187, The script sets status=1 when a series is
missing (in the checks around series1 and series2) but later unconditionally
resets status=0, masking failures; fix by preserving failure state instead of
clearing it: remove the unconditional status=0 reset (or introduce a separate
boolean like had_failure that you set to true on any "ERROR" and only set
status=0 at the very end if had_failure is false), ensure the checks in the
pmseries blocks (series1, series2) only set status=1/had_failure and never
overwrite it with success later so the exit value reflects any missing-series
errors.


echo == Note: check $seq.full for details
echo == pmdaopenmetrics LOG == >>$seq_full
cat $PCP_LOG_DIR/pmcd/openmetrics.log >>$seq_full
echo == pmlogger LOG == >>$seq_full
cat $tmp.pmlogger.log >>$seq_full 2>&1

_pmdaopenmetrics_remove

# success, all done
status=0
exit
50 changes: 50 additions & 0 deletions qa/1744.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
QA output created by 1744
PING
PONG


=== openmetrics agent installation ===
Verify openmetrics metrics are available ...

openmetrics.pmseries_label_test.test_metric1
inst [0 or "0 label1:somelabel"] value 1

openmetrics.pmseries_label_test.test_metric2
inst [0 or "0 label2:somelabel optional_label:some_optional_label"] value 2

Creating test archive with pmlogger ...
Flushing key server on port 54321 ...
Key server flushed successfully
Loading test archive into pmseries ...
pmseries: [Info] processed 3 archive records from ARCHIVE
Waiting for metrics to be indexed ...

=== test_metric1 (identifying labels only) ===

TIMESERIES
PMID: 144.1.0
Data Type: double InDom: 144.5120 0x24001400
Semantics: instant Units: none
Source: TIMESERIES
Metric: openmetrics.pmseries_label_test.test_metric1
inst [0 or "0 label1:somelabel"] series TIMESERIES
inst [0 or "0 label1:somelabel"] labels {"agent":"openmetrics","domainname":"DOMAINNAME","groupid":GROUPID,"hostname":"HOSTNAME","machineid":"MACHINEID","source":"pmseries_label_test","userid":USERID}

=== test_metric2 (with optional label) ===

TIMESERIES
PMID: 144.1.1
Data Type: double InDom: 144.5121 0x24001401
Semantics: instant Units: none
Source: TIMESERIES
Metric: openmetrics.pmseries_label_test.test_metric2
inst [0 or "0 label2:somelabel optional_label:some_optional_label"] series TIMESERIES
inst [0 or "0 label2:somelabel optional_label:some_optional_label"] labels {"agent":"openmetrics","domainname":"DOMAINNAME","groupid":GROUPID,"hostname":"HOSTNAME","machineid":"MACHINEID","source":"pmseries_label_test","userid":USERID}
== Note: check 1744.full for details

=== remove openmetrics agent ===
Culling the Performance Metrics Name Space ...
openmetrics ... done
Updating the PMCD control file, and notifying PMCD ...
[...removing files...]
Check openmetrics metrics have gone away ... OK
6 changes: 3 additions & 3 deletions qa/common.check
Original file line number Diff line number Diff line change
Expand Up @@ -1559,12 +1559,12 @@ _check_key_server_version()
return
fi

if which valkey-cli >/dev/null 2>&1 && test -n `_get_pids_by_name valkey-server`

if which valkey-cli >/dev/null 2>&1 && test -n "`_get_pids_by_name valkey-server`"
then
keys_cli=valkey-cli
keys_ver=valkey_version
elif which redis-cli >/dev/null 2>&1 && test -n _get_pids_by_name redis-server
elif which redis-cli >/dev/null 2>&1 && test -n "`_get_pids_by_name redis-server`"
then
keys_cli=redis-cli
keys_ver=redis_version
Expand Down
1 change: 1 addition & 0 deletions qa/group
Original file line number Diff line number Diff line change
Expand Up @@ -2248,6 +2248,7 @@ suse
1727 pmproxy libpcp_web pmda.openmetrics local
1735 python pmimport local
1740 pmda.proc local
1744 pmseries local
1745 pmlogger libpcp pmval local pmda.sample pmda.simple pmlogdump
1747 pmlogger labels local
1748 atop local
Expand Down
8 changes: 8 additions & 0 deletions qa/openmetrics/samples/pmseries_label_test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# HELP test_metric1 local metric
# Type test_metric1 gauge
test_metric1 {label1="somelabel"} 1


# HELP test_metric2 local metric
# Type test_metric2 gauge
test_metric2 {label2="somelabel", optional_label="some_optional_label"} 2
44 changes: 35 additions & 9 deletions src/libpcp_web/src/util.c
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,13 @@ instance_labelsets(indom_t *indom, instance_t *inst, char *buffer, int length,
return pmMergeLabelSets(sets, nsets, buffer, length, filter, arg);
}

/* extract all labels (identifying and optional) */
static int
all_labels(const pmLabel *label, const char *json, void *arg)
{
return 1;
}

/* extract only the identifying labels (not optional) */
static int
labels(const pmLabel *label, const char *json, void *arg)
Expand Down Expand Up @@ -276,7 +283,13 @@ pmwebapi_source_meta(context_t *c, char *buffer, int length)
static int
context_labels_str(context_t *c, char *buffer, int length)
{
return pmMergeLabelSets(&c->labelset, 1, buffer, length, NULL, NULL);
return pmMergeLabelSets(&c->labelset, 1, buffer, length, all_labels, NULL);
}

static int
context_labels_str_identifying(context_t *c, char *buffer, int length)
{
return pmMergeLabelSets(&c->labelset, 1, buffer, length, labels, NULL);
}

int
Expand Down Expand Up @@ -447,14 +460,17 @@ int
pmwebapi_context_hash(context_t *context)
{
char labels[PM_MAXLABELJSONLEN];
char idlabels[PM_MAXLABELJSONLEN];
int sts;

if (context->labels == NULL) {
if ((sts = context_labels_str(context, labels, sizeof(labels))) < 0)
return sts;
context->labels = sdsnewlen(labels, sts);
}
return pmwebapi_source_hash(context->name.hash, context->labels, sdslen(context->labels));
if ((sts = context_labels_str_identifying(context, idlabels, sizeof(idlabels))) < 0)
return sts;
return pmwebapi_source_hash(context->name.hash, idlabels, sts);
}

void
Expand All @@ -464,22 +480,27 @@ pmwebapi_metric_hash(metric_t *metric)
pmDesc *desc = &metric->desc;
sds identifier;
char buf[PM_MAXLABELJSONLEN];
char idbuf[PM_MAXLABELJSONLEN];
char sem[32], type[32], units[64];
int len, i;

if (metric->labels == NULL) {
len = metric_labelsets(metric, buf, sizeof(buf), labels, NULL);
len = metric_labelsets(metric, buf, sizeof(buf), all_labels, NULL);
if (len <= 0)
len = pmsprintf(buf, sizeof(buf), "null");
metric->labels = sdsnewlen(buf, len);
}

len = metric_labelsets(metric, idbuf, sizeof(idbuf), labels, NULL);
if (len <= 0)
len = pmsprintf(idbuf, sizeof(idbuf), "null");

identifier = sdsempty();
for (i = 0; i < metric->numnames; i++) {
identifier = sdscatfmt(identifier,
"{\"series\":\"metric\",\"name\":\"%S\",\"labels\":%S,"
"{\"series\":\"metric\",\"name\":\"%S\",\"labels\":%s,"
"\"semantics\":\"%s\",\"type\":\"%s\",\"units\":\"%s\"}",
metric->names[i].sds, metric->labels,
metric->names[i].sds, idbuf,
pmSemStr_r(desc->sem, sem, sizeof(sem)),
pmTypeStr_r(desc->type, type, sizeof(type)),
pmUnitsStr_r(&desc->units, units, sizeof(units)));
Expand All @@ -501,7 +522,7 @@ pmwebapi_add_indom_labels(indom_t *indom)
int len;

if (indom->labels == NULL) {
len = instance_labelsets(indom, NULL, buf, sizeof(buf), labels, NULL);
len = instance_labelsets(indom, NULL, buf, sizeof(buf), all_labels, NULL);
if (len <= 0)
len = pmsprintf(buf, sizeof(buf), "null");
indom->labels = sdsnewlen(buf, len);
Expand All @@ -514,18 +535,23 @@ pmwebapi_instance_hash(indom_t *ip, instance_t *instance)
SHA1_CTX shactx;
sds identifier;
char buf[PM_MAXLABELJSONLEN];
char idbuf[PM_MAXLABELJSONLEN];
int len;

if (instance->labels == NULL) {
len = instance_labelsets(ip, instance, buf, sizeof(buf), labels, NULL);
len = instance_labelsets(ip, instance, buf, sizeof(buf), all_labels, NULL);
if (len <= 0)
len = pmsprintf(buf, sizeof(buf), "null");
instance->labels = sdsnewlen(buf, len);
}

len = instance_labelsets(ip, instance, idbuf, sizeof(idbuf), labels, NULL);
if (len <= 0)
len = pmsprintf(idbuf, sizeof(idbuf), "null");

identifier = sdscatfmt(sdsempty(),
"{\"series\":\"instance\",\"name\":\"%S\",\"labels\":%S}",
instance->name.sds, instance->labels);
"{\"series\":\"instance\",\"name\":\"%S\",\"labels\":%s}",
instance->name.sds, idbuf);
/* Calculate unique instance identifier 20-byte SHA1 hash */
SHA1Init(&shactx);
SHA1Update(&shactx, (unsigned char *)identifier, sdslen(identifier));
Expand Down
Loading