1111use std:: sync:: Arc ;
1212use std:: time:: { SystemTime , UNIX_EPOCH } ;
1313
14- use void_box:: observe:: ObserveConfig ;
14+ use void_box:: observe:: { flush_global_otel , ObserveConfig } ;
1515use void_box:: sandbox:: Sandbox ;
1616use void_box:: workflow:: { Workflow , WorkflowExt } ;
1717
1818#[ tokio:: main]
1919async fn main ( ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
20+ let started_at_ms = now_ms ( ) ;
2021 let run_id = SystemTime :: now ( )
2122 . duration_since ( UNIX_EPOCH )
2223 . unwrap_or_default ( )
@@ -60,6 +61,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
6061 observe. logs . in_memory = true ;
6162
6263 let observed = workflow. observe ( observe) . run_in ( sandbox) . await ?;
64+ let ended_at_ms = now_ms ( ) ;
65+
66+ if let Err ( e) = flush_global_otel ( ) {
67+ eprintln ! ( "[playground] WARN: failed to flush OTLP exporters: {e}" ) ;
68+ }
69+
70+ let grafana_base = std:: env:: var ( "PLAYGROUND_GRAFANA_URL" )
71+ . unwrap_or_else ( |_| "http://localhost:3000" . to_string ( ) ) ;
72+ let service_name =
73+ std:: env:: var ( "VOIDBOX_SERVICE_NAME" ) . unwrap_or_else ( |_| "void-box-playground" . into ( ) ) ;
74+ let workflow_span = format ! ( "workflow:{workflow_name}" ) ;
6375
6476 println ! ( "=== Playground Pipeline ===" ) ;
6577 println ! ( "workflow: {}" , workflow_name) ;
@@ -73,16 +85,104 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
7385
7486 println ! ( ) ;
7587 println ! ( "=== Explore in Grafana ===" ) ;
76- println ! ( "Grafana: http://localhost:3000" ) ;
88+ println ! ( "Grafana: {}" , grafana_base) ;
89+ println ! ( "Service: {}" , service_name) ;
90+ println ! ( "Workflow span: {}" , workflow_span) ;
91+ println ! (
92+ "Traces URL: {}" ,
93+ grafana_trace_url(
94+ & grafana_base,
95+ & service_name,
96+ & workflow_span,
97+ started_at_ms,
98+ ended_at_ms
99+ )
100+ ) ;
77101 println ! (
78- "Service : {}" ,
79- std :: env :: var ( "VOIDBOX_SERVICE_NAME" ) . unwrap_or_else ( |_| "void-box-playground" . into ( ) )
102+ "Metrics URL : {}" ,
103+ grafana_metrics_url ( & grafana_base , & service_name , started_at_ms , ended_at_ms )
80104 ) ;
81- println ! ( "Workflow span: workflow:{}" , workflow_name) ;
105+ if let Ok ( log_path) = std:: env:: var ( "PLAYGROUND_LOG_PATH" ) {
106+ if !log_path. is_empty ( ) {
107+ println ! ( "Logs (local): {}" , log_path) ;
108+ }
109+ }
82110
83111 Ok ( ( ) )
84112}
85113
114+ fn now_ms ( ) -> u64 {
115+ SystemTime :: now ( )
116+ . duration_since ( UNIX_EPOCH )
117+ . unwrap_or_default ( )
118+ . as_millis ( ) as u64
119+ }
120+
121+ fn grafana_trace_url (
122+ grafana_base : & str ,
123+ service_name : & str ,
124+ workflow_span : & str ,
125+ from_ms : u64 ,
126+ to_ms : u64 ,
127+ ) -> String {
128+ let query = format ! (
129+ "{{ resource.service.name = \" {}\" && name = \" {}\" }}" ,
130+ service_name, workflow_span
131+ ) ;
132+ let left = format ! (
133+ "[{}, {}, \" tempo\" , {{\" queryType\" :\" traceql\" ,\" query\" :\" {}\" ,\" refId\" :\" A\" }}]" ,
134+ from_ms,
135+ to_ms. saturating_add( 1000 ) ,
136+ escape_json_string( & query) ,
137+ ) ;
138+
139+ format ! (
140+ "{}/explore?orgId=1&left={}" ,
141+ grafana_base. trim_end_matches( '/' ) ,
142+ percent_encode( & left)
143+ )
144+ }
145+
146+ fn grafana_metrics_url ( grafana_base : & str , service_name : & str , from_ms : u64 , to_ms : u64 ) -> String {
147+ let _ = service_name;
148+ let expr = String :: from (
149+ "sum by (__name__) ({__name__=~\" (ingest|normalize|score)_duration_ms(_bucket|_sum|_count)?\" })" ,
150+ ) ;
151+ let left = format ! (
152+ "[{}, {}, \" prometheus\" , {{\" refId\" :\" A\" ,\" expr\" :\" {}\" }}]" ,
153+ from_ms,
154+ to_ms. saturating_add( 1000 ) ,
155+ escape_json_string( & expr)
156+ ) ;
157+
158+ format ! (
159+ "{}/explore?orgId=1&left={}" ,
160+ grafana_base. trim_end_matches( '/' ) ,
161+ percent_encode( & left)
162+ )
163+ }
164+
165+ fn escape_json_string ( input : & str ) -> String {
166+ input. replace ( '\\' , "\\ \\ " ) . replace ( '"' , "\\ \" " )
167+ }
168+
169+ fn percent_encode ( input : & str ) -> String {
170+ let mut encoded = String :: with_capacity ( input. len ( ) * 3 / 2 ) ;
171+ for b in input. bytes ( ) {
172+ match b {
173+ b'A' ..=b'Z' | b'a' ..=b'z' | b'0' ..=b'9' | b'-' | b'_' | b'.' | b'~' => {
174+ encoded. push ( char:: from ( b) ) ;
175+ }
176+ _ => {
177+ encoded. push ( '%' ) ;
178+ encoded. push ( char:: from ( b"0123456789ABCDEF" [ ( b >> 4 ) as usize ] ) ) ;
179+ encoded. push ( char:: from ( b"0123456789ABCDEF" [ ( b & 0x0F ) as usize ] ) ) ;
180+ }
181+ }
182+ }
183+ encoded
184+ }
185+
86186fn build_sandbox ( ) -> Result < Arc < Sandbox > , Box < dyn std:: error:: Error > > {
87187 let has_kvm = std:: path:: Path :: new ( "/dev/kvm" ) . exists ( ) ;
88188 let has_kernel = std:: env:: var ( "VOID_BOX_KERNEL" )
0 commit comments