@@ -14,6 +14,7 @@ use std::path::PathBuf;
14
14
use tempfile:: NamedTempFile ;
15
15
use tokio:: fs;
16
16
use tokio:: process:: Command ;
17
+ use tracing:: { debug, error, info, instrument, warn} ;
17
18
18
19
/// Data structure containing information needed to generate an OpenGraph image
19
20
/// for a crates.io crate.
@@ -108,20 +109,27 @@ impl OgImageGenerator {
108
109
/// let generator = OgImageGenerator::from_environment()?;
109
110
/// # Ok::<(), crates_io_og_image::OgImageError>(())
110
111
/// ```
112
+ #[ instrument]
111
113
pub fn from_environment ( ) -> Result < Self , OgImageError > {
112
114
let typst_path = var ( "TYPST_PATH" ) . map_err ( OgImageError :: EnvVarError ) ?;
113
115
let font_path = var ( "TYPST_FONT_PATH" ) . map_err ( OgImageError :: EnvVarError ) ?;
114
116
115
- let mut generator = if let Some ( path) = typst_path {
117
+ let mut generator = if let Some ( ref path) = typst_path {
118
+ debug ! ( typst_path = %path, "Using custom Typst binary path from environment" ) ;
116
119
Self :: new ( PathBuf :: from ( path) )
117
120
} else {
121
+ debug ! ( "Using default Typst binary path (assumes 'typst' in PATH)" ) ;
118
122
Self :: default ( )
119
123
} ;
120
124
121
- if let Some ( font_path) = font_path {
125
+ if let Some ( ref font_path) = font_path {
126
+ debug ! ( font_path = %font_path, "Setting custom font path from environment" ) ;
122
127
let current_dir = std:: env:: current_dir ( ) ?;
123
128
let font_path = current_dir. join ( font_path) . canonicalize ( ) ?;
129
+ debug ! ( resolved_font_path = %font_path. display( ) , "Resolved font path" ) ;
124
130
generator = generator. with_font_path ( font_path) ;
131
+ } else {
132
+ debug ! ( "No custom font path specified, using Typst default font discovery" ) ;
125
133
}
126
134
127
135
Ok ( generator)
@@ -153,6 +161,7 @@ impl OgImageGenerator {
153
161
/// This method handles both asset-based avatars (which are copied from the bundled assets)
154
162
/// and URL-based avatars (which are downloaded from the internet).
155
163
/// Returns a mapping from avatar source to the local filename.
164
+ #[ instrument( skip( self , data) , fields( crate . name = %data. name) ) ]
156
165
async fn process_avatars < ' a > (
157
166
& self ,
158
167
data : & ' a OgImageData < ' _ > ,
@@ -166,34 +175,71 @@ impl OgImageGenerator {
166
175
let filename = format ! ( "avatar_{index}.png" ) ;
167
176
let avatar_path = assets_dir. join ( & filename) ;
168
177
178
+ debug ! (
179
+ author_name = %author. name,
180
+ avatar_url = %avatar,
181
+ avatar_path = %avatar_path. display( ) ,
182
+ "Processing avatar for author {}" , author. name
183
+ ) ;
184
+
169
185
// Get the bytes either from the included asset or download from URL
170
186
let bytes = if * avatar == "test-avatar" {
187
+ debug ! ( "Using bundled test avatar" ) ;
171
188
// Copy directly from included bytes
172
189
Bytes :: from_static ( include_bytes ! ( "../template/assets/test-avatar.png" ) )
173
190
} else {
191
+ debug ! ( url = %avatar, "Downloading avatar from URL: {avatar}" ) ;
174
192
// Download the avatar from the URL
175
- let response = client. get ( * avatar) . send ( ) . await . map_err ( |err| {
193
+ let response = client
194
+ . get ( * avatar)
195
+ . send ( )
196
+ . await
197
+ . map_err ( |err| OgImageError :: AvatarDownloadError {
198
+ url : avatar. to_string ( ) ,
199
+ source : err,
200
+ } ) ?
201
+ . error_for_status ( )
202
+ . map_err ( |err| OgImageError :: AvatarDownloadError {
203
+ url : avatar. to_string ( ) ,
204
+ source : err,
205
+ } ) ?;
206
+
207
+ let content_length = response. content_length ( ) ;
208
+ debug ! (
209
+ url = %avatar,
210
+ content_length = ?content_length,
211
+ status = %response. status( ) ,
212
+ "Avatar download response received"
213
+ ) ;
214
+
215
+ let bytes = response. bytes ( ) . await ;
216
+ let bytes = bytes. map_err ( |err| {
217
+ error ! ( url = %avatar, error = %err, "Failed to read avatar response bytes" ) ;
176
218
OgImageError :: AvatarDownloadError {
177
219
url : ( * avatar) . to_string ( ) ,
178
220
source : err,
179
221
}
180
222
} ) ?;
181
223
182
- let bytes = response. bytes ( ) . await ;
183
- bytes. map_err ( |err| OgImageError :: AvatarDownloadError {
184
- url : ( * avatar) . to_string ( ) ,
185
- source : err,
186
- } ) ?
224
+ debug ! ( url = %avatar, size_bytes = bytes. len( ) , "Avatar downloaded successfully" ) ;
225
+ bytes
187
226
} ;
188
227
189
228
// Write the bytes to the avatar file
190
- fs:: write ( & avatar_path, bytes) . await . map_err ( |err| {
229
+ fs:: write ( & avatar_path, & bytes) . await . map_err ( |err| {
191
230
OgImageError :: AvatarWriteError {
192
231
path : avatar_path. clone ( ) ,
193
232
source : err,
194
233
}
195
234
} ) ?;
196
235
236
+ debug ! (
237
+ author_name = %author. name,
238
+ path = %avatar_path. display( ) ,
239
+ size_bytes = bytes. len( ) ,
240
+ "Avatar processed and written successfully"
241
+ ) ;
242
+
197
243
// Store the mapping from the avatar source to the numbered filename
198
244
avatar_map. insert ( * avatar, filename) ;
199
245
}
@@ -232,19 +278,32 @@ impl OgImageGenerator {
232
278
/// # Ok(())
233
279
/// # }
234
280
/// ```
281
+ #[ instrument( skip( self , data) , fields(
282
+ crate . name = %data. name,
283
+ crate . version = %data. version,
284
+ author_count = data. authors. len( ) ,
285
+ ) ) ]
235
286
pub async fn generate ( & self , data : OgImageData < ' _ > ) -> Result < NamedTempFile , OgImageError > {
287
+ let start_time = std:: time:: Instant :: now ( ) ;
288
+ info ! ( "Starting OpenGraph image generation" ) ;
289
+
236
290
// Create a temporary folder
237
291
let temp_dir = tempfile:: tempdir ( ) . map_err ( OgImageError :: TempDirError ) ?;
292
+ debug ! ( temp_dir = %temp_dir. path( ) . display( ) , "Created temporary directory" ) ;
238
293
239
294
// Create assets directory and copy logo and icons
240
295
let assets_dir = temp_dir. path ( ) . join ( "assets" ) ;
296
+ debug ! ( assets_dir = %assets_dir. display( ) , "Creating assets directory" ) ;
241
297
fs:: create_dir ( & assets_dir) . await ?;
298
+
299
+ debug ! ( "Copying bundled assets to temporary directory" ) ;
242
300
let cargo_logo = include_bytes ! ( "../template/assets/cargo.png" ) ;
243
301
fs:: write ( assets_dir. join ( "cargo.png" ) , cargo_logo) . await ?;
244
302
let rust_logo_svg = include_bytes ! ( "../template/assets/rust-logo.svg" ) ;
245
303
fs:: write ( assets_dir. join ( "rust-logo.svg" ) , rust_logo_svg) . await ?;
246
304
247
305
// Copy SVG icons
306
+ debug ! ( "Copying SVG icon assets" ) ;
248
307
let code_branch_svg = include_bytes ! ( "../template/assets/code-branch.svg" ) ;
249
308
fs:: write ( assets_dir. join ( "code-branch.svg" ) , code_branch_svg) . await ?;
250
309
let code_svg = include_bytes ! ( "../template/assets/code.svg" ) ;
@@ -257,24 +316,36 @@ impl OgImageGenerator {
257
316
fs:: write ( assets_dir. join ( "weight-hanging.svg" ) , weight_hanging_svg) . await ?;
258
317
259
318
// Process avatars - download URLs and copy assets
319
+ let avatar_start_time = std:: time:: Instant :: now ( ) ;
320
+ info ! ( "Processing avatars" ) ;
260
321
let avatar_map = self . process_avatars ( & data, & assets_dir) . await ?;
322
+ let avatar_duration = avatar_start_time. elapsed ( ) ;
323
+ info ! (
324
+ avatar_count = avatar_map. len( ) ,
325
+ duration_ms = avatar_duration. as_millis( ) ,
326
+ "Avatar processing completed"
327
+ ) ;
261
328
262
329
// Copy the static Typst template file
263
330
let template_content = include_str ! ( "../template/og-image.typ" ) ;
264
331
let typ_file_path = temp_dir. path ( ) . join ( "og-image.typ" ) ;
332
+ debug ! ( template_path = %typ_file_path. display( ) , "Copying Typst template" ) ;
265
333
fs:: write ( & typ_file_path, template_content) . await ?;
266
334
267
335
// Create a named temp file for the output PNG
268
336
let output_file = NamedTempFile :: new ( ) . map_err ( OgImageError :: TempFileError ) ?;
337
+ debug ! ( output_path = %output_file. path( ) . display( ) , "Created output file" ) ;
269
338
270
339
// Serialize data and avatar_map to JSON
271
- let json_data = serde_json:: to_string ( & data) ;
272
- let json_data = json_data. map_err ( OgImageError :: JsonSerializationError ) ?;
340
+ debug ! ( "Serializing data and avatar map to JSON" ) ;
341
+ let json_data =
342
+ serde_json:: to_string ( & data) . map_err ( OgImageError :: JsonSerializationError ) ?;
273
343
274
- let json_avatar_map = serde_json :: to_string ( & avatar_map ) ;
275
- let json_avatar_map = json_avatar_map . map_err ( OgImageError :: JsonSerializationError ) ?;
344
+ let json_avatar_map =
345
+ serde_json :: to_string ( & avatar_map ) . map_err ( OgImageError :: JsonSerializationError ) ?;
276
346
277
347
// Run typst compile command with input data
348
+ info ! ( "Running Typst compilation command" ) ;
278
349
let mut command = Command :: new ( & self . typst_binary_path ) ;
279
350
command. arg ( "compile" ) . arg ( "--format" ) . arg ( "png" ) ;
280
351
@@ -286,8 +357,11 @@ impl OgImageGenerator {
286
357
287
358
// Pass in the font path if specified
288
359
if let Some ( font_path) = & self . typst_font_path {
360
+ debug ! ( font_path = %font_path. display( ) , "Using custom font path" ) ;
289
361
command. arg ( "--font-path" ) . arg ( font_path) ;
290
362
command. arg ( "--ignore-system-fonts" ) ;
363
+ } else {
364
+ debug ! ( "Using system font discovery" ) ;
291
365
}
292
366
293
367
// Pass input and output file paths
@@ -304,19 +378,41 @@ impl OgImageGenerator {
304
378
command. env ( "HOME" , home) ;
305
379
}
306
380
381
+ let compilation_start_time = std:: time:: Instant :: now ( ) ;
307
382
let output = command. output ( ) . await ;
308
383
let output = output. map_err ( OgImageError :: TypstNotFound ) ?;
384
+ let compilation_duration = compilation_start_time. elapsed ( ) ;
309
385
310
386
if !output. status . success ( ) {
311
387
let stderr = String :: from_utf8_lossy ( & output. stderr ) . to_string ( ) ;
312
388
let stdout = String :: from_utf8_lossy ( & output. stdout ) . to_string ( ) ;
389
+ error ! (
390
+ exit_code = ?output. status. code( ) ,
391
+ stderr = %stderr,
392
+ stdout = %stdout,
393
+ duration_ms = compilation_duration. as_millis( ) ,
394
+ "Typst compilation failed"
395
+ ) ;
313
396
return Err ( OgImageError :: TypstCompilationError {
314
397
stderr,
315
398
stdout,
316
399
exit_code : output. status . code ( ) ,
317
400
} ) ;
318
401
}
319
402
403
+ let output_size_bytes = fs:: metadata ( output_file. path ( ) ) . await ;
404
+ let output_size_bytes = output_size_bytes. map ( |m| m. len ( ) ) . unwrap_or ( 0 ) ;
405
+
406
+ debug ! (
407
+ duration_ms = compilation_duration. as_millis( ) ,
408
+ output_size_bytes, "Typst compilation completed successfully"
409
+ ) ;
410
+
411
+ let duration = start_time. elapsed ( ) ;
412
+ info ! (
413
+ duration_ms = duration. as_millis( ) ,
414
+ output_size_bytes, "OpenGraph image generation completed successfully"
415
+ ) ;
320
416
Ok ( output_file)
321
417
}
322
418
}
@@ -332,6 +428,19 @@ impl Default for OgImageGenerator {
332
428
#[ cfg( test) ]
333
429
mod tests {
334
430
use super :: * ;
431
+ use tracing:: dispatcher:: DefaultGuard ;
432
+ use tracing:: { Level , subscriber} ;
433
+ use tracing_subscriber:: fmt;
434
+
435
+ fn init_tracing ( ) -> DefaultGuard {
436
+ let subscriber = fmt ( )
437
+ . compact ( )
438
+ . with_max_level ( Level :: DEBUG )
439
+ . with_test_writer ( )
440
+ . finish ( ) ;
441
+
442
+ subscriber:: set_default ( subscriber)
443
+ }
335
444
336
445
const fn author ( name : & str ) -> OgImageAuthorData < ' _ > {
337
446
OgImageAuthorData :: new ( name, None )
@@ -464,6 +573,7 @@ mod tests {
464
573
465
574
#[ tokio:: test]
466
575
async fn test_generate_og_image_snapshot ( ) {
576
+ let _guard = init_tracing ( ) ;
467
577
let data = create_simple_test_data ( ) ;
468
578
469
579
if let Some ( image_data) = generate_image ( data) . await {
@@ -473,6 +583,7 @@ mod tests {
473
583
474
584
#[ tokio:: test]
475
585
async fn test_generate_og_image_overflow_snapshot ( ) {
586
+ let _guard = init_tracing ( ) ;
476
587
let data = create_overflow_test_data ( ) ;
477
588
478
589
if let Some ( image_data) = generate_image ( data) . await {
@@ -482,6 +593,7 @@ mod tests {
482
593
483
594
#[ tokio:: test]
484
595
async fn test_generate_og_image_minimal_snapshot ( ) {
596
+ let _guard = init_tracing ( ) ;
485
597
let data = create_minimal_test_data ( ) ;
486
598
487
599
if let Some ( image_data) = generate_image ( data) . await {
@@ -491,6 +603,7 @@ mod tests {
491
603
492
604
#[ tokio:: test]
493
605
async fn test_generate_og_image_escaping_snapshot ( ) {
606
+ let _guard = init_tracing ( ) ;
494
607
let data = create_escaping_test_data ( ) ;
495
608
496
609
if let Some ( image_data) = generate_image ( data) . await {
0 commit comments