Skip to content
172 changes: 172 additions & 0 deletions calico-enterprise/_includes/components/AggregationKeyDemo/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import React, { useState, useMemo, useEffect, useRef } from 'react';
import styles from './styles.module.css';

const cx = (...names) => names.filter(Boolean).join(' ');

// HTTP-only sample so every aggregation toggle maps to a real FelixConfiguration
// field, keeping the rendered configuration block accurate.
const SAMPLE = [
{ src: 'frontend', dest: 'cart', method: 'GET', url: '/cart', code: '200' },
{ src: 'frontend', dest: 'cart', method: 'GET', url: '/cart', code: '404' },
{ src: 'frontend', dest: 'cart', method: 'GET', url: '/items', code: '200' },
{ src: 'frontend', dest: 'cart', method: 'POST', url: '/cart', code: '200' },
{ src: 'orders', dest: 'pay', method: 'POST', url: '/charge', code: '200' },
{ src: 'edge', dest: 'pay', method: 'POST', url: '/charge', code: '200' },
{ src: 'orders', dest: 'canary', method: 'POST', url: '/charge', code: '200' },
];

// Each aggregator toggles one field in the aggregation key and maps to a real
// FelixConfiguration field with its include/exclude values.
const AGGREGATORS = [
{ id: 'source', label: 'Source info', field: 'src', yaml: 'l7LogsFileAggregationSourceInfo', inc: 'IncludeL7SourceInfo', exc: 'ExcludeL7SourceInfo' },
{ id: 'dest', label: 'Destination info', field: 'dest', yaml: 'l7LogsFileAggregationDestinationInfo', inc: 'IncludeL7DestinationInfo', exc: 'ExcludeL7DestinationInfo' },
{ id: 'method', label: 'HTTP method', field: 'method', yaml: 'l7LogsFileAggregationHTTPMethod', inc: 'IncludeL7HTTPMethod', exc: 'ExcludeL7HTTPMethod' },
{ id: 'url', label: 'URL', field: 'url', yaml: 'l7LogsFileAggregationTrimURL', inc: 'IncludeL7FullURL', exc: 'ExcludeL7URL' },
{ id: 'code', label: 'Response code', field: 'code', yaml: 'l7LogsFileAggregationResponseCode', inc: 'IncludeL7ResponseCode', exc: 'ExcludeL7ResponseCode' },
];

// Maps each raw field to the log key it surfaces under, in log output order.
const LOG_KEYS = [
{ id: 'source', field: 'src', key: 'src_name_aggr' },
{ id: 'dest', field: 'dest', key: 'dest_name_aggr' },
{ id: 'method', field: 'method', key: 'method' },
{ id: 'url', field: 'url', key: 'url' },
{ id: 'code', field: 'code', key: 'response_code' },
];

const RAW_COLUMNS = LOG_KEYS.map((k) => ({ field: k.field, label: k.key }));
const STATIC_CONFIG = [
'apiVersion: projectcalico.org/v3',
'kind: FelixConfiguration',
'metadata:',
' name: default',
'spec:',
' l7LogsFileEnabled: true',
];
const HIGHLIGHT_MS = 1500;

