Skip to content

Commit f2ba58e

Browse files
authored
feat: add Rust-to-Python const code generator for prometheus_names.py (#3425)
Signed-off-by: Keiven Chang <[email protected]>
1 parent ca67409 commit f2ba58e

File tree

15 files changed

+779
-614
lines changed

15 files changed

+779
-614
lines changed

Cargo.lock

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ members = [
1010
"lib/async-openai",
1111
"lib/parsers",
1212
"lib/bindings/c",
13+
"lib/bindings/python/codegen",
1314
"lib/engines/*",
1415
]
1516
# Exclude certain packages that are slow to build and we don't ship as flagship

components/src/dynamo/planner/utils/prometheus.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from prometheus_api_client import PrometheusConnect
2020
from pydantic import BaseModel, ValidationError
2121

22-
from dynamo._core import prometheus_names
22+
from dynamo import prometheus_names
2323
from dynamo.runtime.logging import configure_dynamo_logging
2424

2525
configure_dynamo_logging()
@@ -94,23 +94,23 @@ def _get_average_metric(
9494

9595
def get_avg_inter_token_latency(self, interval: str, model_name: str):
9696
return self._get_average_metric(
97-
prometheus_names.frontend.inter_token_latency_seconds,
97+
prometheus_names.frontend_service.INTER_TOKEN_LATENCY_SECONDS,
9898
interval,
9999
"avg inter token latency",
100100
model_name,
101101
)
102102

103103
def get_avg_time_to_first_token(self, interval: str, model_name: str):
104104
return self._get_average_metric(
105-
prometheus_names.frontend.time_to_first_token_seconds,
105+
prometheus_names.frontend_service.TIME_TO_FIRST_TOKEN_SECONDS,
106106
interval,
107107
"avg time to first token",
108108
model_name,
109109
)
110110

111111
def get_avg_request_duration(self, interval: str, model_name: str):
112112
return self._get_average_metric(
113-
prometheus_names.frontend.request_duration_seconds,
113+
prometheus_names.frontend_service.REQUEST_DURATION_SECONDS,
114114
interval,
115115
"avg request duration",
116116
model_name,
@@ -119,7 +119,7 @@ def get_avg_request_duration(self, interval: str, model_name: str):
119119
def get_avg_request_count(self, interval: str, model_name: str):
120120
# This function follows a different query pattern than the other metrics
121121
try:
122-
requests_total_metric = prometheus_names.frontend.requests_total
122+
requests_total_metric = prometheus_names.frontend_service.REQUESTS_TOTAL
123123
raw_res = self.prom.custom_query(
124124
query=f"increase({requests_total_metric}[{interval}])"
125125
)
@@ -138,15 +138,15 @@ def get_avg_request_count(self, interval: str, model_name: str):
138138

139139
def get_avg_input_sequence_tokens(self, interval: str, model_name: str):
140140
return self._get_average_metric(
141-
prometheus_names.frontend.input_sequence_tokens,
141+
prometheus_names.frontend_service.INPUT_SEQUENCE_TOKENS,
142142
interval,
143143
"avg input sequence tokens",
144144
model_name,
145145
)
146146

147147
def get_avg_output_sequence_tokens(self, interval: str, model_name: str):
148148
return self._get_average_metric(
149-
prometheus_names.frontend.output_sequence_tokens,
149+
prometheus_names.frontend_service.OUTPUT_SEQUENCE_TOKENS,
150150
interval,
151151
"avg output sequence tokens",
152152
model_name,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
[package]
5+
name = "dynamo-codegen"
6+
version = "0.1.0"
7+
edition = "2021"
8+
license = "Apache-2.0"
9+
10+
[dependencies]
11+
syn = { version = "2.0", features = ["full", "extra-traits"] }
12+
quote = "1.0"
13+
proc-macro2 = "1.0"
14+
anyhow = "1.0"
15+
16+
[[bin]]
17+
name = "gen-python-prometheus-names"
18+
path = "src/gen_python_prometheus_names.rs"
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Dynamo Codegen
2+
3+
Python code generator for Dynamo Python bindings.
4+
5+
## gen-python-prometheus-names
6+
7+
Generates `prometheus_names.py` from Rust source `lib/runtime/src/metrics/prometheus_names.rs`.
8+
9+
### Usage
10+
11+
```bash
12+
cargo run -p dynamo-codegen --bin gen-python-prometheus-names
13+
```
14+
15+
### What it does
16+
17+
- Parses Rust AST from `lib/runtime/src/metrics/prometheus_names.rs`
18+
- Generates Python classes with constants at `lib/bindings/python/src/dynamo/prometheus_names.py`
19+
- Handles macro-generated constants (e.g., `kvstats_name!("active_blocks")``"kvstats_active_blocks"`)
20+
21+
### Example
22+
23+
**Rust input:**
24+
```rust
25+
pub mod kvstats {
26+
pub const ACTIVE_BLOCKS: &str = kvstats_name!("active_blocks");
27+
}
28+
```
29+
30+
**Python output:**
31+
```python
32+
class kvstats:
33+
ACTIVE_BLOCKS = "kvstats_active_blocks"
34+
```
35+
36+
### When to run
37+
38+
Run after modifying `lib/runtime/src/metrics/prometheus_names.rs` to regenerate the Python file.
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Binary to generate Python prometheus_names from Rust source
5+
6+
use anyhow::{Context, Result};
7+
use dynamo_codegen::prometheus_parser::{ModuleDef, PrometheusParser};
8+
use std::collections::HashMap;
9+
use std::path::PathBuf;
10+
11+
/// Generates Python module code from parsed Rust prometheus_names modules.
12+
/// Converts Rust const declarations into Python class attributes with deterministic ordering.
13+
struct PythonGenerator<'a> {
14+
modules: &'a HashMap<String, ModuleDef>,
15+
}
16+
17+
impl<'a> PythonGenerator<'a> {
18+
fn new(parser: &'a PrometheusParser) -> Self {
19+
Self {
20+
modules: &parser.modules,
21+
}
22+
}
23+
24+
fn load_template(template_name: &str) -> String {
25+
let template_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
26+
.join("templates")
27+
.join(template_name);
28+
29+
std::fs::read_to_string(&template_path)
30+
.unwrap_or_else(|_| panic!("Failed to read template: {}", template_path.display()))
31+
}
32+
33+
fn generate_python_file(&self) -> String {
34+
let mut output = Self::load_template("prometheus_names.py.template");
35+
36+
// Append generated classes
37+
output.push_str(&self.generate_classes());
38+
39+
output
40+
}
41+
42+
fn generate_classes(&self) -> String {
43+
let mut lines = Vec::new();
44+
45+
// Sort module names to ensure deterministic output
46+
let mut module_names: Vec<&String> = self.modules.keys().collect();
47+
module_names.sort();
48+
49+
// Generate simple classes with constants as class attributes
50+
for module_name in module_names {
51+
let module = &self.modules[module_name];
52+
lines.push(format!("class {}:", module_name));
53+
54+
// Use doc comment from module if available
55+
if !module.doc_comment.is_empty() {
56+
let first_line = module.doc_comment.lines().next().unwrap_or("").trim();
57+
if !first_line.is_empty() {
58+
lines.push(format!(" \"\"\"{}\"\"\"", first_line));
59+
}
60+
}
61+
lines.push("".to_string());
62+
63+
for constant in &module.constants {
64+
if !constant.doc_comment.is_empty() {
65+
for comment_line in constant.doc_comment.lines() {
66+
lines.push(format!(" # {}", comment_line));
67+
}
68+
}
69+
lines.push(format!(" {} = \"{}\"", constant.name, constant.value));
70+
}
71+
72+
lines.push("".to_string());
73+
}
74+
75+
lines.join("\n")
76+
}
77+
}
78+
79+
fn main() -> Result<()> {
80+
let args: Vec<String> = std::env::args().collect();
81+
82+
let mut source_path: Option<PathBuf> = None;
83+
let mut output_path: Option<PathBuf> = None;
84+
85+
let mut i = 1;
86+
while i < args.len() {
87+
match args[i].as_str() {
88+
"--source" => {
89+
i += 1;
90+
if i < args.len() {
91+
source_path = Some(PathBuf::from(&args[i]));
92+
}
93+
}
94+
"--output" => {
95+
i += 1;
96+
if i < args.len() {
97+
output_path = Some(PathBuf::from(&args[i]));
98+
}
99+
}
100+
"--help" | "-h" => {
101+
print_usage();
102+
return Ok(());
103+
}
104+
_ => {
105+
eprintln!("Unknown argument: {}", args[i]);
106+
print_usage();
107+
std::process::exit(1);
108+
}
109+
}
110+
i += 1;
111+
}
112+
113+
// Determine paths relative to codegen directory
114+
let codegen_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
115+
116+
let source = source_path.unwrap_or_else(|| {
117+
// From: lib/bindings/python/codegen
118+
// To: lib/runtime/src/metrics/prometheus_names.rs
119+
codegen_dir
120+
.join("../../../runtime/src/metrics/prometheus_names.rs")
121+
.canonicalize()
122+
.expect("Failed to resolve source path")
123+
});
124+
125+
let output = output_path.unwrap_or_else(|| {
126+
// From: lib/bindings/python/codegen
127+
// To: lib/bindings/python/src/dynamo/prometheus_names.py
128+
codegen_dir
129+
.join("../src/dynamo/prometheus_names.py")
130+
.canonicalize()
131+
.unwrap_or_else(|_| {
132+
// If file doesn't exist yet, resolve the parent directory
133+
let dir = codegen_dir
134+
.join("../src/dynamo")
135+
.canonicalize()
136+
.expect("Failed to resolve output directory");
137+
dir.join("prometheus_names.py")
138+
})
139+
});
140+
141+
println!("Generating Python prometheus_names from Rust source");
142+
println!("Source: {}", source.display());
143+
println!("Output: {}", output.display());
144+
println!();
145+
146+
let content = std::fs::read_to_string(&source)
147+
.with_context(|| format!("Failed to read source file: {}", source.display()))?;
148+
149+
println!("Parsing Rust AST...");
150+
let parser = PrometheusParser::parse_file(&content)?;
151+
152+
println!("Found {} modules:", parser.modules.len());
153+
let mut module_names: Vec<&String> = parser.modules.keys().collect();
154+
module_names.sort();
155+
for name in module_names.iter() {
156+
let module = &parser.modules[name.as_str()];
157+
println!(
158+
" - {}: {} constants{}",
159+
name,
160+
module.constants.len(),
161+
if module.is_macro_generated {
162+
" (macro-generated)"
163+
} else {
164+
""
165+
}
166+
);
167+
}
168+
169+
println!("\nGenerating Python prometheus_names module...");
170+
let generator = PythonGenerator::new(&parser);
171+
let python_code = generator.generate_python_file();
172+
173+
// Ensure output directory exists
174+
if let Some(parent) = output.parent() {
175+
std::fs::create_dir_all(parent)
176+
.with_context(|| format!("Failed to create output directory: {}", parent.display()))?;
177+
}
178+
179+
std::fs::write(&output, python_code)
180+
.with_context(|| format!("Failed to write output file: {}", output.display()))?;
181+
182+
println!("✓ Generated Python prometheus_names: {}", output.display());
183+
println!("\nSuccess! Python module ready for import.");
184+
185+
Ok(())
186+
}
187+
188+
fn print_usage() {
189+
println!(
190+
r#"
191+
gen-python-prometheus-names - Generate Python prometheus_names from Rust source
192+
193+
Usage: gen-python-prometheus-names [OPTIONS]
194+
195+
Parses lib/runtime/src/metrics/prometheus_names.rs and generates a pure Python
196+
module with 1:1 constant mappings at lib/bindings/python/src/dynamo/prometheus_names.py
197+
198+
This allows Python code to import Prometheus metric constants without Rust bindings:
199+
from dynamo.prometheus_names import frontend_service, kvstats
200+
201+
OPTIONS:
202+
--source PATH Path to Rust source file
203+
(default: lib/runtime/src/metrics/prometheus_names.rs)
204+
205+
--output PATH Path to Python output file
206+
(default: lib/bindings/python/src/dynamo/prometheus_names.py)
207+
208+
--help, -h Print this help message
209+
210+
EXAMPLES:
211+
# Generate with default paths
212+
cargo run -p dynamo-codegen --bin gen-python-prometheus-names
213+
214+
# Generate with custom output
215+
cargo run -p dynamo-codegen --bin gen-python-prometheus-names -- --output /tmp/test.py
216+
"#
217+
);
218+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Code generation utilities for Dynamo project
5+
//!
6+
//! This crate provides tools to generate code from Rust sources to other languages.
7+
8+
pub mod prometheus_parser;

0 commit comments

Comments
 (0)