Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 12 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,19 @@ func (c *Frontend) GetAggregateKeyLabels() map[string][]string {
"droppedCause": {"PktDropLatestDropCause"},
"dnsRCode": {"DnsFlagsResponseCode"},
}
const tlsVersionAggSuffix = "__TLSVersion"
for i := range c.Scopes {
keyLabels[c.Scopes[i].ID] = c.Scopes[i].Labels
id := c.Scopes[i].ID
labs := c.Scopes[i].Labels
keyLabels[id] = labs
// Same src/dst dimensions as topology scope, plus TLSVersion and TLSTypes (per-link TLS breakdown).
// TLSTypes must be a label for LogQL sum by(...). Loki |json does not extract JSON arrays as labels;
// the topology builder adds a regexp stage for TlsFlows to capture the TLSTypes array from the line.
// For Prometheus metrics, series must expose a scalar TLSTypes label at export time.
withTLS := make([]string, len(labs), len(labs)+2)
copy(withTLS, labs)
withTLS = append(withTLS, "TLSVersion", "TLSTypes")
keyLabels[id+tlsVersionAggSuffix] = withTLS
}
return keyLabels
}
10 changes: 10 additions & 0 deletions pkg/loki/flow_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package loki
import (
"fmt"
"regexp"
"strconv"
"strings"

"github.com/netobserv/network-observability-console-plugin/pkg/config"
Expand Down Expand Up @@ -228,6 +229,15 @@ func (q *FlowQueryBuilder) appendTLSFilter(sb *strings.Builder) {
sb.WriteString(`|="TLSTypes"`)
}

// appendTLSTypesArrayLabelFromLine adds a TLSTypes label from the raw log line.
// Loki's |json stage does not extract JSON array fields as labels, so sum by(TLSTypes,...) would
// otherwise drop the dimension even though flows include "TLSTypes":[...] in JSON.
func (q *FlowQueryBuilder) appendTLSTypesArrayLabelFromLine(sb *strings.Builder) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure to understand why not just using the appendTLSFilter defined above. Did I miss an edge case there?

Copy link
Copy Markdown
Member

@jotak jotak May 19, 2026

Choose a reason for hiding this comment

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

Ah, I guess I get it: it's because you're doing an aggregation per TLS type, right?
I think we can simplify and forget about TLS types here; not only because imho it's not a super important thing to show in aggregated data, but also because I omitted it intentionally in prom metrics (for cardinality)
I think if we omit TLS types in aggregations, that simplifies the whole things quite a bit?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Actually, TLSGroup is much more meaningful to have in aggregateed data. PQC compliance is based on TLSGroup.

const pattern = "\"TLSTypes\"\\s*:\\s*(?P<TLSTypes>\\[[^\\]]*\\])"
sb.WriteString("|regexp ")
sb.WriteString(strconv.Quote(pattern))
}

func (q *FlowQueryBuilder) appendDNSLatencyFilter(sb *strings.Builder) {
// ensure DnsLatencyMs field is specified and value is not zero
// |~`"DnsLatencyMs`!~`DnsLatencyMs%22:0[,}]`
Expand Down
3 changes: 3 additions & 0 deletions pkg/loki/topology_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ func (q *TopologyQueryBuilder) Build() string {
}

q.appendJSON(sb, true)
if q.topology.DataField == constants.MetricTypeTLSFlows {
q.appendTLSTypesArrayLabelFromLine(sb)
}

dataField := q.topology.GetActualDataField()
if len(dataField) > 0 {
Expand Down
44 changes: 34 additions & 10 deletions pkg/loki/topology_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@ var lokiConfig = config.Loki{
}

var aggregateKeyLabels = map[string][]string{
"app": {"app"},
"droppedState": {"PktDropLatestState"},
"droppedCause": {"PktDropLatestDropCause"},
"dnsRCode": {"DnsFlagsResponseCode"},
"cluster": {"K8S_ClusterName"},
"zone": {"SrcK8S_Zone", "DstK8S_Zone"},
"host": {"SrcK8S_HostName", "DstK8S_HostName"},
"namespace": {"SrcK8S_Namespace", "DstK8S_Namespace"},
"owner": {"SrcK8S_OwnerName", "SrcK8S_OwnerType", "DstK8S_OwnerName", "DstK8S_OwnerType", "SrcK8S_Namespace", "DstK8S_Namespace"},
"resource": {"SrcK8S_Name", "SrcK8S_Type", "SrcK8S_OwnerName", "SrcK8S_OwnerType", "SrcK8S_Namespace", "SrcAddr", "SrcK8S_HostName", "DstK8S_Name", "DstK8S_Type", "DstK8S_OwnerName", "DstK8S_OwnerType", "DstK8S_Namespace", "DstAddr", "DstK8S_HostName"},
"app": {"app"},
"droppedState": {"PktDropLatestState"},
"droppedCause": {"PktDropLatestDropCause"},
"dnsRCode": {"DnsFlagsResponseCode"},
"cluster": {"K8S_ClusterName"},
"zone": {"SrcK8S_Zone", "DstK8S_Zone"},
"host": {"SrcK8S_HostName", "DstK8S_HostName"},
"namespace": {"SrcK8S_Namespace", "DstK8S_Namespace"},
"owner": {"SrcK8S_OwnerName", "SrcK8S_OwnerType", "DstK8S_OwnerName", "DstK8S_OwnerType", "SrcK8S_Namespace", "DstK8S_Namespace"},
"owner__TLSVersion": {"SrcK8S_OwnerName", "SrcK8S_OwnerType", "DstK8S_OwnerName", "DstK8S_OwnerType", "SrcK8S_Namespace", "DstK8S_Namespace", "TLSVersion", "TLSTypes"},
"resource": {"SrcK8S_Name", "SrcK8S_Type", "SrcK8S_OwnerName", "SrcK8S_OwnerType", "SrcK8S_Namespace", "SrcAddr", "SrcK8S_HostName", "DstK8S_Name", "DstK8S_Type", "DstK8S_OwnerName", "DstK8S_OwnerType", "DstK8S_Namespace", "DstAddr", "DstK8S_HostName"},
}

func TestBuildTopologyQuery_SimpleAggregate(t *testing.T) {
Expand Down Expand Up @@ -123,3 +124,26 @@ func TestBuildTopologyQuery_CustomLabelAggregate(t *testing.T) {
result,
)
}

func TestBuildTopologyQuery_TlsFlowsOwnerPlusTlsVersionAggregate(t *testing.T) {
in := TopologyInput{
Start: "(start)",
End: "",
Top: "50",
RateInterval: "2m",
Step: "10s",
DataField: constants.MetricTypeTLSFlows,
MetricFunction: constants.MetricFunctionRate,
RecordType: constants.RecordTypeLog,
DataSource: constants.DataSourceAuto,
Aggregate: "owner__TLSVersion",
}
q, err := NewTopologyQuery(&lokiConfig, aggregateKeyLabels, &in)
require.NoError(t, err)
result := q.Build()
assert.Contains(t, result, "sum by(SrcK8S_OwnerName,SrcK8S_OwnerType,DstK8S_OwnerName,DstK8S_OwnerType,SrcK8S_Namespace,DstK8S_Namespace,TLSVersion,TLSTypes)")
assert.Contains(t, result, "|=\"TLSTypes\"")
assert.Contains(t, result, "|json")
assert.Contains(t, result, "|regexp ")
assert.Contains(t, result, "?P<TLSTypes>")
}
13 changes: 13 additions & 0 deletions web/locales/en/plugin__netobserv-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
"Cluster name": "Cluster name",
"UDN": "UDN",
"Can't find metrics for this element. Check your capture filters to ensure we can monitor it. Else it probably means there is no traffic here.": "Can't find metrics for this element. Check your capture filters to ensure we can monitor it. Else it probably means there is no traffic here.",
"TLS": "TLS",
"TLS-encrypted traffic was observed on this link for the selected scope and time range.": "TLS-encrypted traffic was observed on this link for the selected scope and time range.",
"TLS versions": "TLS versions",
"TLS message types": "TLS message types",
"No TLS message type breakdown is available for this aggregation.": "No TLS message type breakdown is available for this aggregation.",
"Endpoint A": "Endpoint A",
"Source": "Source",
"Endpoint B": "Endpoint B",
Expand Down Expand Up @@ -158,6 +163,8 @@
"Show": "Show",
"Edges": "Edges",
"Edges label": "Edges label",
"Marks volume edges as cleartext when aggregated logs show no TLS signals, using an open-lock icon. Can add many icons; use for focused troubleshooting.": "Marks volume edges as cleartext when aggregated logs show no TLS signals, using an open-lock icon. Can add many icons; use for focused troubleshooting.",
"Cleartext traffic": "Cleartext traffic",
"Badges": "Badges",
"Empty": "Empty",
"Collapse groups": "Collapse groups",
Expand Down Expand Up @@ -545,6 +552,12 @@
"Show NoError responses grouped in a separate series": "Show NoError responses grouped in a separate series",
"Show NoError": "Show NoError",
"Export panel": "Export panel",
"No TLS signals in aggregated flow logs for this link (cleartext or not classified as TLS).": "No TLS signals in aggregated flow logs for this link (cleartext or not classified as TLS).",
"Observed TLS uses deprecated protocol versions (e.g. TLS 1.0 / 1.1 or SSL).": "Observed TLS uses deprecated protocol versions (e.g. TLS 1.0 / 1.1 or SSL).",
"Observed TLS includes TLS 1.2 (older but still common). Prefer TLS 1.3 where possible.": "Observed TLS includes TLS 1.2 (older but still common). Prefer TLS 1.3 where possible.",
"Observed TLS includes TLS 1.3 (current recommended).": "Observed TLS includes TLS 1.3 (current recommended).",
"Observed TLS on this link; protocol version could not be classified from labels.": "Observed TLS on this link; protocol version could not be classified from labels.",
"Observed TLS on this link.": "Observed TLS on this link.",
"Step into this {{name}}": "Step into this {{name}}",
"Filter by source or destination {{name}}": "Filter by source or destination {{name}}",
"Filter by {{name}}": "Filter by {{name}}",
Expand Down
2 changes: 2 additions & 0 deletions web/src/api/ipfix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ export interface Flow {
_IsFirst?: string;
numFlowLogs?: number;
UdnId?: string;
TLSTypes?: string[];
TLSVersion?: string;
}

export enum FlowDirection {
Expand Down
9 changes: 9 additions & 0 deletions web/src/api/loki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,18 @@ export interface TopologyMetricPeer {
subnetLabel?: string;
}

/** TLS message types and protocol versions from Loki matrix metric labels (TLSTypes, TLSVersion), when present. */
export type GenericMetricTls = {
types?: string[];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

types can be removed I guess?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Removed in 3ab6360

I initially kept that in the UI just in case but it seems we are going to a direction where it will never be the case.
We can reintroduce it if needed in the future (maybe if we introduce a side panel for overview metrics ?)

versions?: string[];
};

export type GenericMetric = {
name: string;
values: [number, number][];
stats: MetricStats;
aggregateBy: Field;
tls?: GenericMetricTls;
};

export type FunctionMetrics = {
Expand Down Expand Up @@ -198,6 +205,8 @@ export type TopologyMetrics = {
values: [number, number][];
stats: MetricStats;
scope: FlowScope;
/** TLSTypes / TLSVersion from Loki topology matrix labels when present. */
tls?: GenericMetricTls;
};

export type NamedMetric = TopologyMetrics & {
Expand Down
32 changes: 32 additions & 0 deletions web/src/components/drawer/element/__tests__/element-panel.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,38 @@ describe('<ElementPanel />', () => {
expect(container.querySelector('#destination-content')?.textContent).toContain('172.30.0.10');
});

it('should show TLS block on edge when TLS data is present', async () => {
const edge = getEdge();
edge.setData({ tagTlsSecure: true, tlsTypeLabels: ['ClientHello', 'AppData'] });
const { container } = render(<ElementPanelContent {...mocks} element={edge} />);
await waitFor(() => {
expect(container.querySelector('#edge-tls-info')).toBeTruthy();
});
expect(container.querySelector('#edge-tls-info')?.textContent).toContain('ClientHello');
expect(container.querySelector('#edge-tls-info')?.textContent).toContain('AppData');
});

it('should show TLS block on edge when only tlsTypeLabels are set', async () => {
const edge = getEdge();
edge.setData({ tlsTypeLabels: ['ClientHello', 'AppData'] });
const { container } = render(<ElementPanelContent {...mocks} element={edge} />);
await waitFor(() => {
expect(container.querySelector('#edge-tls-info')).toBeTruthy();
});
expect(container.querySelector('#edge-tls-info')?.textContent).toContain('ClientHello');
});

it('should list TLS versions on edge when tlsVersionLabels are set', async () => {
const edge = getEdge();
edge.setData({ tagTlsSecure: true, tlsVersionLabels: ['TLS 1.3', 'TLS 1.2'] });
const { container } = render(<ElementPanelContent {...mocks} element={edge} />);
await waitFor(() => {
expect(container.querySelector('#edge-tls-info')).toBeTruthy();
});
expect(container.querySelector('#edge-tls-info')?.textContent).toContain('TLS 1.3');
expect(container.querySelector('#edge-tls-info')?.textContent).toContain('TLS 1.2');
});

it('should render node metrics', async () => {
const { container } = render(
<ElementPanelMetrics
Expand Down
120 changes: 118 additions & 2 deletions web/src/components/drawer/element/element-panel-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,26 @@ import { FilterIcon, TimesIcon } from '@patternfly/react-icons';
import { BaseEdge, BaseNode } from '@patternfly/react-topology';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { Filter, FilterDefinition, Filters } from '../../../model/filters';
import { GraphElementPeer, isElementFiltered, NodeData, toggleElementFilter } from '../../../model/topology';
import {
createFilterValue,
doesIncludeFilter,
Filter,
FilterCompare,
FilterDefinition,
Filters,
type FilterId
} from '../../../model/filters';
import {
EdgeTlsPanelData,
GraphElementPeer,
isElementFiltered,
NodeData,
toggleElementFilter,
toggleQuickFilterValue
} from '../../../model/topology';
import { findFilter } from '../../../utils/filter-definitions';
import { createPeer } from '../../../utils/metrics';
import { TlsVersionLockIcon } from '../../icons/tls-lock-icons';
import { ElementFields } from './element-fields';

export interface ElementPanelContentProps {
Expand Down Expand Up @@ -121,6 +138,103 @@ export const ElementPanelContent: React.FC<ElementPanelContentProps> = ({
[t]
);

const edgeTlsInfo = React.useCallback(
(d: EdgeTlsPanelData | undefined) => {
const fromEdge =
Boolean(d?.tagTlsSecure) || Boolean(d?.tlsTypeLabels?.length) || Boolean(d?.tlsVersionLabels?.length);
const panel = fromEdge ? d : undefined;
const hasPanel =
Boolean(panel?.tagTlsSecure) ||
Boolean(panel?.tlsTypeLabels?.length) ||
Boolean(panel?.tlsVersionLabels?.length);
if (!panel || !hasPanel) {
return <></>;
}
const { tagTlsSecure, tlsTypeLabels, tlsVersionLabels } = panel;

const renderTlsQuickFilter = (filterId: FilterId, value: string, buttonId: string) => {
const def = findFilter(filterDefinitions, filterId);
if (!def) {
return null;
}
const filterKey = { def, compare: FilterCompare.equal };
const filterValue = createFilterValue(def, value);
const filterValues = [filterValue];
const isFiltered = doesIncludeFilter(filters.list, filterKey, filterValues);
return (
<Button
id={buttonId}
variant="plain"
className="overflow-button"
icon={isFiltered ? <TimesIcon /> : <FilterIcon />}
onClick={() => toggleQuickFilterValue(def, value, isFiltered, filters.list, setFilters)}
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
};

return (
<Content id="edge-tls-info" className="record-field-container">
<Content component={ContentVariants.h4}>{t('TLS')}</Content>
{tagTlsSecure ? (
<Flex>
<FlexItem flex={{ default: 'flex_1' }}>
<Content component={ContentVariants.p}>
{t('TLS-encrypted traffic was observed on this link for the selected scope and time range.')}
</Content>
</FlexItem>
</Flex>
) : null}
{tlsVersionLabels && tlsVersionLabels.length > 0 ? (
<>
<Content component={ContentVariants.h4}>{t('TLS versions')}</Content>
<Flex direction={{ default: 'column' }} gap={{ default: 'gapSm' }}>
{tlsVersionLabels.map((label, i) => {
const versionFilterBtn = renderTlsQuickFilter('tls_version', label, `edge-tls-version-filter-${i}`);
return (
<Flex key={`${label}-${i}`} alignItems={{ default: 'alignItemsCenter' }} gap={{ default: 'gapSm' }}>
<TlsVersionLockIcon versionLabel={label} />
<FlexItem flex={{ default: 'flex_1' }}>
<Content component={ContentVariants.p}>{label}</Content>
</FlexItem>
{versionFilterBtn ? <FlexItem>{versionFilterBtn}</FlexItem> : null}
</Flex>
);
})}
</Flex>
</>
) : null}
{tlsTypeLabels && tlsTypeLabels.length > 0 ? (
<>
<Content component={ContentVariants.h4}>{t('TLS message types')}</Content>
<Flex direction={{ default: 'column' }} gap={{ default: 'gapSm' }}>
{tlsTypeLabels.map((label, i) => {
const typeFilterBtn = renderTlsQuickFilter('tls_types', label, `edge-tls-type-filter-${i}`);
return (
<Flex key={`${label}-${i}`} alignItems={{ default: 'alignItemsCenter' }} gap={{ default: 'gapSm' }}>
<FlexItem flex={{ default: 'flex_1' }}>
<Content component={ContentVariants.p}>{label}</Content>
</FlexItem>
{typeFilterBtn ? <FlexItem>{typeFilterBtn}</FlexItem> : null}
</Flex>
);
})}
</Flex>
</>
) : tagTlsSecure && !(tlsVersionLabels && tlsVersionLabels.length > 0) ? (
<Flex>
<FlexItem flex={{ default: 'flex_1' }}>
<Content component={ContentVariants.small}>
{t('No TLS message type breakdown is available for this aggregation.')}
</Content>
</FlexItem>
</Flex>
) : null}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
</Content>
);
},
[filterDefinitions, filters, setFilters, t]
);

if (element instanceof BaseNode && data) {
return (
<>
Expand All @@ -141,6 +255,7 @@ export const ElementPanelContent: React.FC<ElementPanelContentProps> = ({
// Edge A to B (prefering neutral naming here as there is no assumption about what is source, what is destination
const aData: NodeData = element.getSource().getData();
const bData: NodeData = element.getTarget().getData();
const edgeData = element.getData();
const combinedData = Object.assign({}, aData, bData);
return (
<>
Expand Down Expand Up @@ -189,6 +304,7 @@ export const ElementPanelContent: React.FC<ElementPanelContentProps> = ({
</AccordionItem>
</div>
</Accordion>
{edgeTlsInfo(edgeData)}
</>
);
}
Expand Down
1 change: 1 addition & 0 deletions web/src/components/drawer/element/element-panel.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#pf-tab-section-details-drawer-tabs,
#pf-tab-section-metrics-drawer-tabs,
#pf-tab-section-dropped-drawer-tabs,
#pf-tab-section-tls-drawer-tabs,
#pf-tab-section-health-drawer-tabs {
overflow-x: hidden !important;
overflow-y: auto !important;
Expand Down
Loading
Loading