Metrics: Reliability, Maintainability, Security, Complexity, and Size: Guided Hands-On Workflow
Capture a metric baseline, make one controlled revision that increases structural complexity and adds uncovered code, reanalyze, compare raw measures and ratings, and explain every changed and unchanged signal from evidence.
Learning objectives
- Create a disposable Python project with a reproducible baseline revision, tests, coverage report, and full Git history.
- Capture a mode-aware baseline metric snapshot only after the corresponding Compute Engine task succeeds.
- Predict the direction of structural and coverage measures before adding a deliberately complex, untested function.
- Reanalyze the changed revision and compare raw measures, ratings, New Code, and Overall Code without assuming every metric should move.
- Explain why complexity or LOC can increase while reliability/security ratings stay unchanged, and why maintainability rating can remain stable even if remediation effort changes.
- Preserve task IDs, metric snapshots, source revisions, coverage evidence, and credential cleanup as one auditable chain.
1. Lab contract and dated assumptions
The lab uses a tiny Python project because it can produce deterministic structural and coverage changes without requiring a commercial feature. It does not aim for a particular letter rating. The experiment is successful when the evidence explains the observed values—even if an analyzer/rule update changes an issue count.
-
SonarQube Community Build 26.9.0.129388 at
http://localhost:9000. - SonarScanner CLI 8.1.0.6389 with default JRE auto-provisioning where supported.
- Python 3.11+; pytest and pytest-cov.
- Disposable project key
academy-sq-ch13. - Full local Git history.
-
SONAR_TOKEN: project-analysis token used only by the scanner. -
SONAR_API_TOKEN: short-lived lab user token with Browse permission for read-only metric API calls. -
SONAR_ADMIN_TOKEN: optional short-lived project-admin token used only to pin the Specific-analysis New Code baseline. - No third-party plugin, paid CI, enterprise identity, commercial edition, external database, Kubernetes, or hosted service is required.
sonar-project.properties. The scanner reads
SONAR_TOKEN from the environment; API calls use bearer
authentication. Revoke disposable tokens after the lab.
2. Create the fixture and baseline revision
mkdir -p academy-sq-ch13/src academy-sq-ch13/tests academy-sq-ch13/evidence
cd academy-sq-ch13
git init
git config user.name "Academy Learner"
git config user.email "academy@example.invalid"
cat > .gitignore <<'EOF'
.venv/
.scannerwork/
.coverage
coverage.xml
__pycache__/
.pytest_cache/
evidence/
EOF
python -m venv .venv
# Linux/macOS
. .venv/bin/activate
# Windows PowerShell equivalent: .\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install pytest pytest-cov
cat > src/pricing.py <<'PY'
def subtotal(lines):
total = 0.0
for quantity, unit_price in lines:
if quantity < 0 or unit_price < 0:
raise ValueError("negative input")
total += quantity * unit_price
return round(total, 2)
def shipping(total, expedited=False):
if expedited:
return 15.0
if total >= 100:
return 0.0
return 6.0
def invoice_total(lines, tax_rate=0.0, expedited=False):
base = subtotal(lines)
tax = base * tax_rate
return round(base + tax + shipping(base, expedited), 2)
PY
cat > tests/test_pricing.py <<'PY'
import pytest
from src.pricing import invoice_total, shipping, subtotal
def test_subtotal_and_validation():
assert subtotal([(2, 10.0), (1, 5.5)]) == 25.5
with pytest.raises(ValueError):
subtotal([(-1, 10.0)])
def test_shipping():
assert shipping(50.0) == 6.0
assert shipping(100.0) == 0.0
assert shipping(50.0, expedited=True) == 15.0
def test_invoice_total():
assert invoice_total([(2, 50.0)], 0.10) == 110.0
PY
cat > sonar-project.properties <<'EOF'
sonar.projectKey=academy-sq-ch13
sonar.projectName=Academy SonarQube Chapter 13 Metrics Lab
sonar.sources=src
sonar.tests=tests
sonar.sourceEncoding=UTF-8
sonar.python.coverage.reportPaths=coverage.xml
EOF
git add .gitignore src tests sonar-project.properties
git commit -m "ch13 metric baseline"
git rev-parse HEAD | tee evidence/baseline-revision.txt
pytest --cov=src --cov-report=term-missing --cov-report=xml
cp coverage.xml evidence/baseline-coverage.xml
Before scanning, predict: baseline coverage should be high, LOC/complexity modest, duplication low, and there should be no reason to expect security/reliability ratings to degrade. Exact values are analyzer evidence, not constants to invent.
3. Run baseline analysis and wait for Compute Engine
Create the disposable project in the local UI if it does not yet
exist and generate the project-analysis token. Keep
SONAR_HOST_URL, SONAR_TOKEN, and API/admin
tokens in the environment.
export SONAR_HOST_URL="http://localhost:9000"
export PROJECT_KEY="academy-sq-ch13"
sonar-scanner \
-Dsonar.projectVersion="1.0.0" \
-Dsonar.buildString="ch13-baseline"
cp .scannerwork/report-task.txt evidence/baseline-report-task.txt
cat .scannerwork/report-task.txt
export CE_TASK_ID="$(sed -n 's/^ceTaskId=//p' .scannerwork/report-task.txt)"
while :; do
curl -fsS -H "Authorization: Bearer $SONAR_API_TOKEN" \
"$SONAR_HOST_URL/api/ce/task?id=$CE_TASK_ID" > evidence/ce-current.json
STATUS="$(python -c 'import json; print(json.load(open("evidence/ce-current.json"))["task"]["status"])')"
echo "Compute Engine: $STATUS"
case "$STATUS" in
SUCCESS) break ;;
FAILED|CANCELED) echo "Preserve evidence and diagnose before continuing"; exit 1 ;;
esac
sleep 2
done
cp evidence/ce-current.json evidence/baseline-ce-task.json
Scanner exit 0 proves the scanner completed its local/upload phase. The persisted metric state belongs to the successfully completed Compute Engine task, so measurements are taken only after that state is proven.
4. Create a mode-aware metric snapshot helper
The helper discovers metric keys from the running instance and requests only keys that exist. This prevents the lab from confusing MQR and Standard Experience.
cat > snapshot_metrics.py <<'PY'
import json, os, sys, urllib.parse, urllib.request
host = os.environ["SONAR_HOST_URL"].rstrip("/")
project = os.environ["PROJECT_KEY"]
token = os.environ["SONAR_API_TOKEN"]
outfile = sys.argv[1]
def get(path, params=None):
url = host + path
if params:
url += "?" + urllib.parse.urlencode(params)
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(req) as response:
return json.load(response)
catalog = get("/api/metrics/search", {"ps": 500})
available = {m["key"] for m in catalog.get("metrics", [])}
core = [
"ncloc", "lines", "complexity", "cognitive_complexity",
"coverage", "line_coverage", "duplicated_lines_density",
"new_lines", "new_coverage", "new_line_coverage",
"new_duplicated_lines_density", "violations", "new_violations",
]
mqr = [
"software_quality_reliability_issues",
"software_quality_reliability_rating",
"software_quality_security_issues",
"software_quality_security_rating",
"software_quality_maintainability_issues",
"software_quality_maintainability_remediation_effort",
"software_quality_maintainability_debt_ratio",
"software_quality_maintainability_rating",
"new_software_quality_maintainability_issues",
"new_software_quality_maintainability_remediation_effort",
"new_software_quality_maintainability_debt_ratio",
"new_software_quality_maintainability_rating",
]
standard = [
"bugs", "reliability_rating", "vulnerabilities", "security_rating",
"code_smells", "sqale_index", "sqale_debt_ratio", "sqale_rating",
"new_bugs", "new_reliability_rating", "new_vulnerabilities",
"new_security_rating", "new_code_smells", "new_technical_debt",
"new_sqale_debt_ratio", "new_maintainability_rating",
]
selected = [k for k in core + mqr + standard if k in available]
measures = get("/api/measures/component", {
"component": project,
"metricKeys": ",".join(selected),
})
result = {
"project": project,
"selected_metric_keys": selected,
"measures": measures,
}
with open(outfile, "w", encoding="utf-8") as fh:
json.dump(result, fh, indent=2, sort_keys=True)
print(f"wrote {outfile} with {len(selected)} supported metric keys")
PY
python snapshot_metrics.py evidence/baseline-metrics.json
Also record the instance’s visible Mode setting in
evidence/instance-mode.txt. The helper’s available-key
set is evidence, but the administrator-visible mode is the clearest
governance record.
5. Pin the baseline for New Code comparison
Reuse the Chapter 12 control rather than inventing a new baseline
rule. Identify the baseline analysis by
buildString=ch13-baseline, record its analysis key,
then set that key as the project’s Specific analysis baseline with a
disposable project-admin token.
curl -fsS -H "Authorization: Bearer $SONAR_ADMIN_TOKEN" --get \
--data-urlencode "project=$PROJECT_KEY" \
--data-urlencode "ps=20" \
"$SONAR_HOST_URL/api/project_analyses/search" \
> evidence/project-analyses.json
export BASELINE_ANALYSIS_KEY="<analysis-key-for-buildString-ch13-baseline>"
curl -fsS -X POST -H "Authorization: Bearer $SONAR_ADMIN_TOKEN" \
-d "project=$PROJECT_KEY" \
-d "type=SPECIFIC_ANALYSIS" \
-d "value=$BASELINE_ANALYSIS_KEY" \
"$SONAR_HOST_URL/api/new_code_periods/set"
curl -fsS -H "Authorization: Bearer $SONAR_ADMIN_TOKEN" --get \
--data-urlencode "project=$PROJECT_KEY" \
"$SONAR_HOST_URL/api/new_code_periods/show" \
> evidence/new-code-definition.json
6. Predict the controlled change before editing source
| Metric family | Prediction | Why |
|---|---|---|
ncloc |
Increase | New non-comment source lines will be added. |
complexity |
Increase | The added function contains multiple decision branches. |
cognitive_complexity |
Increase | Nested/compound control flow increases comprehension burden. |
coverage |
Decrease | The new function is intentionally not covered by tests, while coverage import remains enabled. |
new_coverage |
Low / below baseline | The Specific-analysis New Code population contains the new uncovered function. |
| Duplication density | No deliberate change | The new code is not copied from existing blocks. |
| Reliability/security ratings | Probably unchanged | The change is intended to affect structure, not create a reliability/security issue; actual analyzer evidence wins. |
| Maintainability issues/effort/rating | May change; rating may still stay in same band | Active complexity-related rules can raise maintainability issues, but ratings are banded debt-ratio outputs. |
7. Add a deliberately complex, untested function
cat >> src/pricing.py <<'PY'
def route_order(country, total, fragile, expedited, customer_tier):
if country == "local":
if expedited:
if fragile:
return "local-priority-fragile"
return "local-priority"
if total > 250:
return "local-free"
return "local-standard"
elif country == "regional":
if fragile and expedited:
return "regional-secure-priority"
if fragile:
return "regional-secure"
if expedited:
return "regional-priority"
return "regional-standard"
else:
if customer_tier == "vip":
if expedited:
return "international-vip-priority"
return "international-vip"
if total > 1000 and not fragile:
return "international-consolidated"
if fragile:
return "international-secure"
return "international-standard"
PY
git add src/pricing.py
git commit -m "add deliberately complex routing example"
git rev-parse HEAD | tee evidence/change-revision.txt
pytest --cov=src --cov-report=term-missing --cov-report=xml
cp coverage.xml evidence/change-coverage.xml
Do not add a test for route_order() yet. The temporary
lack of coverage is intentional laboratory evidence; the repository
is synthetic and isolated. Chapter 14 will later focus on coverage
ingestion and test-report semantics in depth.
8. Reanalyze and capture the second snapshot
sonar-scanner \
-Dsonar.projectVersion="1.0.1" \
-Dsonar.buildString="ch13-complex-change"
cp .scannerwork/report-task.txt evidence/change-report-task.txt
export CE_TASK_ID="$(sed -n 's/^ceTaskId=//p' .scannerwork/report-task.txt)"
while :; do
curl -fsS -H "Authorization: Bearer $SONAR_API_TOKEN" \
"$SONAR_HOST_URL/api/ce/task?id=$CE_TASK_ID" > evidence/ce-current.json
STATUS="$(python -c 'import json; print(json.load(open("evidence/ce-current.json"))["task"]["status"])')"
case "$STATUS" in SUCCESS) break ;; FAILED|CANCELED) exit 1 ;; esac
sleep 2
done
cp evidence/ce-current.json evidence/change-ce-task.json
python snapshot_metrics.py evidence/change-metrics.json
9. Compare values without forcing a narrative
cat > compare_metrics.py <<'PY'
import json, sys
def values(path):
data = json.load(open(path, encoding="utf-8"))
measures = data["measures"]["component"].get("measures", [])
out = {}
for m in measures:
if "value" in m:
out[m["metric"]] = m["value"]
elif "period" in m and "value" in m["period"]:
out[m["metric"]] = m["period"]["value"]
else:
out[m["metric"]] = "<no-value>"
return out
before = values(sys.argv[1])
after = values(sys.argv[2])
keys = sorted(set(before) | set(after))
print(f"{'metric':48} {'before':>18} {'after':>18}")
print("-" * 88)
for key in keys:
print(f"{key:48} {str(before.get(key, '-')):>18} {str(after.get(key, '-')):>18}")
PY
python compare_metrics.py evidence/baseline-metrics.json evidence/change-metrics.json \
| tee evidence/metric-delta.txt
Interpret in causal order. First prove revision and indexed source changed as intended. Then verify CE success. Then inspect raw structural measures. Only after that interpret issues, effort, ratios, and ratings. If a rating did not change, explain its derivation instead of calling the scanner “wrong.” If an unexpected issue appears, preserve its rule key and evidence and update the interpretation.
10. Small challenge: choose the right evidence layer
Your manager asks: “Complexity rose by 40%; did application performance become 40% worse?” Write a response that identifies the wrong inference, names the metric’s actual scope, and specifies the performance evidence that would be required. Then answer a second question: “Maintainability is still A, so can we ignore the new complexity?” Explain why the rating and diagnostic structural measures answer different questions.
Knowledge check
Why wait for Compute Engine before capturing measures?
The scanner uploads an analysis report asynchronously. Persisted project measures belong to the completed server-side analysis task, not merely to scanner process exit.
The complexity rises and reliability rating stays A. Is that contradictory?
No. Complexity is structural; reliability rating is issue/severity-derived. They can move independently.
Why does the helper discover metric keys first?
To remain compatible with the actual instance mode/version and avoid mixing MQR and Standard Experience metric families.
Why can new coverage be much lower than overall coverage?
The two metrics use different code populations. Under the pinned baseline, the deliberately uncovered changed code dominates the New Code denominator while historical covered code remains in Overall Code.
What should happen if a predicted maintainability issue does not appear?
Inspect the active profile/rule, analyzer version, source indexing, and actual metric values. Do not change the profile just to force the expected demonstration.
Official references and version notes
- Understanding measures and metrics — current metric definitions and metric keys for software qualities, maintainability, coverage, duplication, size, complexity, and issues.
- Code metrics introduction — how metrics participate in rules, quality gates, monitoring, and mode-dependent UI behavior.
- Changing instance modes — MQR versus Standard Experience classifications, severities, and metric-family implications.
- Instance mode overview — current MQR/Standard model and the default for new Community Build instances.
-
Web API
— bearer authentication,
/api/measures,/api/metrics, and Web API V2 migration guidance. - Understanding quality gates — how selected measures become enforceable policy rather than descriptive dashboards.
-
New Code
— population semantics used by
new_*measures. - Analysis overview — scanner/report/Compute Engine lifecycle behind persisted measures.
- Test coverage overview — current external-coverage model; Chapter 14 expands this subject.
- SonarQube downloads — current Community Build release identity.
- SonarScanner CLI 8.1.0.6389 — scanner baseline used by the local lab.
Rechecked 2026-09-07. Mandatory executable examples target
SonarQube Community Build 26.9.0.129388 and
SonarScanner CLI 8.1.0.6389. New Community Build
instances use MQR Mode by default, but upgraded instances can
remain in Standard Experience; therefore examples discover
available metric keys and record the actual instance mode rather
than hard-coding one issue/rating vocabulary. Core structural keys
used in both modes include ncloc, lines,
complexity, cognitive_complexity,
coverage, and duplicated_lines_density.
MQR and Standard issue/rating families are treated as distinct.
Re-check primary documentation and the instance’s built-in Web API
before automating against another release.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.