Skip to content
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';

import Admonition from '@theme/Admonition';
import CodeBlock from '@theme/CodeBlock';
import Link from '@docusaurus/Link';

import { baseUrl } from '../../variables';

export default function EnvironmentFile(props) {
return (
<>
<p>
{props.install === 'container' ? (
<span>
Use the following guidelines and sample file to define the environment variables for starting Calico on the host.
For more help, see the <Link href={`${baseUrl}/reference/component-resources/node/configuration`}>{props.nodecontainer} configuration reference</Link>
</span>
) : (
<span>
Use the following guidelines and sample file to define the environment variables for starting Calico on the host.
</span>
)}
</p>
<p>For the Kubernetes datastore set the following:</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Configuration guidance</th>
</tr>
</thead>
<tbody>
<tr>
<td>KUBECONFIG</td>
<td>Path to kubeconfig file to access the Kubernetes API Server</td>
</tr>
</tbody>
</table>
{props.install === 'container' && (
<Admonition type='note'>
If using certificates and keys, you will need to volume mount them into the container at the location
specified by the paths mentioned above.
</Admonition>
)}
<p>
Sample <code>EnvironmentFile</code> - save to <code>/etc/calico/calico.env</code>
</p>
<CodeBlock language='bash'>
{`DATASTORE_TYPE=kubernetes
CALICO_NODENAME=""
NO_DEFAULT_POOLS="true"
CALICO_IP=""
CALICO_IP6=""
CALICO_AS=""
CALICO_NETWORKING_BACKEND=bird`}
</CodeBlock>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react';
import sanitizeHtml from 'sanitize-html';
import styles from './styles.module.css';

const getSanitizedData = ({ fieldData }) => ({
sanitizedDescription: { __html: sanitizeHtml(fieldData.DescriptionHTML) },
sanitizedSchema: { __html: sanitizeHtml(fieldData.StringSchemaHTML) },
});

const TableConfig = ({ fieldData }) => {
const { sanitizedDescription, sanitizedSchema } = getSanitizedData({ fieldData });

return (
<table className={styles.felixTable}>
<thead>
<tr>
<th>Attribute</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Key</td>
<td>
<code>{fieldData.NameConfigFile || 'No Default Value'}</code>
</td>
</tr>
<tr>
<td>Description</td>
<td dangerouslySetInnerHTML={sanitizedDescription} />
</tr>
<tr>
<td>Schema</td>
<td dangerouslySetInnerHTML={sanitizedSchema} />
</tr>
<tr>
<td>Default</td>
<td>
{fieldData.StringDefault === '' ? (
'none'
) : fieldData.GoType === '*v1.Duration' ? (
<>
<code>{fieldData.StringDefault}</code> ({fieldData.ParsedDefault})
</>
) : (
<code>{fieldData.StringDefault}</code>
)}
</td>
</tr>
</tbody>
</table>
);
};

export default TableConfig;
Loading
Loading