export default function AggregationKeyDemo() {
const [checked, setChecked] = useState(() =>
Object.fromEntries(AGGREGATORS.map((a) => [a.id, true])),
);

const toggle = (id) => setChecked((c) => ({ ...c, [id]: !c[id] }));

// Aggregate the raw requests into buckets keyed by the included fields.
const logEntries = useMemo(() => {
const order = [];
const buckets = new Map();
for (const row of SAMPLE) {
const keyParts = AGGREGATORS
.filter((a) => checked[a.id])
.map((a) => `${a.field}:${row[a.field]}`);
const k = keyParts.join('|');
if (!buckets.has(k)) {
const entry = { count: 0 };
for (const lk of LOG_KEYS) {
entry[lk.key] = checked[lk.id] ? row[lk.field] : '-';
}
buckets.set(k, entry);
order.push(k);
}
buckets.get(k).count += 1;
}
return order.map((k) => buckets.get(k));
}, [checked]);

const logText = useMemo(
() => logEntries.map((e) => JSON.stringify(e)).join('\n'),
[logEntries],
);

// Highlight the config lines whose include/exclude value just changed.
const prevChecked = useRef(checked);
const [highlighted, setHighlighted] = useState(() => new Set());
useEffect(() => {
const changed = new Set(
AGGREGATORS.map((a) => a.id).filter((id) => checked[id] !== prevChecked.current[id]),
);
prevChecked.current = checked;
if (changed.size === 0) return;
setHighlighted(changed);
const t = setTimeout(() => setHighlighted(new Set()), HIGHLIGHT_MS);
return () => clearTimeout(t);
}, [checked]);

return (
<div>
<p className={styles.sectionTitle}>1. Observed traffic ({SAMPLE.length} requests)</p>
<p className={styles.sectionHint}>
Consider these {SAMPLE.length} requests as the raw L7 events captured
between workloads in your cluster — one row per request, exactly as seen
on the wire before any aggregation is applied. This is the input that the
aggregation key collapses into log entries.
</p>
<div className={styles.tableWrap}>
<table className={styles.table}>
<thead>
<tr>
{RAW_COLUMNS.map((c) => (
<th key={c.field} className={styles.header}>{c.label}</th>
))}
</tr>
</thead>
<tbody>
{SAMPLE.map((row, i) => (
<tr key={i}>
{RAW_COLUMNS.map((c) => (
<td key={c.field} className={styles.cell}>{row[c.field]}</td>
))}
</tr>
))}
</tbody>
</table>
</div>

<p className={styles.sectionTitle}>2. Aggregation key fields</p>
<p className={styles.sectionHint}>
Include or exclude each field from the aggregation key.
</p>
<div className={styles.controls}>
{AGGREGATORS.map((a) => (
<label key={a.id} className={styles.controlLabel}>
<input
type="checkbox"
checked={checked[a.id]}
onChange={() => toggle(a.id)}
/>
<span>{a.label}</span>
</label>
))}
</div>

<p className={styles.sectionTitle}>3. Configuration</p>
<p className={styles.sectionHint}>
The <code>FelixConfiguration</code> produced by the selected fields.
</p>
<pre className={styles.code}>
{STATIC_CONFIG.map((line) => (
<div key={line} className={styles.codeLine}>{line}</div>
))}
{AGGREGATORS.map((a) => (
<div
key={a.id}
className={cx(styles.codeLine, highlighted.has(a.id) && styles.codeLineHi)}
>
{` ${a.yaml}: ${checked[a.id] ? a.inc : a.exc}`}
</div>
))}
</pre>

<p className={styles.sectionTitle}>
4. Resulting log ({logEntries.length} {logEntries.length === 1 ? 'entry' : 'entries'})
</p>
<p className={styles.sectionHint}>
What is written to the log file: one JSON entry per unique combination of
the included fields, with <code>count</code> requests merged into it.
Excluded fields collapse to <code>"-"</code>.
</p>
<pre className={styles.code}>{logText}</pre>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 16px;
margin: 12px 0;
padding: 12px;
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 4px;
}

.controlsTitle {
font-weight: 600;
}

.controlLabel {
display: inline-flex;
align-items: center;
gap: 4px;
cursor: pointer;
}

.sectionTitle {
margin: 20px 0 2px;
font-weight: 600;
}

.sectionHint {
margin: 0 0 8px;
font-size: 0.85em;
opacity: 0.8;
}

.tableWrap {
overflow-x: auto;
}

.table {
width: 100%;
}

.cell {
padding: 4px 8px;
font-family: var(--ifm-font-family-monospace);
font-size: 0.85em;
overflow-wrap: anywhere;
vertical-align: top;
}

.header {
padding: 4px 8px;
font-family: var(--ifm-font-family-monospace);
font-size: 0.85em;
white-space: normal;
word-break: break-word;
}

.code {
overflow-x: auto;
white-space: pre;
font-size: 0.85em;
}

.codeLine {
display: block;
border-radius: 2px;
transition: background-color 1s ease;
}

.codeLineHi {
background-color: var(--ifm-color-success-contrast-background);
transition: background-color 0s;
}
2 changes: 1 addition & 1 deletion calico-enterprise/observability/dashboards.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Seeing this data helps you spot unusual flow activity, which may indicate a comp

The **HTTP Traffic** dashboard provides application performance metrics for in-scope Kubernetes services.
The data can assist service owners and platform personnel in assessing the health of cluster workloads without the need for a full service mesh.
[L7 logs](elastic/l7/configure.mdx) are not enabled by default, and must be configured.
[L7 logs](elastic/l7/overview.mdx) are not enabled by default, and must be configured.

<Screenshot src="/img/calico-enterprise/dashboard-l7.png" alt="HTTP Traffic dashboard" />

Expand Down
26 changes: 26 additions & 0 deletions calico-enterprise/observability/elastic/aggregation.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
description: Understand how Calico Enterprise aggregates log entries by selecting which fields participate in the aggregation key.
---

import AggregationKeyDemo from '../../_includes/components/AggregationKeyDemo';

# Log aggregation

## Big picture

$[prodname] aggregates log entries before writing them to disk. Rather than emitting one record per event, it merges events that share the same values across a chosen set of fields — the *aggregation key* — into a single entry that carries a `count` of how many events were merged.

Aggregation trades detail for volume:

- **Including a field** makes the log more granular: more entries, fewer events per entry.
- **Excluding a field** collapses events that differ only in that field into one entry and increments its `count` for each merged event.

The same mechanism applies across $[prodname] log types (flow, DNS, L7); only the available fields differ. The example below uses L7 (HTTP) traffic to make the behavior concrete.

## Aggregation playground

<AggregationKeyDemo />

## Aggregation fields

The fields that control the aggregation key, including their accepted values and defaults, are documented in the [Felix Configuration reference](../../reference/component-resources/node/felix/configuration.mdx).
Loading