add listener routes for internal service proxying (#793)

This commit is contained in:
Adil Hafeez 2026-03-01 23:51:14 -08:00
parent 198c912202
commit c2480639b2
No known key found for this signature in database
GPG key ID: 9B18EF7691369645
13 changed files with 219 additions and 2 deletions

View file

@ -145,6 +145,41 @@ def validate_and_render_schema():
inferred_clusters[name]["port"],
) = get_endpoint_and_port(endpoint, protocol)
# Process routes in listeners and generate clusters for upstream services
for listener in listeners:
for route in listener.get("routes", []):
path_prefix = route.get("path_prefix", "")
upstream = route.get("upstream", "")
if not path_prefix or not upstream:
continue
urlparse_result = urlparse(upstream)
if not urlparse_result.scheme or not urlparse_result.hostname:
raise Exception(
f"Invalid upstream URL '{upstream}' in route for listener '{listener.get('name')}'. "
f"Must be a valid URL with scheme (http/https) and hostname."
)
protocol = urlparse_result.scheme
port = urlparse_result.port
if port is None:
port = 80 if protocol == "http" else 443
sanitized_prefix = (
path_prefix.strip("/").replace("/", "_").replace("-", "_")
)
listener_name = listener.get("name", "unknown").replace(" ", "_")
cluster_name = f"route_{listener_name}_{sanitized_prefix}"
route["cluster_name"] = cluster_name
if cluster_name not in inferred_clusters:
inferred_clusters[cluster_name] = {
"endpoint": urlparse_result.hostname,
"port": port,
"protocol": protocol,
}
print("defined clusters from plano_config.yaml: ", json.dumps(inferred_clusters))
if "prompt_targets" in config_yaml:

View file

