Skip to content

Conversation

@ariary
Copy link

@ariary ariary commented Dec 4, 2025

Add Export Button for Service Map Connections

Related issue #239

Overview

This PR adds an export button to the service map that allows users to download endpoint connections as a JSON file. The export captures the current service map topology without including the underlying flow logs.

Changes

  • New ExportButton component (src/components/TopBar/ExportButton.tsx)

    • Simple button with export icon positioned in the TopBar
    • Triggers export on click
  • Export utilities (src/utils/export/)

    • ServiceMapExporter class with JSON export functionality
    • Type definitions for exported data structure
    • Console logging for debugging and verification
  • Integration

    • Added export handler to ServiceMapApp component
    • Wired export button to TopBar component

Exported Data Format

The JSON export includes for each connection:

  • Source/Destination: Service names, IDs, and namespaces
  • Network Details: Destination port and IP protocol
  • Security: Verdicts (Allowed, Dropped, Error), authentication types, encryption status
  • Metrics: Throughput (flow amount, latency, bytes transferred)
  • Summary: Total connections and unique services count

Testing

Tested with a local kind cluster running Cilium/Hubble:

  • Export button appears in the TopBar
  • Downloads JSON file with accurate connection data
  • Console logs verify export process

Example Export

Extracted from tests

{
"exportedAt": "2025-12-03T16:15:23.768Z",
"connections": [
  {
    "source": {
      "id": "4231",
      "name": "client",
      "namespace": "app-a"
    },
    "destination": {
      "id": "43557",
      "name": "kube-dns",
      "namespace": "kube-system"
    },
    "destinationPort": 53,
    "ipProtocol": "2",
    "verdicts": [
      "1"
    ],
    "authTypes": [
      "0"
    ],
    "isEncrypted": false,
    "throughput": {
      "flowAmount": 0,
      "latency": {
        "min": 0,
        "max": 0,
        "avg": 0
      },
      "bytesTransfered": 0
    }
  },
  {
    "source": {
      "id": "5853",
      "name": "client",
      "namespace": "app-b"
    },
    "destination": {
      "id": "43557",
      "name": "kube-dns",
      "namespace": "kube-system"
    },
    "destinationPort": 53,
    "ipProtocol": "2",
    "verdicts": [
      "1"
    ],
    "authTypes": [
      "0"
    ],
    "isEncrypted": false,
    "throughput": {
      "flowAmount": 0,
      "latency": {
        "min": 0,
        "max": 0,
        "avg": 0
      },
      "bytesTransfered": 0
    }
  },
  {
    "source": {
      "id": "1",
      "name": "host",
      "namespace": null
    },
    "destination": {
      "id": "43557",
      "name": "kube-dns",
      "namespace": "kube-system"
    },
    "destinationPort": 8080,
    "ipProtocol": "1",
    "verdicts": [
      "1"
    ],
    "authTypes": [
      "0"
    ],
    "isEncrypted": false,
    "throughput": {
      "flowAmount": 0,
      "latency": {
        "min": 0,
        "max": 0,
        "avg": 0
      },
      "bytesTransfered": 0
    }
  },
  {
    "source": {
      "id": "1",
      "name": "host",
      "namespace": null
    },
    "destination": {
      "id": "43557",
      "name": "kube-dns",
      "namespace": "kube-system"
    },
    "destinationPort": 8181,
    "ipProtocol": "1",
    "verdicts": [
      "1"
    ],
    "authTypes": [
      "0"
    ],
    "isEncrypted": false,
    "throughput": {
      "flowAmount": 0,
      "latency": {
        "min": 0,
        "max": 0,
        "avg": 0
      },
      "bytesTransfered": 0
    }
  },
  {
    "source": {
      "id": "14998",
      "name": "hubble-relay",
      "namespace": "kube-system"
    },
    "destination": {
      "id": "1",
      "name": "host",
      "namespace": null
    },
    "destinationPort": 4244,
    "ipProtocol": "1",
    "verdicts": [
      "1"
    ],
    "authTypes": [
      "0"
    ],
    "isEncrypted": false,
    "throughput": {
      "flowAmount": 0,
      "latency": {
        "min": 0,
        "max": 0,
        "avg": 0
      },
      "bytesTransfered": 0
    }
  },
  {
    "source": {
      "id": "1",
      "name": "host",
      "namespace": null
    },
    "destination": {
      "id": "14998",
      "name": "hubble-relay",
      "namespace": "kube-system"
    },
    "destinationPort": 4222,
    "ipProtocol": "1",
    "verdicts": [
      "1"
    ],
    "authTypes": [
      "0"
    ],
    "isEncrypted": false,
    "throughput": {
      "flowAmount": 0,
      "latency": {
        "min": 0,
        "max": 0,
        "avg": 0
      },
      "bytesTransfered": 0
    }
  },
  {
    "source": {
      "id": "1",
      "name": "host",
      "namespace": null
    },
    "destination": {
      "id": "3615",
      "name": "hubble-ui",
      "namespace": "kube-system"
    },
    "destinationPort": 8081,
    "ipProtocol": "1",
    "verdicts": [
      "1"
    ],
    "authTypes": [
      "0"
    ],
    "isEncrypted": false,
    "throughput": {
      "flowAmount": 0,
      "latency": {
        "min": 0,
        "max": 0,
        "avg": 0
      },
      "bytesTransfered": 0
    }
  },
  {
    "source": {
      "id": "3615",
      "name": "hubble-ui",
      "namespace": "kube-system"
    },
    "destination": {
      "id": "43557",
      "name": "kube-dns",
      "namespace": "kube-system"
    },
    "destinationPort": 53,
    "ipProtocol": "2",
    "verdicts": [
      "1"
    ],
    "authTypes": [
      "0"
    ],
    "isEncrypted": false,
    "throughput": {
      "flowAmount": 0,
      "latency": {
        "min": 0,
        "max": 0,
        "avg": 0
      },
      "bytesTransfered": 0
    }
  },
  {
    "source": {
      "id": "3615",
      "name": "hubble-ui",
      "namespace": "kube-system"
    },
    "destination": {
      "id": "14998",
      "name": "hubble-relay",
      "namespace": "kube-system"
    },
    "destinationPort": 4245,
    "ipProtocol": "1",
    "verdicts": [
      "1"
    ],
    "authTypes": [
      "0"
    ],
    "isEncrypted": false,
    "throughput": {
      "flowAmount": 0,
      "latency": {
        "min": 0,
        "max": 0,
        "avg": 0
      },
      "bytesTransfered": 0
    }
  },
  {
    "source": {
      "id": "43557",
      "name": "kube-dns",
      "namespace": "kube-system"
    },
    "destination": {
      "id": "1",
      "name": "host",
      "namespace": null
    },
    "destinationPort": 6443,
    "ipProtocol": "1",
    "verdicts": [
      "1"
    ],
    "authTypes": [
      "0"
    ],
    "isEncrypted": false,
    "throughput": {
      "flowAmount": 0,
      "latency": {
        "min": 0,
        "max": 0,
        "avg": 0
      },
      "bytesTransfered": 0
    }
  }
],
"summary": {
  "totalConnections": 10,
  "totalServices": 6
}
}

UI changes

export

Add an export button to the service map that allows users to download
endpoint connections as JSON. The export includes:

- Source and destination service names, IDs, and namespaces
- Destination ports and IP protocols
- Connection verdicts (Allowed, Dropped, Error, etc.)
- Authentication types and encryption status
- Throughput metrics (flow amount, latency, bytes transferred)

The export contains only the connection topology (arrows between services),
not the underlying flow logs, making it suitable for documentation,
analysis, and integration with external tools.

Fixes cilium#239
@ariary ariary requested a review from a team as a code owner December 4, 2025 17:08
@ariary ariary requested review from chancez and removed request for a team December 4, 2025 17:08
@yannikmesserli yannikmesserli requested review from yannikmesserli and removed request for chancez December 5, 2025 08:17
Copy link
Contributor

@yannikmesserli yannikmesserli left a comment

Choose a reason for hiding this comment

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

Thanks @aviary for this PR! I haven't yet tested it locally, but I want to request code changes first. Especially, the class situation and the console situation.

I know this code base is filled with pattern like you are proposing, but I believe it's not good one. Especially static methods are much better isolated, pure, so we can easily tests them and reuse without the entire context of the class. Can you change this already in the PR - I am working on a proposal for restructuring Hubble UI with better frontend patterns.

Secondly, I will also challenge the feature itself you are implementing. You export a specific schema (ExportedServiceMap) which seems particular to some use-cases (?) but does not feel useful for the entire community. Should we build on top of your PR to address fully #239? A SVG of the service map seems more appropriate for everyone... and we can directly get it from the rendering. I can help with this, if you want?

import { Link } from '~/domain/link';
import { ExportedConnection, ExportedServiceMap } from './types';

export class ServiceMapExporter {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please, use pure functions and unit tests them. (No class, and especially no static method)

const connections: ExportedConnection[] = [];
let skippedLinks = 0;

console.log('[ServiceMapExporter] Starting export...', {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is all the console logs really necessary for this feature? Hubble UI console is already filled with some many messages, which usually are useless for most users, I think we should use them with care. Hopefully in the future we can find a more sustainable way. I would recommend to only console log the summary, especially remove all the console logs that you have used for developing (e.g. sample flow)

onExport?: () => void;
}

export const ExportButton = observer(function ExportButton(props: Props) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
export const ExportButton = observer(function ExportButton(props: Props) {
export const ExportButton = function ExportButton(props: Props) {

you are not using any Mobx elements in this component, so no need to wrap it in observer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants