Operational upgrade notes for Flowfish. See CHANGELOG.md at the repository root for the full release history.
This release fixes HTTP path visibility across the entire L7 pipeline.
Before v2.7.0 the Service Map and Integration Hub displayed / for
every outbound HTTP dependency (url.path only ever exists on SERVER
spans, Beyla CLIENT spans use url.full), and a Neo4j MERGE key that
collapsed every request between two workloads onto a single edge —
overwriting the previous path on every flush. Operators saw the same
Grafana data shown correctly in their own dashboards but only / in
Flowfish.
The fix is in five layers: span-parsing helper rewrite, MERGE key extension to per-path granularity, dedup migration alignment, frontend canvas bundling, and graph-query distinct-peer counting.
None for API consumers. Behavioural change for Neo4j edge counts
(see below) — automation that asserts "exactly one
L7_COMMUNICATES_WITH between (src, dst)" will need updating.
| Layer | File | Change |
|---|---|---|
| Span parsing | services/flowfish-l7-collector/app/event_transformer.py |
_extract_http_path walks url.path → http.route → url.full → http.target → http.url with relative-path guard for malformed values. HTTP branch trigger extended to cover CLIENT-only spans (url.full, http.url, http.target). Method falls back to legacy http.method. |
| Graph write | services/graph-writer/app/l7_graph_builder.py |
L7_COMMUNICATES_WITH MERGE key is now (analysis_id, http_method, http_path) with coalesce(..., '') to satisfy Cypher's null-in-MERGE-key restriction. _MIGRATE_OUT_CYPHER / _MIGRATE_IN_CYPHER use the same composite key so dedup doesn't collapse per-path edges. |
| Schema | services/graph-writer/app/graph_client.py |
New relationship property indexes l7_comm_method, l7_comm_path (Neo4j 4.3+, IF NOT EXISTS so it's idempotent). |
| Graph read | services/graph-query/app/graph_query_engine.py |
get_l7_dependency_summary aggregates distinct peers, not edges, so per-path multiplication doesn't inflate inbound_count / outbound_count. |
| Frontend canvas | frontend/src/pages/ServiceMap.tsx |
Edge bundling: raw per-path edges grouped by (source, target, protocol) into a single React Flow edge. Bundle data includes a paths array. Edge CSV export expands paths so the CSV stays operator-useful. |
| Frontend table | frontend/src/pages/IntegrationHub.tsx |
Edges Table rowKey extended with (cluster, namespace, method, path) to eliminate React rowKey collisions under per-path multiplication. |
| Audience | Action |
|---|---|
| Cluster operators (single release) | Roll out collector + graph-writer + graph-query + frontend in the same release. The collector now emits http_path for CLIENT spans; if graph-writer is still on v2.6.0 it will accept the new payload but write it to a single MERGE bucket — the per-path edges only materialise once graph-writer v2.7.0 is in. Indexes are created automatically on first start-up. |
| Cluster operators (existing analyses) | The fix only affects edges written after the upgrade. Old L7_COMMUNICATES_WITH rows in Neo4j retain their pre-fix shape (/ paths, single edge per (src, dst)). To get clean data, re-run the analysis. Optional purge query is below. |
| Cluster operators (Neo4j capacity) | Per-path edges multiply edge count in proportion to per-source endpoint cardinality. A typical microservice with five outbound endpoints will go from ~1 edge to ~5 edges per source. Watch the cardinality observation queries below. |
| API consumers (read-only) | None — response shape is unchanged; request_count totals still aggregate correctly. |
| Frontend snippet/export consumers | Edge CSV now has one row per (method, path). JSON export's data.paths array is the new structured per-path breakdown. |
If you want the cleanest possible Neo4j state after upgrading, drop the
pre-v2.7.0 analyses that still have / paths and a single edge per
(src, dst):
// Identify candidates (read-only):
MATCH ()-[r:L7_COMMUNICATES_WITH]->()
WHERE r.http_path = '/' AND r.analysis_id IN $aids
RETURN r.analysis_id, count(*) AS edge_count
ORDER BY edge_count DESC;
// Purge after operator confirmation:
MATCH (n:L7Workload {analysis_id: $aid})
DETACH DELETE n;The first query lists per-analysis edge counts where every edge has
http_path = '/' — these are the legacy analyses. The second deletes
all nodes (and via DETACH all edges) for a confirmed analysis id.
Replace $aid with the analysis you want to clean.
Real HTTP paths can contain personally identifiable or otherwise
sensitive data (account numbers, user IDs, tokens in path-style API
keys, session ids in legacy services). Before v2.7.0 the fall-through
to / silently mitigated this; in v2.7.0 actual paths are persisted
in Neo4j (L7_COMMUNICATES_WITH.http_path) and ClickHouse (l7_http_flows.http_path).
Mitigations:
- Beyla SERVER-side templating is configurable via
routes.unmatched(low-cardinalitycollapses numeric ids to{:id}). This applies to spans Beyla classifies as SERVER. CLIENT spans (url.full) are NOT templated by Beyla — operators who need that have to filter paths in theflowfish-l7-collector(out of scope for v2.7.0; track as follow-up). - Tighten the
l7_capture_filterper analysis if you want to exclude PII-prone services from L7 collection altogether. The wizard already supports namespace/workload allow-lists. - Treat Neo4j and ClickHouse exports as data-classified per the most sensitive namespace included. L7 tables include enough URL structure to reconstruct API traffic patterns.
The MERGE key change means every distinct (method, path) between two
workloads is now a separate edge. For most services this multiplies
edge count by 3–15x. Use these queries periodically to spot pathological
cases (services with thousands of unique paths usually mean unbounded
ids in path segments).
// Top-20 sources by per-target path variant count:
MATCH (s)-[r:L7_COMMUNICATES_WITH]->(t)
WHERE r.analysis_id = $aid
WITH s, t, count(DISTINCT r.http_path) AS path_variants,
sum(r.request_count) AS total_req
WHERE path_variants > 1
RETURN s.name AS source, t.name AS target,
path_variants, total_req
ORDER BY path_variants DESC
LIMIT 20;
// Sources emitting >50 unique paths to a single target (likely needs
// templating in Beyla routes config):
MATCH (s)-[r:L7_COMMUNICATES_WITH]->(t)
WHERE r.analysis_id = $aid
WITH s, t, count(DISTINCT r.http_path) AS path_variants
WHERE path_variants > 50
RETURN s.name, s.namespace, t.name, t.namespace, path_variants
ORDER BY path_variants DESC;
// Total edge count by analysis — track this before/after the upgrade
// to size Neo4j storage:
MATCH ()-[r:L7_COMMUNICATES_WITH]->()
WHERE r.analysis_id = $aid
RETURN count(*) AS edge_count;When path_variants > 50 for a single source, extend the Beyla
routes.patterns for that service to template the high-cardinality
segment — the routes config feeds the http.route attribute which the
collector picks before url.path, so adding a pattern there collapses
the variants to one templated edge.
After deploying v2.7.0, run the following smoke tests:
- CLIENT span path recovery. Pick a known outgoing HTTP dependency
(e.g. your service → Elasticsearch). In the Service Map drawer for
that source workload, verify the Connections tab shows the real
downstream path (
/_bulk,/_search, …) instead of/. - Per-path edge fan-out. Open the Integration Hub Edges table for
a busy gateway. Verify multiple rows for the same
(source, target, protocol)differing only bymethod/path. No React "duplicate key" console warnings. - Canvas bundling. Open the Service Map canvas for the same
gateway. Verify the high-fan-out dependency renders as ONE edge with
a
N pathslabel suffix, not as N stacked arrows. - CSV / JSON export integrity. Click Export → CSV on the
Service Map. Verify one CSV row per
(method, path). Click Export → JSON — verify each edge has apathsarray. - Dependency count stability. Pull
GET /api/v1/l7/dependencies/summary?analysis_id=...for any analysis. Verifyoutbound_countreflects distinct downstream services, not total edges (a gateway talking to one service over 50 paths must showoutbound_count = 1). - Backward-compat regression. Confirm legacy analyses created
before the upgrade still render — they will continue to show a
single edge with
/per dependency (no data migration is performed automatically).
This release closes the L7 parity gap in the Integration Hub. L7 dependency summaries can now be filtered by annotation, label, owner/workload name and pod name (matching the L4 surface), and the Integration Hub fans BOTH-mode analyses out to L4 and L7 endpoints in parallel.
The release is backwards compatible for all existing API consumers. No data migration is required.
None.
New optional parameters:
| Parameter | Type | Default | Notes |
|---|---|---|---|
annotation_key |
string | — | fnmatch glob supported |
annotation_value |
string | — | fnmatch glob supported |
label_key |
string | — | fnmatch glob supported |
label_value |
string | — | fnmatch glob supported |
owner_name |
string | — | Server-side alias for workload_name; workload_name takes precedence when both are supplied |
pod_name |
string | — | Case-insensitive substring match against L7Workload.name |
workload_name |
string | — | Case-insensitive substring match against L7Workload.name |
filter_noise_annotations |
boolean | false |
Strips infrastructure annotation prefixes (kubectl.kubernetes.io/, kubernetes.io/, openshift.io/) from the response |
Response shape additions:
- Every workload entry now includes
is_matched: boolean.- With no filter active:
is_matched=trueon every entry (no neighbour expansion). - With any filter active: matched workloads are
is_matched=true; their immediate neighbours are returned withis_matched=falseso callers retain dependency context.
- With no filter active:
- The Neo4j query LIMIT is multiplied by 10 internally when a filter is active so post-filtering does not truncate results. Callers can still set their own
limitupstream.
New optional parameter:
| Parameter | Type | Default | Notes |
|---|---|---|---|
workload_name_exact |
boolean | true |
Default preserves existing exact-match behaviour. Set false for case-insensitive substring match (mirrors L4 owner_name semantics). |
analysis_idon both L7 endpoints is now typed asstringto match the multi-cluster sub-analysis prefix already used at runtime.- The
namespaceandinclude_metadataparameters on/l7/dependencies/summaryare now explicitly listed in the spec (the runtime already supported them).
- The dedicated L7 Workload Search card has been removed. The unified Service Identification card now handles every analysis level.
- Namespace is now an orthogonal field — it applies to every identification method (annotation, label, namespace + deployment / workload, pod name, advanced), not just
namespace_deployment. - The
namespace_deploymentmethod label has been renamed to Namespace + Deployment / Workload for clarity across L4 and L7. - BOTH-mode analyses fan out to L4 and L7 endpoints in parallel and render two preview tabs plus an L4/L7 snippet toggle.
- Snippet builders emit the full filter surface (annotation/label/owner_name/pod_name) for L4 and L7 alike. L7 tree snippets now always set
workload_name_exact=false.
| Behaviour | v2.5.0 | v2.6.0 |
|---|---|---|
GET /l7/dependencies/summary with annotation filter |
Filter parameters ignored; full namespace result returned. | Cypher CONTAINS pre-filter + Python fnmatch post-filter; only matched workloads + immediate neighbours returned. |
Integration Hub with analysis_level=both |
L4 endpoint only; L7 data missing in Preview and snippets. | Both endpoints queried in parallel; Network/Application tabs in Preview; L4/L7 toggle in Integration Code. |
| Integration Hub Service Identification card visibility | Hidden for L7 analyses (only the L7 Workload Search card shown). | Always visible. |
tree-summary workload_name semantic |
Exact match only. | Default exact (workload_name_exact=true); opt-in substring (workload_name_exact=false). The Integration Hub explicitly opts in. |
| Audience | Action |
|---|---|
| Cluster operators | None — drop-in upgrade. |
| API consumers (read-only) | None — defaults preserve existing behaviour. |
API consumers using the L7 tree-summary substring trick |
Explicitly pass workload_name_exact=false. |
| Integration Hub snippet copy/paste users | Re-copy snippets after upgrading; new identification parameters are emitted. |
After deploying v2.6.0, run the following smoke tests against any cluster:
- L7 annotation filter parity. Start a BOTH analysis, then in the Integration Hub configure
annotation_key=mycompany.com/project,annotation_value=NBA. Verify the workload count on the Application Dependencies (L7) tab matches what the Service Map renders for the same filter. - Pure L7 with namespace. Start an L7-only analysis, pick Namespace + Deployment / Workload, fill in a namespace + workload name. Verify the Preview lists matched workloads + neighbours and that the Integration Code emits the L7 query string.
- Partial failure. Temporarily scale
graph-querydown (or simulate by blocking/l7/*at the gateway), launch a BOTH-mode query, and verify the L4 tab renders while the L7 tab shows an inline error banner. - Multi-cluster annotation. With two clusters connected, run the annotation filter scenario and verify workloads from both clusters appear with the correct
clusterfield. Frontend deduplication uses the(cluster, namespace, name)tuple — re-confirm there are no duplicates. - Backward compatibility. Call
GET /api/v1/l7/dependencies/tree-summaryfrom any existing pipeline withoutworkload_name_exact; verify the response matches v2.5.0 exact-match behaviour.
Pre-v2.5.0 release notes are not tracked in this file.