@ -289,6 +289,63 @@ llm_providers:
tracing:
random_sampling: 100
""",
},
{
"id": "routes_with_agents",
"expected_error": None,
"plano_config": """
version: v0.3.0
agents:
- id: test_agent
url: http://localhost:10500
listeners:
- type: agent
name: agent_1
port: 8001
routes:
- path_prefix: /traces
upstream: http://localhost:16686
agents:
- id: test_agent
description: a test agent
- type: model
name: model_listener
port: 12000
model_providers:
- model: openai/gpt-4o
access_key: $OPENAI_API_KEY
""",
},
{
"id": "routes_only_listener",
"expected_error": None,
"plano_config": """
version: v0.3.0
listeners:
- type: agent
name: observability
port: 8002
routes:
- path_prefix: /traces
upstream: http://localhost:16686
- path_prefix: /metrics
upstream: http://localhost:9090
- type: model
name: model_listener
port: 12000
model_providers:
- model: openai/gpt-4o
access_key: $OPENAI_API_KEY
""",
},
]

View file

@ -277,7 +277,7 @@ static_resources:
{% for listener in listeners %}
{% if listener.agents %}
{% if listener.agents or listener.routes %}
# agent listeners
- name: {{ listener.name | replace(" ", "_") }}
@ -330,6 +330,17 @@ static_resources:
prefix: "/healthz"
direct_response:
status: 200
{% if listener.routes %}
{% for route in listener.routes %}
- match:
prefix: "{{ route.path_prefix }}"
route:
auto_host_rewrite: true
cluster: {{ route.cluster_name }}
timeout: {{ listener.timeout | default('30s') }}
{% endfor %}
{% endif %}
{% if listener.agents %}
- match:
prefix: "/"
route:
@ -337,6 +348,7 @@ static_resources:
prefix_rewrite: "/agents/"
cluster: bright_staff
timeout: {{ listener.timeout | default('30s') }}
{% endif %}
http_filters:
- name: envoy.filters.http.compressor
typed_config:

View file

@ -93,6 +93,19 @@ properties:
required:
- id
- description
routes:
type: array
items:
type: object
properties:
path_prefix:
type: string
upstream:
type: string
additionalProperties: false
required:
- path_prefix
- upstream
additionalProperties: false
required:
- type

View file

@ -194,6 +194,7 @@ mod tests {
Listener {
name: name.to_string(),
agents: Some(agents),
routes: None,
port: 8080,
router: None,
}

View file

@ -74,6 +74,7 @@ mod tests {
let listener = Listener {
name: "test-listener".to_string(),
agents: Some(vec![agent_pipeline.clone()]),
routes: None,
port: 8080,
router: None,
};

View file

@ -36,11 +36,18 @@ pub struct AgentFilterChain {
pub filter_chain: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListenerRoute {
pub path_prefix: String,
pub upstream: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Listener {
pub name: String,
pub router: Option<String>,
pub agents: Option<Vec<AgentFilterChain>>,
pub routes: Option<Vec<ListenerRoute>>,
pub port: u16,
}

View file

@ -51,6 +51,7 @@ This starts:
- Flight Agent on port 10520
- Open WebUI on port 8080
- Plano Proxy on port 8001
- Jaeger UI accessible at http://localhost:8001/traces (routed through Plano)
### 4. Test the System
@ -105,9 +106,30 @@ Both agents run as Docker containers and communicate with Plano via `host.docker
## Observability
This demo includes full OpenTelemetry (OTel) compatible distributed tracing to monitor and debug agent interactions:
This demo includes full OpenTelemetry (OTel) compatible distributed tracing to monitor and debug agent interactions.
The tracing data provides complete visibility into the multi-agent system, making it easy to identify bottlenecks, debug issues, and optimize performance.
Jaeger UI is accessible through Plano's agent listener using the `routes` config:
```bash
# Access Jaeger UI through the same Plano port
curl http://localhost:8001/traces
```
This is configured in `config.yaml` using the `routes` field on the listener, which proxies `/traces` requests to the Jaeger service:
```yaml
listeners:
- type: agent
name: travel_booking_service
port: 8001
routes:
- path_prefix: /traces
upstream: http://jaeger:16686
agents:
...
```
For more details on setting up and using tracing, see the [Plano Observability documentation](https://docs.planoai.dev/guides/observability/tracing.html).
![alt text](tracing.png)

View file

@ -18,6 +18,9 @@ listeners:
name: travel_booking_service
port: 8001
router: plano_orchestrator_v1
routes:
- path_prefix: /traces
upstream: http://jaeger:16686
agents:
- id: weather_agent
description: |

View file

@ -77,3 +77,33 @@ listener with address, port, and protocol details:
When you start Plano, you specify a listener address/port that you want to bind downstream. Plano also exposes a
predefined internal listener (``127.0.0.1:12000``) that you can use to proxy egress calls originating from your
application to LLMs (API-based or hosted) via prompt targets.
Internal Service Routes
^^^^^^^^^^^^^^^^^^^^^^^
Listeners support an optional ``routes`` block that lets you forward path-prefix traffic to co-located internal
services (for example, a Jaeger tracing UI or a Prometheus dashboard) through the same listener port:
.. code-block:: yaml
listeners:
- type: agent
name: agent_1
port: 8001
routes:
- path_prefix: /traces
upstream: http://jaeger:16686
- path_prefix: /metrics
upstream: http://localhost:9090
agents:
- id: my_agent
description: my agent
With the configuration above, requests to ``http://localhost:8001/traces`` are proxied to the Jaeger UI, while
all other requests are routed to the agent as usual. Each route entry requires:
- ``path_prefix`` — the URL prefix to match (e.g., ``/traces``).
- ``upstream`` — the full URL of the internal service to forward to.
Routes are evaluated before the default agent catch-all, so specific prefixes always take priority. A listener
can also define only ``routes`` (without ``agents``) to act as a lightweight reverse proxy for internal services.

View file

@ -453,6 +453,30 @@ Handle incoming requests:
print(f"Payment service response: {response.content}")
Exposing a Trace UI via Listener Routes
----------------------------------------
If you run a trace collector with a web UI (such as Jaeger) alongside Plano, you can expose it through the same
listener port using the ``routes`` configuration. This avoids publishing extra ports and gives developers a single
endpoint for both agent traffic and trace inspection.
.. code-block:: yaml
listeners:
- type: agent
name: agent_1
port: 8001
routes:
- path_prefix: /traces
upstream: http://jaeger:16686
agents:
- id: my_agent
description: my agent
With this configuration, browsing to ``http://localhost:8001/traces`` opens the Jaeger UI while all other
requests continue to be routed to the agent. See :ref:`plano_overview_listeners` for more details on the
``routes`` block.
Integrating with Tracing Tools
------------------------------

View file

@ -55,6 +55,10 @@ listeners:
port: 8001
router: plano_orchestrator_v1
address: 0.0.0.0
# Routes forward path-prefix traffic to internal services (e.g., Jaeger UI)
routes:
- path_prefix: /traces
upstream: http://jaeger:16686
agents:
- id: rag_agent
description: virtual assistant for retrieval augmented generation tasks

View file

@ -19,6 +19,10 @@ endpoints:
mistral_local:
endpoint: 127.0.0.1
port: 8001
route_travel_booking_service_traces:
endpoint: jaeger
port: 16686
protocol: http
weather_agent:
endpoint: host.docker.internal
port: 10510
@ -36,6 +40,10 @@ listeners:
name: travel_booking_service
port: 8001
router: plano_orchestrator_v1
routes:
- cluster_name: route_travel_booking_service_traces
path_prefix: /traces
upstream: http://jaeger:16686
type: agent
- address: 0.0.0.0
model_providers: