Skip to content

AWS VPC Flow Logs OTEL Dashboards

DevOps/SRE monitoring dashboards for AWS VPC Flow Logs collected via OpenTelemetry.

Overview

Based on the aws_vpcflow_otel package from elastic/integrations.

Dashboards

Dashboard File Description
VPC Flow Logs Overview overview.yaml High-level KPIs and time-series trends
Traffic Analysis traffic.yaml Traffic distribution, source analysis, and security deep dive
Interface Analysis interface.yaml Per-interface analysis and account metrics

Dashboard Definitions

VPC Flow Logs Overview (overview.yaml)
---
dashboards:
  # Dashboard 1: Overview - High-level KPIs and trends
  - id: aws_vpcflow_otel-overview
    name: '[AWS VPC OTEL] VPC Flow Logs Overview'
    description: High-level status and trends for AWS VPC Flow Logs
    filters:
      - field: data_stream.dataset
        equals: aws.vpcflow.otel
    controls:
      - type: options
        label: Cloud Account ID
        data_view: logs-*
        field: cloud.account.id
      - type: options
        label: Network Interface
        data_view: logs-*
        field: network.interface.name
      - type: options
        label: Action
        width: small
        data_view: logs-*
        field: aws.vpc.flow.action
    panels:
      # Navigation
      - size: {w: 48, h: 2}
        links:
          layout: horizontal
          items:
            - label: Overview
              dashboard: aws_vpcflow_otel-overview
            - label: Traffic Analysis
              dashboard: aws_vpcflow_otel-traffic
            - label: Interface Analysis
              dashboard: aws_vpcflow_otel-interface
      # KPI Metrics (no section header - first section)
      - title: Total Flow Records
        hide_title: true
        size: {w: 8, h: 4}
        esql:
          type: metric
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel"
            - STATS total_flows = COUNT()
          primary:
            field: total_flows
            label: Flow Records
            format:
              type: number
              decimals: 0
      - title: Rejection Rate
        hide_title: true
        size: {w: 8, h: 4}
        esql:
          type: metric
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel"
            - EVAL is_rejected = CASE(aws.vpc.flow.action == "REJECT", 1, 0)
            - STATS total_flows = COUNT(), rejected_flows = SUM(is_rejected)
            - EVAL rejection_rate = CASE(total_flows > 0, rejected_flows::double / total_flows::double, 0)
          primary:
            field: rejection_rate
            label: Rejection Rate
            format:
              type: percent
              decimals: 1
      - title: Total Bandwidth
        hide_title: true
        size: {w: 8, h: 4}
        esql:
          type: metric
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND aws.vpc.flow.bytes IS NOT NULL
            - STATS total_bytes = SUM(aws.vpc.flow.bytes)
          primary:
            field: total_bytes
            label: Total Bandwidth
            format:
              type: bytes
      - title: Active Interfaces
        hide_title: true
        size: {w: 8, h: 4}
        esql:
          type: metric
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND network.interface.name IS NOT NULL
            - STATS unique_interfaces = COUNT_DISTINCT(network.interface.name)
          primary:
            field: unique_interfaces
            label: Active Interfaces
            format:
              type: number
              decimals: 0
      - title: Cloud Accounts
        hide_title: true
        size: {w: 8, h: 4}
        esql:
          type: metric
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND cloud.account.id IS NOT NULL
            - STATS unique_accounts = COUNT_DISTINCT(cloud.account.id)
          primary:
            field: unique_accounts
            label: Cloud Accounts
            format:
              type: number
              decimals: 0
      - title: Unique Protocols
        hide_title: true
        size: {w: 8, h: 4}
        esql:
          type: metric
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND network.protocol.name IS NOT NULL
            - STATS unique_protocols = COUNT_DISTINCT(network.protocol.name)
          primary:
            field: unique_protocols
            label: Unique Protocols
            format:
              type: number
              decimals: 0

      # Quick Insights
      - markdown:
          content: '### Quick Insights'
        size: {w: 48, h: 3}
      - title: Top 5 Interfaces by Rejected Traffic
        size: {w: 24, h: 10}
        esql:
          type: bar
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND aws.vpc.flow.action == "REJECT" AND network.interface.name IS NOT NULL
            - STATS rejected_count = COUNT() BY network.interface.name
            - SORT rejected_count DESC
            - LIMIT 5
          dimension:
            field: network.interface.name
            label: Interface
          metrics:
            - field: rejected_count
              label: Rejected Flows
              format:
                type: number
                decimals: 0
          legend:
            visible: hide
          color:
            palette: negative
          appearance:
            y_left_axis:
              title: Rejected Flows
      - title: Top 5 Rejected Destination Ports
        size: {w: 24, h: 10}
        esql:
          type: bar
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND aws.vpc.flow.action == "REJECT" AND destination.port IS NOT NULL
            - STATS rejected_count = COUNT() BY destination.port
            - SORT rejected_count DESC
            - LIMIT 5
          dimension:
            field: destination.port
            label: Dest Port
          metrics:
            - field: rejected_count
              label: Rejected Flows
              format:
                type: number
                decimals: 0
          legend:
            visible: hide
          color:
            palette: negative
          appearance:
            y_left_axis:
              title: Rejected Flows

      # Time-Series Trends
      - markdown:
          content: '### Time-Series Trends'
        size: {w: 48, h: 3}
      - title: Traffic Volume Over Time
        size: {w: 48, h: 12}
        esql:
          type: area
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND aws.vpc.flow.action IS NOT NULL
            - STATS flow_count = COUNT() BY aws.vpc.flow.action, time_bucket = BUCKET(@timestamp, 50, ?_tstart, ?_tend)
            - SORT time_bucket ASC
          mode: stacked
          dimension:
            field: time_bucket
            label: Time
            data_type: date
          metrics:
            - field: flow_count
              label: Flow Count
              format:
                type: number
                decimals: 0
          breakdown:
            field: aws.vpc.flow.action
          color:
            palette: eui_amsterdam_color_blind
            assignments:
              - value: REJECT
                color: '#BD271E'
              - value: ACCEPT
                color: '#00BF6F'
          appearance:
            x_axis:
              title: '@timestamp'
            y_left_axis:
              title: Flow Count
      - title: Bandwidth Usage Over Time
        size: {w: 48, h: 12}
        esql:
          type: line
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND aws.vpc.flow.bytes IS NOT NULL
            - STATS total_bytes = SUM(aws.vpc.flow.bytes) BY time_bucket = BUCKET(@timestamp, 50, ?_tstart, ?_tend)
            - SORT time_bucket ASC
          dimension:
            field: time_bucket
            label: Time
            data_type: date
          metrics:
            - field: total_bytes
              label: Bytes
              format:
                type: bytes
          appearance:
            x_axis:
              title: '@timestamp'
            y_left_axis:
              title: Bandwidth
Traffic Analysis (traffic.yaml)
---
dashboards:
  # Dashboard 2: Traffic Analysis - Distribution, sources, and security
  - id: aws_vpcflow_otel-traffic
    name: '[AWS VPC OTEL] Traffic Analysis'
    description: Detailed traffic distribution, source analysis, and security deep dive
    filters:
      - field: data_stream.dataset
        equals: aws.vpcflow.otel
    controls:
      # Primary filters
      - type: options
        label: Cloud Account ID
        data_view: logs-*
        field: cloud.account.id
      - type: options
        label: Network Interface
        data_view: logs-*
        field: network.interface.name
      - type: options
        label: Action
        width: small
        data_view: logs-*
        field: aws.vpc.flow.action
      # Traffic filters
      - type: options
        label: Protocol
        width: small
        data_view: logs-*
        field: network.protocol.name
      - type: options
        label: Destination Port
        width: small
        data_view: logs-*
        field: destination.port
      - type: options
        label: Source Port
        width: small
        data_view: logs-*
        field: source.port
    panels:
      # Navigation
      - size: {w: 48, h: 2}
        links:
          layout: horizontal
          items:
            - label: Overview
              dashboard: aws_vpcflow_otel-overview
            - label: Traffic Analysis
              dashboard: aws_vpcflow_otel-traffic
            - label: Interface Analysis
              dashboard: aws_vpcflow_otel-interface

      # Traffic KPIs (no section header - first section)
      - title: Total Flow Records
        hide_title: true
        size: {w: 10, h: 4}
        esql:
          type: metric
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel"
            - STATS total_flows = COUNT()
          primary:
            field: total_flows
            label: Flow Records
            format:
              type: number
              decimals: 0
      - title: Unique Source IPs
        hide_title: true
        size: {w: 10, h: 4}
        esql:
          type: metric
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND source.address IS NOT NULL
            - STATS unique_sources = COUNT_DISTINCT(source.address)
          primary:
            field: unique_sources
            label: Unique Sources
            format:
              type: number
              decimals: 0
      - title: Unique Destination IPs
        hide_title: true
        size: {w: 9, h: 4}
        esql:
          type: metric
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND destination.address IS NOT NULL
            - STATS unique_destinations = COUNT_DISTINCT(destination.address)
          primary:
            field: unique_destinations
            label: Unique Destinations
            format:
              type: number
              decimals: 0
      - title: Unique Protocols
        hide_title: true
        size: {w: 10, h: 4}
        esql:
          type: metric
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND network.protocol.name IS NOT NULL
            - STATS unique_protocols = COUNT_DISTINCT(network.protocol.name)
          primary:
            field: unique_protocols
            label: Protocols
            format:
              type: number
              decimals: 0
      - title: Unique Destination Ports
        hide_title: true
        size: {w: 9, h: 4}
        esql:
          type: metric
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND destination.port IS NOT NULL
            - STATS unique_ports = COUNT_DISTINCT(destination.port)
          primary:
            field: unique_ports
            label: Dest Ports
            format:
              type: number
              decimals: 0

      # Traffic Trend
      - title: Traffic by Action Over Time
        size: {w: 48, h: 10}
        esql:
          type: area
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND aws.vpc.flow.action IS NOT NULL
            - STATS flow_count = COUNT() BY aws.vpc.flow.action, time_bucket = BUCKET(@timestamp, 50, ?_tstart, ?_tend)
            - SORT time_bucket ASC
          mode: stacked
          dimension:
            field: time_bucket
            label: Time
            data_type: date
          metrics:
            - field: flow_count
              label: Flow Count
              format:
                type: number
                decimals: 0
          breakdown:
            field: aws.vpc.flow.action
          color:
            palette: eui_amsterdam_color_blind
            assignments:
              - value: REJECT
                color: '#BD271E'
              - value: ACCEPT
                color: '#00BF6F'
          appearance:
            x_axis:
              title: '@timestamp'
            y_left_axis:
              title: Flow Count

      # Traffic Distribution
      - title: Top Protocols
        size: {w: 24, h: 12}
        esql:
          type: pie
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND network.protocol.name IS NOT NULL
            - STATS flow_count = COUNT() BY network.protocol.name
            - SORT flow_count DESC
            - LIMIT 10
          metrics:
            - field: flow_count
              label: Flow Count
              format:
                type: number
                decimals: 0
          breakdowns:
            - field: network.protocol.name
              label: Protocol
      - title: Top Destination Ports
        size: {w: 24, h: 12}
        esql:
          type: bar
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND destination.port IS NOT NULL
            - STATS flow_count = COUNT() BY destination.port
            - SORT flow_count DESC
            - LIMIT 10
          dimension:
            field: destination.port
            label: Dest Port
          metrics:
            - field: flow_count
              label: Flow Count
              format:
                type: number
                decimals: 0
          legend:
            visible: hide
          appearance:
            y_left_axis:
              title: Flow Count
      # Source Analysis
      - markdown:
          content: '### Source Analysis'
        size: {w: 48, h: 3}
      - title: Top Source IPs - Detailed
        size: {w: 48, h: 13}
        esql:
          type: datatable
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND source.address IS NOT NULL
            - EVAL is_accepted = CASE(aws.vpc.flow.action == "ACCEPT", 1, 0)
            - EVAL is_rejected = CASE(aws.vpc.flow.action == "REJECT", 1, 0)
            - STATS total_flows = COUNT(), accepted_flows = SUM(is_accepted), rejected_flows = SUM(is_rejected), total_bytes = SUM(aws.vpc.flow.bytes),
              total_packets = SUM(aws.vpc.flow.packets) BY source.address
            - EVAL rejection_rate = CASE(total_flows > 0, rejected_flows::double / total_flows::double, 0)
            - SORT total_flows DESC
            - LIMIT 20
          breakdowns:
            - field: source.address
              label: Source IP
          metrics:
            - field: total_flows
              label: Total Flows
              format:
                type: number
                decimals: 0
            - field: accepted_flows
              label: Accepted
              format:
                type: number
                decimals: 0
            - field: rejected_flows
              label: Rejected
              format:
                type: number
                decimals: 0
            - field: rejection_rate
              label: Rejection %
              format:
                type: percent
                decimals: 1
            - field: total_bytes
              label: Bytes
              format:
                type: bytes
            - field: total_packets
              label: Packets
              format:
                type: number
                decimals: 0
          paging:
            enabled: true
            page_size: 10

      # Security Deep Dive
      - markdown:
          content: '### Security Deep Dive'
        size: {w: 48, h: 3}
      - title: Rejected Traffic by Protocol
        size: {w: 24, h: 12}
        esql:
          type: bar
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND aws.vpc.flow.action == "REJECT" AND network.protocol.name IS NOT NULL
            - STATS rejected_count = COUNT() BY network.protocol.name
            - SORT rejected_count DESC
            - LIMIT 10
          dimension:
            field: network.protocol.name
            label: Protocol
          metrics:
            - field: rejected_count
              label: Rejected Count
              format:
                type: number
                decimals: 0
          legend:
            visible: hide
          color:
            palette: negative
          appearance:
            y_left_axis:
              title: Rejected Count
      - title: Top Rejected Ports
        size: {w: 24, h: 12}
        esql:
          type: bar
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND aws.vpc.flow.action == "REJECT" AND destination.port IS NOT NULL
            - STATS rejected_count = COUNT() BY destination.port
            - SORT rejected_count DESC
            - LIMIT 10
          dimension:
            field: destination.port
            label: Dest Port
          metrics:
            - field: rejected_count
              label: Rejected Count
              format:
                type: number
                decimals: 0
          legend:
            visible: hide
          color:
            palette: negative
          appearance:
            y_left_axis:
              title: Rejected Count
      - title: Detailed Rejection Logs
        size: {w: 48, h: 13}
        esql:
          type: datatable
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND aws.vpc.flow.action == "REJECT"
            - KEEP @timestamp, source.address, destination.address, destination.port, network.protocol.name, network.interface.name, aws.vpc.flow.bytes,
              aws.vpc.flow.packets
            - SORT @timestamp DESC
            - LIMIT 1000
          breakdowns:
            - field: '@timestamp'
              label: Timestamp
            - field: source.address
              label: Source IP
            - field: destination.address
              label: Destination IP
            - field: destination.port
              label: Dest Port
            - field: network.protocol.name
              label: Protocol
            - field: network.interface.name
              label: Interface
          metrics:
            - field: aws.vpc.flow.bytes
              label: Bytes
              format:
                type: bytes
            - field: aws.vpc.flow.packets
              label: Packets
              format:
                type: number
                decimals: 0
          paging:
            enabled: true
            page_size: 15

      # Top Sources and Destinations
      - markdown:
          content: '### Top Sources and Destinations'
        size: {w: 48, h: 3}
      - title: Top Destination Ports
        size: {w: 16, h: 12}
        esql:
          type: bar
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND destination.port IS NOT NULL
            - STATS flow_count = COUNT() BY destination.port
            - SORT flow_count DESC
            - LIMIT 10
          dimension:
            field: destination.port
            label: Dest Port
          metrics:
            - field: flow_count
              label: Flow Count
              format:
                type: number
                decimals: 0
          legend:
            visible: hide
          appearance:
            y_left_axis:
              title: Flow Count
      - title: Top Destination IPs
        size: {w: 16, h: 12}
        esql:
          type: bar
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND destination.address IS NOT NULL
            - STATS flow_count = COUNT() BY destination.address
            - SORT flow_count DESC
            - LIMIT 10
          dimension:
            field: destination.address
            label: Destination IP
          metrics:
            - field: flow_count
              label: Flow Count
              format:
                type: number
                decimals: 0
          legend:
            visible: hide
          appearance:
            y_left_axis:
              title: Flow Count
      - title: Top Source IPs
        size: {w: 16, h: 12}
        esql:
          type: bar
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND source.address IS NOT NULL
            - STATS flow_count = COUNT() BY source.address
            - SORT flow_count DESC
            - LIMIT 10
          dimension:
            field: source.address
            label: Source IP
          metrics:
            - field: flow_count
              label: Flow Count
              format:
                type: number
                decimals: 0
          legend:
            visible: hide
          appearance:
            y_left_axis:
              title: Flow Count
Interface Analysis (interface.yaml)
---
dashboards:
  # Dashboard 3: Interface Analysis - Per-interface deep dive
  - id: aws_vpcflow_otel-interface
    name: '[AWS VPC OTEL] Interface Analysis'
    description: Per-interface analysis for investigating specific network interfaces
    filters:
      - field: data_stream.dataset
        equals: aws.vpcflow.otel
    controls:
      # Primary filters
      - type: options
        label: Cloud Account ID
        data_view: logs-*
        field: cloud.account.id
      - type: options
        label: Network Interface
        data_view: logs-*
        field: network.interface.name
      - type: options
        label: Action
        width: small
        data_view: logs-*
        field: aws.vpc.flow.action
      # Traffic filters for drill-down
      - type: options
        label: Destination Port
        width: small
        data_view: logs-*
        field: destination.port
      - type: options
        label: Protocol
        width: small
        data_view: logs-*
        field: network.protocol.name
    panels:
      # Navigation
      - size: {w: 48, h: 2}
        links:
          layout: horizontal
          items:
            - label: Overview
              dashboard: aws_vpcflow_otel-overview
            - label: Traffic Analysis
              dashboard: aws_vpcflow_otel-traffic
            - label: Interface Analysis
              dashboard: aws_vpcflow_otel-interface

      # Interface KPIs (no section header - first section)
      - title: Total Interfaces
        hide_title: true
        size: {w: 10, h: 4}
        esql:
          type: metric
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND network.interface.name IS NOT NULL
            - STATS total_interfaces = COUNT_DISTINCT(network.interface.name)
          primary:
            field: total_interfaces
            label: Total Interfaces
            format:
              type: number
              decimals: 0
      - title: Total Flow Records
        hide_title: true
        size: {w: 9, h: 4}
        esql:
          type: metric
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel"
            - STATS total_flows = COUNT()
          primary:
            field: total_flows
            label: Flow Records
            format:
              type: number
              decimals: 0
      - title: Unique Source IPs
        hide_title: true
        size: {w: 10, h: 4}
        esql:
          type: metric
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND source.address IS NOT NULL
            - STATS unique_sources = COUNT_DISTINCT(source.address)
          primary:
            field: unique_sources
            label: Unique Sources
            format:
              type: number
              decimals: 0
      - title: Unique Destination IPs
        hide_title: true
        size: {w: 9, h: 4}
        esql:
          type: metric
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND destination.address IS NOT NULL
            - STATS unique_destinations = COUNT_DISTINCT(destination.address)
          primary:
            field: unique_destinations
            label: Unique Destinations
            format:
              type: number
              decimals: 0
      - title: Unique Protocols
        hide_title: true
        size: {w: 10, h: 4}
        esql:
          type: metric
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND network.protocol.name IS NOT NULL
            - STATS unique_protocols = COUNT_DISTINCT(network.protocol.name)
          primary:
            field: unique_protocols
            label: Protocols
            format:
              type: number
              decimals: 0

      # Interface Traffic Trend
      - title: Interface Traffic Over Time
        size: {w: 48, h: 10}
        esql:
          type: area
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND network.interface.name IS NOT NULL
            - STATS flow_count = COUNT() BY time_bucket = BUCKET(@timestamp, 50, ?_tstart, ?_tend)
            - SORT time_bucket ASC
          dimension:
            field: time_bucket
            label: Time
            data_type: date
          metrics:
            - field: flow_count
              label: Flow Count
              format:
                type: number
                decimals: 0
          appearance:
            x_axis:
              title: '@timestamp'
            y_left_axis:
              title: Flow Count

      # Interface Traffic Analysis
      - title: Interface Traffic Analysis
        size: {w: 48, h: 13}
        esql:
          type: bar
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND network.interface.name IS NOT NULL AND aws.vpc.flow.action IS NOT NULL
            - STATS flow_count = COUNT() BY network.interface.name, aws.vpc.flow.action
            - SORT flow_count DESC
          mode: stacked
          dimension:
            field: network.interface.name
            label: Interface
          metrics:
            - field: flow_count
              label: Flow Count
              format:
                type: number
                decimals: 0
          breakdown:
            field: aws.vpc.flow.action
          color:
            palette: eui_amsterdam_color_blind
            assignments:
              - value: REJECT
                color: '#BD271E'
              - value: ACCEPT
                color: '#00BF6F'
          appearance:
            y_left_axis:
              title: Flow Count

      # Top Interfaces by Bandwidth
      - title: Top Interfaces by Bandwidth
        size: {w: 48, h: 13}
        esql:
          type: bar
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND network.interface.name IS NOT NULL AND aws.vpc.flow.bytes IS NOT NULL
            - STATS total_bytes = SUM(aws.vpc.flow.bytes) BY network.interface.name
            - SORT total_bytes DESC
            - LIMIT 10
          dimension:
            field: network.interface.name
            label: Interface
          metrics:
            - field: total_bytes
              label: Bytes
              format:
                type: bytes
          legend:
            visible: hide
          appearance:
            y_left_axis:
              title: Bytes

      # Traffic Details
      - markdown:
          content: '### Traffic Details'
        size: {w: 48, h: 3}
      - title: Accepted vs Rejected by Protocol
        size: {w: 24, h: 12}
        esql:
          type: bar
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND network.protocol.name IS NOT NULL AND aws.vpc.flow.action IS NOT NULL
            - STATS flow_count = COUNT() BY network.protocol.name, aws.vpc.flow.action
            - SORT flow_count DESC
          mode: stacked
          dimension:
            field: network.protocol.name
            label: Protocol
          metrics:
            - field: flow_count
              label: Flow Count
              format:
                type: number
                decimals: 0
          breakdown:
            field: aws.vpc.flow.action
          color:
            palette: eui_amsterdam_color_blind
            assignments:
              - value: REJECT
                color: '#BD271E'
              - value: ACCEPT
                color: '#00BF6F'
          appearance:
            y_left_axis:
              title: Flow Count
      - title: Bandwidth by Protocol
        size: {w: 24, h: 12}
        esql:
          type: bar
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND network.protocol.name IS NOT NULL AND aws.vpc.flow.bytes IS NOT NULL
            - STATS total_bytes = SUM(aws.vpc.flow.bytes) BY network.protocol.name
            - SORT total_bytes DESC
            - LIMIT 10
          dimension:
            field: network.protocol.name
            label: Protocol
          metrics:
            - field: total_bytes
              label: Bytes
              format:
                type: bytes
          legend:
            visible: hide
          appearance:
            y_left_axis:
              title: Bytes

      # Account Analysis
      - markdown:
          content: '### Account Analysis'
        size: {w: 48, h: 3}
      - title: Traffic by Cloud Account
        size: {w: 48, h: 13}
        esql:
          type: datatable
          query:
            - FROM logs-*
            - WHERE data_stream.dataset == "aws.vpcflow.otel" AND cloud.account.id IS NOT NULL
            - EVAL is_accepted = CASE(aws.vpc.flow.action == "ACCEPT", 1, 0)
            - EVAL is_rejected = CASE(aws.vpc.flow.action == "REJECT", 1, 0)
            - STATS total_flows = COUNT(), accepted_flows = SUM(is_accepted), rejected_flows = SUM(is_rejected), total_bytes = SUM(aws.vpc.flow.bytes),
              unique_interfaces = COUNT_DISTINCT(network.interface.name), unique_sources = COUNT_DISTINCT(source.address) BY cloud.account.id
            - EVAL rejection_rate = CASE(total_flows > 0, rejected_flows::double / total_flows::double, 0)
            - SORT total_flows DESC
            - LIMIT 100
          breakdowns:
            - field: cloud.account.id
              label: Cloud Account ID
          metrics:
            - field: total_flows
              label: Total Flows
              format:
                type: number
                decimals: 0
            - field: accepted_flows
              label: Accepted
              format:
                type: number
                decimals: 0
            - field: rejected_flows
              label: Rejected
              format:
                type: number
                decimals: 0
            - field: rejection_rate
              label: Rejection %
              format:
                type: percent
                decimals: 1
            - field: total_bytes
              label: Bytes
              format:
                type: bytes
            - field: unique_interfaces
              label: Interfaces
              format:
                type: number
                decimals: 0
            - field: unique_sources
              label: Unique Sources
              format:
                type: number
                decimals: 0
          paging:
            enabled: true
            page_size: 10

Prerequisites

  • AWS VPC Flow Logs: Configured for OpenTelemetry collection
  • OpenTelemetry Collector: Configured for VPC Flow Logs
  • Kibana: Version 8.x or later

Data Requirements

  • Data stream dataset: aws.vpcflow.otel
  • Data view: logs-*

Field Reference

Field Description
@timestamp Event timestamp
aws.vpc.flow.action Allow/Deny action
aws.vpc.flow.bytes Bytes transferred
aws.vpc.flow.packets Packets transferred
source.address Source IP address
source.port Source port
destination.address Destination IP address
destination.port Destination port
network.protocol.name Protocol name
network.interface.name Network interface name
cloud.account.id AWS account ID