+ );
+}
diff --git a/calico-enterprise/_includes/components/L7AggregationKeyDemo/styles.module.css b/calico-enterprise/_includes/components/L7AggregationKeyDemo/styles.module.css
new file mode 100644
index 0000000000..f3c7bb959e
--- /dev/null
+++ b/calico-enterprise/_includes/components/L7AggregationKeyDemo/styles.module.css
@@ -0,0 +1,118 @@
+.controls {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px 16px;
+ margin: 12px 0;
+ padding: 12px;
+ border: 1px solid var(--ifm-color-emphasis-300);
+ border-radius: 4px;
+}
+
+.controlLabel {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ cursor: pointer;
+}
+
+.legend {
+ display: flex;
+ gap: 16px;
+ margin: 8px 0;
+ font-size: 0.85em;
+ opacity: 0.8;
+}
+
+.legendItem {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.legendSwatch {
+ display: inline-block;
+ width: 0.9em;
+ height: 0.9em;
+ border: 1px solid var(--ifm-color-emphasis-300);
+ border-radius: 2px;
+}
+
+.legendSwatchAdd {
+ background-color: var(--ifm-color-success-contrast-background);
+}
+
+.legendSwatchRemove {
+ background-color: var(--ifm-color-danger-contrast-background);
+}
+
+.tableWrap {
+ overflow-x: auto;
+}
+
+.table {
+ table-layout: fixed;
+ width: 100%;
+}
+
+.row {
+ background-color: transparent;
+ transition: opacity 0.4s ease;
+}
+
+.cell {
+ padding: 4px 8px;
+ font-family: var(--ifm-font-family-monospace);
+ font-size: 0.85em;
+ overflow-wrap: anywhere;
+ vertical-align: top;
+ transition: background-color 1s ease;
+ height: 3.2em;
+ box-sizing: border-box;
+ background-color: transparent;
+}
+
+.header {
+ white-space: normal;
+ word-break: break-word;
+ transition: none;
+}
+
+.cellAdd {
+ background-color: var(--ifm-color-success-contrast-background);
+}
+
+.cellRemove {
+ background-color: var(--ifm-color-danger-contrast-background);
+}
+
+.bucketStart {
+ border-top: 2px solid var(--ifm-color-emphasis-300);
+}
+
+.expandBtn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 1.5em;
+ height: 1.5em;
+ margin-right: 6px;
+ padding: 0;
+ background: var(--ifm-color-emphasis-100);
+ border: 1px solid var(--ifm-color-emphasis-400);
+ border-radius: 3px;
+ cursor: pointer;
+ color: var(--ifm-color-emphasis-800);
+ font-family: inherit;
+ font-size: 1em;
+ font-weight: 700;
+ line-height: 1;
+}
+
+.expandBtn:hover {
+ background: var(--ifm-color-emphasis-200);
+}
+
+.expandBtn:focus-visible {
+ outline: 2px solid var(--ifm-color-primary);
+ outline-offset: 1px;
+}
diff --git a/calico-enterprise/observability/dashboards.mdx b/calico-enterprise/observability/dashboards.mdx
index e2ef910e64..363ea1f048 100644
--- a/calico-enterprise/observability/dashboards.mdx
+++ b/calico-enterprise/observability/dashboards.mdx
@@ -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.
diff --git a/calico-enterprise/observability/elastic/l7/aggregation.mdx b/calico-enterprise/observability/elastic/l7/aggregation.mdx
new file mode 100644
index 0000000000..8a857fc325
--- /dev/null
+++ b/calico-enterprise/observability/elastic/l7/aggregation.mdx
@@ -0,0 +1,23 @@
+---
+description: Control how Calico Enterprise aggregates L7 log entries by selecting which fields participate in the aggregation key.
+---
+
+import L7AggregationKeyDemo from '../../../_includes/components/L7AggregationKeyDemo';
+
+# L7 log aggregation
+
+## Big picture
+
+$[prodname] aggregates L7 events before writing them to disk. Each row in the resulting log represents a unique combination of metadata fields and carries a `count` of how many requests matched that combination during the aggregation interval. Which fields participate in that combination — the aggregation key — is controlled by a set of fields on [Felix Configuration](../../../reference/component-resources/node/felix/configuration.mdx#l7-logs).
+
+Including a field makes the logs more granular (more rows, fewer requests per row). Excluding a field collapses requests that differ only in that field into a single row and increments its `count` for each merged request.
+
+## Aggregation playground
+
+The table below shows a small set of sample L7 events. Each visible row is what would be written to the L7 log, with `count` equal to the number of requests merged into it. Toggle the checkboxes to include or exclude each field from the aggregation key.
+
+
+
+## Aggregation fields
+
+The `FelixConfiguration` fields that control the L7 log aggregation key, including their accepted values and default settings, are documented in the [Felix Configuration reference](../../../reference/component-resources/node/felix/configuration.mdx#l7-logs).
diff --git a/calico-enterprise/observability/elastic/l7/datatypes.mdx b/calico-enterprise/observability/elastic/l7/datatypes.mdx
index 478adeeb37..750a460e69 100644
--- a/calico-enterprise/observability/elastic/l7/datatypes.mdx
+++ b/calico-enterprise/observability/elastic/l7/datatypes.mdx
@@ -1,36 +1,47 @@
---
-description: Reference of key/value fields that Calico Enterprise sends to Elasticsearch for L7 logs, including durations, byte counts, and HTTP request metadata.
+description: Reference of key/value fields that Calico Enterprise sends to Elasticsearch for L7 logs, with the collector that populates each field.
---
# L7 log data types
## Big picture
-$[prodname] sends the following data to Elasticsearch.
+$[prodname] supports multiple L7 collectors that write into the same Elasticsearch index:
-The following table details the key/value pairs in the JSON blob, including their [Elasticsearch datatype](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html).
+- **Envoy** — the legacy sidecar/proxy-based collector. See [Enable Envoy collector](configure.mdx).
+- **eBPF** — kernel TCP-layer probes capturing HTTP request/response and TLS handshake metadata. See [Enable eBPF collector](enable-ebpf.mdx).
+{/* TODO: re-add the Waypoint collector bullet when the Waypoint PR lands. */}
-| Name | Datatype | Description |
-| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `host` | keyword | Name of the node that collected the L7 log entry. |
-| `start_time` | date | Start time of log collection in Unix timestamp format. |
-| `end_time` | date | End time of log collection in Unix timestamp format. |
-| `bytes_in` | long | Number of incoming bytes since the last export. |
-| `bytes_out` | long | Number of outgoing bytes since the last export. |
-| `duration_mean` | long | Mean duration time of all the requests that match this combination of L7 data in nanoseconds. |
-| `duration_max` | long | Max duration time of all the requests that match this combination of L7 data in nanoseconds. |
-| `count` | long | Number of requests that match this combination of L7 data. |
-| `src_name_aggr` | keyword | Contains one of the following values: - Aggregated name of the source pod. - `pvt`: endpoint is not a pod. Its IP address belongs to a private subnet. - `pub`: endpoint is not a pod. Its IP address does not belong to a private subnet. It is probably an endpoint on the public internet. |
-| `src_namespace` | keyword | Namespace of the source endpoint. |
-| `src_type` | keyword | Source endpoint type. Possible values: - `wep`: A workload endpoint, a pod in Kubernetes. - `ns`: A network set. If multiple match, priority is given to NetworkSets in the workload’s own namespace, then to GlobalNetworkSets, and then to NetworkSets in other namespaces. For ties between matching network sets within each category, the one with the longest-prefix match is chosen. Remaining ties are resolved alphabetically by the NetworkSet’s full identity (using namespace/name or just name). - `net`: A Network. The IP address did not fall into a known endpoint type. |
-| `dest_name_aggr` | keyword | Contains one of the following values: - Aggregated name of the destination pod. - `pvt`: endpoint is not a pod. Its IP address belongs to a private subnet. - `pub`: endpoint is not a pod. Its IP address does not belong to a private subnet. It is probably an endpoint on the public internet. |
-| `dest_namespace` | keyword | Namespace of the destination endpoint. |
-| `dest_type` | keyword | Destination endpoint type. Possible values: - `wep`: A workload endpoint, a pod in Kubernetes. - `ns`: A network set. If multiple match, priority is given to NetworkSets in the workload’s own namespace, then to GlobalNetworkSets, and then to NetworkSets in other namespaces. For ties between matching network sets within each category, CIDR matches outrank domain matches and longest-prefix wins between competing CIDR matches. Remaining ties are resolved alphabetically by the NetworkSet’s full identity (using namespace/name or just name). - `net`: A Network. The IP address did not fall into a known endpoint type. |
-| `dest_service_name` | keyword | Name of the destination service. This may be empty if the request was not made against a service. |
-| `dest_service_namespace` | keyword | Namespace of the destination service. This may be empty if the request was not made against a service. |
-| `dest_service_port` | long | Destination service port. |
-| `url` | keyword | URL that the request was made against. |
-| `response_code` | keyword | Response code returned by the request. |
-| `method` | keyword | HTTP method for the request. |
-| `user_agent` | keyword | User agent of the request. |
-| `type` | keyword | Type of request made. Possible values include `tcp`, `tls`, and `html/`. |
+The table below details the key/value pairs in the JSON blob, including their [Elasticsearch datatype](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html) and which collector(s) populate each field. "All" means every collector populates the field. Descriptions prefixed with _HTTP only_ or _TLS only_ mark fields that are populated for events of only that protocol family.
+
+| Name | Datatype | Populated by | Description |
+| ------------------------ | -------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `host` | keyword | All | Name of the node that collected the L7 log entry. |
+| `start_time` | date | All | Start time of log collection in Unix timestamp format. |
+| `end_time` | date | All | End time of log collection in Unix timestamp format. |
+| `bytes_in` | long | All | Number of incoming bytes since the last export. |
+| `bytes_out` | long | All | Number of outgoing bytes since the last export. |
+| `duration_mean` | long | All | Mean duration time of all the requests that match this combination of L7 data in nanoseconds. |
+| `duration_max` | long | All | Max duration time of all the requests that match this combination of L7 data in nanoseconds. |
+| `count` | long | All | Number of requests that match this combination of L7 data. |
+| `src_name_aggr` | keyword | All | Contains one of the following values: - Aggregated name of the source pod. - `pvt`: endpoint is not a pod. Its IP address belongs to a private subnet. - `pub`: endpoint is not a pod. Its IP address does not belong to a private subnet. It is probably an endpoint on the public internet. |
+| `src_namespace` | keyword | All | Namespace of the source endpoint. |
+| `src_type` | keyword | All | Source endpoint type. Possible values: - `wep`: A workload endpoint, a pod in Kubernetes. - `ns`: A network set. If multiple match, priority is given to NetworkSets in the workload's own namespace, then to GlobalNetworkSets, and then to NetworkSets in other namespaces. For ties between matching network sets within each category, the one with the longest-prefix match is chosen. Remaining ties are resolved alphabetically by the NetworkSet's full identity (using namespace/name or just name). - `net`: A Network. The IP address did not fall into a known endpoint type. |
+| `dest_name_aggr` | keyword | All | Contains one of the following values: - Aggregated name of the destination pod. - `pvt`: endpoint is not a pod. Its IP address belongs to a private subnet. - `pub`: endpoint is not a pod. Its IP address does not belong to a private subnet. It is probably an endpoint on the public internet. |
+| `dest_namespace` | keyword | All | Namespace of the destination endpoint. |
+| `dest_type` | keyword | All | Destination endpoint type. Possible values: - `wep`: A workload endpoint, a pod in Kubernetes. - `ns`: A network set. If multiple match, priority is given to NetworkSets in the workload's own namespace, then to GlobalNetworkSets, and then to NetworkSets in other namespaces. For ties between matching network sets within each category, CIDR matches outrank domain matches and longest-prefix wins between competing CIDR matches. Remaining ties are resolved alphabetically by the NetworkSet's full identity (using namespace/name or just name). - `net`: A Network. The IP address did not fall into a known endpoint type. |
+| `dest_service_name` | keyword | All | Name of the destination service. This may be empty if the request was not made against a service. |
+| `dest_service_namespace` | keyword | All | Namespace of the destination service. This may be empty if the request was not made against a service. |
+| `dest_service_port` | long | All | Destination service port. |
+| `url` | keyword | All | _HTTP only._ URL that the request was made against. |
+| `response_code` | keyword | All | _HTTP only._ Response code returned by the request. |
+| `method` | keyword | All | _HTTP only._ HTTP method for the request. |
+| `user_agent` | keyword | All | _HTTP only._ User agent of the request. |
+| `type` | keyword | All | Type of request made. Possible values include `tcp`, `tls`, and `html/`. |
+| `protocol` | keyword | eBPF | Wire-protocol family of the captured event. Possible values: `http`, `tls`. |
+| `protocol_version` | keyword | eBPF | Wire-protocol version. For HTTP events, possible values include `1.0` and `1.1`. |
+| `collector_type` | keyword | eBPF | Category of the collector that produced the event. For the kernel TCP-layer eBPF collector, the value is `ebpf`. |
+| `collector_name` | keyword | eBPF | Name of the specific collector that produced the event. For the kernel TCP-layer eBPF collector, the value is `ebpf-tcp`. |
+| `tls_sni` | keyword | eBPF | _TLS only._ TLS Server Name Indication sent by the client in the ClientHello. |
+| `tls_version` | keyword | eBPF | _TLS only._ Negotiated TLS protocol version (for example, `TLS 1.2`, `TLS 1.3`). |
+| `tls_cipher_suite` | keyword | eBPF | _TLS only._ Negotiated TLS cipher suite (for example, `TLS_AES_256_GCM_SHA384`). |
diff --git a/calico-enterprise/observability/elastic/l7/enable-ebpf.mdx b/calico-enterprise/observability/elastic/l7/enable-ebpf.mdx
new file mode 100644
index 0000000000..f18573b1d8
--- /dev/null
+++ b/calico-enterprise/observability/elastic/l7/enable-ebpf.mdx
@@ -0,0 +1,42 @@
+---
+description: Enable the kernel TCP-layer eBPF collector to capture HTTP and TLS L7 logs without a proxy or sidecar.
+---
+
+# Enable eBPF collector
+
+Enable the kernel TCP-layer eBPF collector to capture HTTP request/response and TLS handshake metadata without deploying a proxy or injecting sidecars. For background on what the collector does and how it compares to other options, see [L7 logs overview](overview.mdx).
+
+## Before you begin
+
+### Limitations
+
+* Requires Linux kernel 5.17 or later on every node.
+* HTTP parsing covers HTTP/1.0 and HTTP/1.1 only, because their requests and responses travel as plain text on the wire. HTTP/2 and HTTP/3 are not captured as HTTP, and encrypted HTTP traffic surfaces only as TLS metadata.
+* HTTP capture tracks only the most recent request per socket; when a single connection carries multiple back-to-back requests, some may be missed.
+* TLS capture is limited to handshake metadata (SNI, version, cipher suite). The collector does not decrypt application traffic.
+
+## Enable the collector
+
+Enable the eBPF L7 collector by setting the [L7ObservabilityEnabled](../../../reference/component-resources/node/felix/configuration.mdx#L7ObservabilityEnabled) field to `true` on the default `FelixConfiguration`. The setting is independent of `bpfEnabled` — the collector works with any data plane.
+
+```bash
+kubectl patch felixconfiguration default --type=merge -p '{"spec":{"l7ObservabilityEnabled":true}}'
+```
+
+To disable the collector, set the field back to `false`. Felix tears down its BPF programs and cleans up pinned maps; no node reboot is required.
+
+```bash
+kubectl patch felixconfiguration default --type=merge -p '{"spec":{"l7ObservabilityEnabled":false}}'
+```
+
+## View L7 logs
+
+The eBPF collector writes L7 events as JSON to a log file on each node. By default the file is at `/var/log/calico/l7logs/l7.log`. The directory is controlled by the [L7LogsFileDirectory](../../../reference/component-resources/node/felix/configuration.mdx#L7LogsFileDirectory) field on `FelixConfiguration`.
+
+To tail the log on a node:
+
+```bash
+tail -f /var/log/calico/l7logs/l7.log
+```
+
+For the JSON schema, see [L7 log data types](datatypes.mdx).
diff --git a/calico-enterprise/observability/elastic/l7/enable-waypoint.mdx b/calico-enterprise/observability/elastic/l7/enable-waypoint.mdx
new file mode 100644
index 0000000000..e78bd48de5
--- /dev/null
+++ b/calico-enterprise/observability/elastic/l7/enable-waypoint.mdx
@@ -0,0 +1,20 @@
+---
+description: Enable the Istio Ambient Mode Waypoint-based L7 collector.
+draft: true
+---
+
+# Enable Waypoint collector
+
+Enable the Istio Ambient Mode Waypoint-based L7 collector to capture HTTP/1, HTTP/2, and gRPC traffic routed through a Waypoint proxy. For background on what the collector does and how it compares to other options, see [L7 logs overview](overview.mdx).
+
+:::note
+
+This page is a placeholder. Content for L7 log collection via Istio Ambient Mode Waypoint proxies is pending.
+
+:::
+
+## Before you begin
+
+## Enable the collector
+
+## View L7 logs
diff --git a/calico-enterprise/observability/elastic/l7/overview.mdx b/calico-enterprise/observability/elastic/l7/overview.mdx
new file mode 100644
index 0000000000..ebc7884f1d
--- /dev/null
+++ b/calico-enterprise/observability/elastic/l7/overview.mdx
@@ -0,0 +1,55 @@
+---
+description: Overview of Calico Enterprise L7 logs, the available collectors, and how to choose between them.
+---
+
+# L7 logs
+
+## Big picture
+
+$[prodname] L7 logs capture HTTP request/response and TLS handshake metadata for traffic between workloads in your cluster. They show what was actually sent (method, URL, response code, SNI, TLS version) between specific pods, going beyond the L3/4 flow log view of who-talked-to-whom.
+
+## Value
+
+Platform operators and development teams use L7 logs to:
+
+- See how applications interact, request by request.
+- Spot anomalous behavior — attempts to access restricted URLs, unexpected user agents, scans for particular paths.
+- Inspect TLS sessions by SNI, negotiated version, and cipher suite.
+
+L7 logs complement L3/4 flow logs, which expose only the source/destination pair without request-level detail.
+
+## Collectors
+
+$[prodname] supports multiple L7 collectors. They all feed the same aggregation and reporting pipeline; each entry on the log file is tagged with `collector_type` and `collector_name` so consumers can filter or join sources as needed.
+
+- **eBPF collector** — kernel TCP-layer probes that capture HTTP and TLS metadata with no data path proxy or sidecar. Enable once at the cluster level. Data-plane-independent. See [Enable eBPF collector](enable-ebpf.mdx).
+{/* TODO: re-add Istio Waypoint collector bullet when the Waypoint PR lands. */}
+- **Envoy collector** _(legacy, deprecated)_ — sidecar Envoy injected per workload. Being phased out. See [Enable Envoy collector](configure.mdx).
+
+### Why more than one collector?
+
+Each collector lives at a different point in the request path and exposes different trade-offs. Pick by where you need coverage and what infrastructure you're willing to run:
+
+{/* TODO: re-add the Istio Waypoint column when the Waypoint PR lands. */}
+
+| | eBPF | Envoy (legacy) |
+| --- | --- | --- |
+| Additional infrastructure | None — runs inside Felix | Sidecar Envoy per workload |
+| Protocol coverage | HTTP/1.x, TLS handshake | HTTP |
+| Opt-in granularity | Cluster-wide (FelixConfiguration) | Per workload (label + annotation) |
+| Service-mesh compatibility | Any data plane | Not compatible with Istio |
+
+Multiple collectors can coexist in the same cluster, with each L7 log entry tagged by `collector_type` and `collector_name` so consumers can filter or join sources as needed.
+
+:::note
+
+L7 logs require a minimum of 1 additional GB of log storage per node, per one-day retention period. Adjust your [Log Storage](../../../operations/logstorage/adjust-log-storage-size.mdx) before enabling a collector.
+
+:::
+
+## Next steps
+
+- [Enable eBPF collector](enable-ebpf.mdx)
+{/* TODO: re-add [Enable Waypoint collector](enable-waypoint.mdx) when the Waypoint PR lands. */}
+- [L7 log aggregation](aggregation.mdx)
+- [L7 log data types](datatypes.mdx)
diff --git a/calico-enterprise/observability/get-started-cem.mdx b/calico-enterprise/observability/get-started-cem.mdx
index 8f24a637a7..67856a421a 100644
--- a/calico-enterprise/observability/get-started-cem.mdx
+++ b/calico-enterprise/observability/get-started-cem.mdx
@@ -19,7 +19,7 @@ The Dashboard provides a birds-eye view of cluster activity. Note the following:
- The filter panel at the top lets you change dashboard views and the time range.
- The **Layout Settings** shows the default metrics. To get WireGuard metrics for pod-to-pod and host-to-host encryption, you must [enable WireGuard](../compliance/encrypt-cluster-pod-traffic.mdx).
-- For application-related dashboard cards to show data, like HTTP Response Codes or Url Requests, you need to [configure L7 logs](elastic/l7/configure.mdx).
+- For application-related dashboard cards to show data, like HTTP Response Codes or Url Requests, you need to [configure L7 logs](elastic/l7/overview.mdx).

diff --git a/calico-enterprise/observability/kibana.mdx b/calico-enterprise/observability/kibana.mdx
index f1370d3f81..766111a95e 100644
--- a/calico-enterprise/observability/kibana.mdx
+++ b/calico-enterprise/observability/kibana.mdx
@@ -57,7 +57,7 @@ The dashboard provides the following metrics/data, which can be edited as requir

-The L7 HTTP 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.
+The L7 HTTP 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/overview.mdx) are not enabled by default, and must be configured.
The default metrics are:
diff --git a/sidebars-calico-enterprise.js b/sidebars-calico-enterprise.js
index 5c9e7a3f73..8834013ecd 100644
--- a/sidebars-calico-enterprise.js
+++ b/sidebars-calico-enterprise.js
@@ -402,7 +402,14 @@ module.exports = {
type: 'category',
label: 'L7 logs',
link: { type: 'doc', id: 'observability/elastic/l7/index' },
- items: ['observability/elastic/l7/configure', 'observability/elastic/l7/datatypes'],
+ items: [
+ { type: 'doc', id: 'observability/elastic/l7/overview', label: 'Overview' },
+ { type: 'doc', id: 'observability/elastic/l7/enable-ebpf', label: 'Enable eBPF collector' },
+ { type: 'doc', id: 'observability/elastic/l7/enable-waypoint', label: 'Enable Waypoint collector' },
+ { type: 'doc', id: 'observability/elastic/l7/configure', label: 'Enable Envoy collector' },
+ { type: 'doc', id: 'observability/elastic/l7/datatypes', label: 'Data types' },
+ { type: 'doc', id: 'observability/elastic/l7/aggregation', label: 'Aggregation' },
+ ],
},
'observability/elastic/troubleshoot',
],