mirror of
https://github.com/katanemo/plano.git
synced 2026-04-25 00:36:34 +02:00
add listener routes for internal service proxying (#793)
This commit is contained in:
parent
198c912202
commit
c2480639b2
13 changed files with 219 additions and 2 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
""",
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -194,6 +194,7 @@ mod tests {
|
|||
Listener {
|
||||
name: name.to_string(),
|
||||
agents: Some(agents),
|
||||
routes: None,
|
||||
port: 8080,
|
||||
router: None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||

|
||||
|
|
|
|||
|
|
@ -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: |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
------------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue