Skip to content

Service Topology

CoSky's Service Topology module automatically builds a dependency graph of your microservice ecosystem. When one service calls another, the caller's identity (consumer) and the target's identity (producer) are recorded in Redis. This enables you to visualize which services depend on which, track cross-namespace dependencies, and monitor service-level instance statistics -- all without any manual configuration.

AspectDetail
InterfaceServiceTopology
Redis ImplementationRedisServiceTopology
Storage EngineRedis Hash (topology_idx + topology:{consumer})
Consumer IdentityFrom ServiceInstanceContext.serviceInstance
Concurrency ModelReactive (Mono<Map<String, Set<String>>>)

ServiceTopology Interface

The ServiceTopology interface defines two operations:

MethodReturn TypeDescriptionSource
addTopologyMono<Void>Records that the current service depends on a producer serviceServiceTopology.kt:23
getTopologyMono<Map<String, Set<String>>>Retrieves the full topology graph for a namespaceServiceTopology.kt:24

The interface also provides:

  • A NO_OP singleton that does nothing, used when topology tracking is disabled (ServiceTopology.kt:27).
  • consumerName derived from ServiceInstanceContext.serviceInstance.serviceId, falling back to "_Client_" if no instance is registered (ServiceTopology.kt:39).
  • getProducerName which prefixes the namespace if the producer is in a different namespace (e.g., "otherNs.order-service") (ServiceTopology.kt:48).

RedisServiceTopology

RedisServiceTopology stores topology data in two Redis hash structures:

addTopology

The addTopology method (RedisServiceTopology.kt:11) executes service_topology_add.lua, which:

  1. Records the consumer name in the topology index: HSETNX {namespace}:topology_idx {consumerName} {timestamp}
  2. Records the producer dependency: HSETNX {namespace}:topology:{consumerName} {producerName} {timestamp}

Self-dependencies (consumer == producer) are skipped (RedisServiceTopology.kt:15).

getTopology

The getTopology method (RedisServiceTopology.kt:26) executes service_topology_get.lua, which:

  1. Reads all consumer names from {namespace}:topology_idx via HKEYS
  2. For each consumer, reads their dependency set from {namespace}:topology:{consumerName} via HKEYS
  3. Returns the result as Map<String, Set<String>> -- mapping each consumer to its set of producers

Topology Building Process

mermaid
flowchart TD
    A[Service A makes HTTP call to Service B] --> B{ServiceInstanceContext<br>has serviceInstance?}
    B -->|Yes| C[consumerName = serviceInstance.serviceId]
    B -->|No| D[consumerName = '_Client_']
    C --> E[producerName = producerServiceId]
    D --> E
    E --> F{consumerName == producerName?}
    F -->|Yes| G[Skip - self dependency]
    F -->|No| H[EVAL service_topology_add.lua]
    H --> I[HSETNX topology_idx<br>consumer -> timestamp]
    H --> J[HSETNX topology:consumer<br>producer -> timestamp]

    style A fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style B fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style F fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style G fill:#161b22,stroke:#30363d,color:#e6edf3
    style H fill:#161b22,stroke:#30363d,color:#e6edf3
    style I fill:#161b22,stroke:#30363d,color:#e6edf3
    style J fill:#161b22,stroke:#30363d,color:#e6edf3

Sequence Diagram: Topology Query Flow

mermaid
sequenceDiagram
    autonumber
    participant Client as Dashboard / API
    participant RST as RedisServiceTopology
    participant Redis as Redis (Lua Script)

    Client->>RST: getTopology("production")
    RST->>Redis: EVAL service_topology_get.lua KEYS=["production"]
    Redis->>Redis: HKEYS production:topology_idx
    Redis-->>Redis: [gateway-svc, order-svc, user-svc]

    loop For each consumer
        Redis->>Redis: HKEYS production:topology:{consumer}
        Redis-->>Redis: List of producer names
    end

    Redis-->>RST: [consumer1, [producers1], consumer2, [producers2], ...]
    RST->>RST: parse into Map~String, Set~String~~
    RST-->>Client: Map of consumer -> set of producers

Sequence Diagram: Topology Recording Flow

mermaid
sequenceDiagram
    autonumber
    participant SvcA as Service A (Gateway)
    participant SC as ServiceInstanceContext
    participant RST as RedisServiceTopology
    participant Redis as Redis (Lua Script)
    participant Sub as PubSub

    SvcA->>SvcA: Prepare call to Service B
    SvcA->>SC: get current serviceInstance
    SC-->>SvcA: serviceInstance.serviceId = "gateway-svc"
    SvcA->>RST: addTopology(producerNs="production", producerServiceId="order-svc")
    RST->>RST: consumerName = "gateway-svc"
    RST->>RST: producerName = "order-svc"
    RST->>RST: Check self-dependency (skip if same)
    RST->>Redis: EVAL service_topology_add.lua KEYS=["production"]
    Redis->>Redis: HSETNX production:topology_idx gateway-svc {timestamp}
    Redis->>Redis: HSETNX production:topology:gateway-svc order-svc {timestamp}
    Redis-->>RST: (void)
    RST-->>SvcA: Mono~Void~

Redis Key Patterns

Redis KeyTypePurposeLua Script
{namespace}:topology_idxHASHMaps consumer service names to first-seen timestampsservice_topology_add.lua
{namespace}:topology:{consumer}HASHMaps producer service names to first-seen timestamps for a given consumerservice_topology_add.lua

Cross-namespace producer names are formatted as {producerNamespace}.{producerServiceId} by ServiceTopology.getProducerName. This allows the topology graph to distinguish between same-name services in different namespaces.

Service Stat Integration

The ServiceStat data class pairs with the topology view to show per-service instance counts. The RedisServiceStatistic maintains a {namespace}:svc_stat hash where each entry maps a serviceId to its instanceCount.

Statistics are automatically recalculated when instance events occur (excluding renewals), driven by the Lua service_stat.lua script. This integration allows the dashboard to display both the dependency graph and the health/load of each node in the topology.

mermaid
flowchart TD
    subgraph ProdNS["Production Namespace"]
        GW["gateway-svc<br>instances: 3"]
        OS["order-svc<br>instances: 5"]
        US["user-svc<br>instances: 2"]
        PS["payment-svc<br>instances: 4"]
    end

    GW -->|"depends on"| OS
    GW -->|"depends on"| US
    OS -->|"depends on"| PS
    OS -->|"depends on"| US

    style GW fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style OS fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style US fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style PS fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ProdNS fill:#161b22,stroke:#30363d,color:#e6edf3

Dashboard Integration

The topology data powers the CoSky dashboard's service topology visualization. The dashboard calls getTopology(namespace) to retrieve the full dependency graph and getServiceStats(namespace) to display instance counts per service. Together, these produce an interactive graph showing:

  • Service nodes with their instance counts
  • Directed edges representing call dependencies
  • Cross-namespace relationships (shown as {namespace}.{serviceId})

Class Diagram

mermaid
classDiagram
    class ServiceTopology {
        <<interface>>
        +addTopology(producerNamespace, producerServiceId) Mono~Void~
        +getTopology(namespace) Mono~Map~
        +consumerName$ String
        +getProducerName$(ns, serviceId) String
        +NO_OP$ ServiceTopology
    }
    class RedisServiceTopology {
        -redisTemplate: ReactiveStringRedisTemplate
        +addTopology(producerNamespace, producerServiceId) Mono~Void~
        +getTopology(namespace) Mono~Map~
    }
    class ServiceInstanceContext {
        <<object>>
        +serviceInstance: ServiceInstance
        +namespace: String
    }
    class ServiceStat {
        +serviceId: String
        +instanceCount: Int
    }
    class ServiceStatistic {
        <<interface>>
        +statService(namespace) Mono~Void~
        +statService(namespace, serviceId) Mono~Void~
        +getServiceStats(namespace) Flux~ServiceStat~
        +getInstanceCount(namespace) Mono~Long~
    }
    class RedisServiceStatistic {
        -redisTemplate: ReactiveStringRedisTemplate
        -instanceEventListenerContainer
        +statService(namespace) Mono~Void~
        +getServiceStats(namespace) Flux~ServiceStat~
        +getInstanceCount(namespace) Mono~Long~
    }

    ServiceTopology <|.. RedisServiceTopology : implements
    ServiceStatistic <|.. RedisServiceStatistic : implements
    RedisServiceTopology --> ServiceInstanceContext : reads consumerName
    RedisServiceStatistic --> ServiceStat : produces

References

Released under the Apache License 2.0.