Skip to content

Commit d129157

Browse files
committed
og_image: Add structured tracing for image generation process with performance metrics
1 parent 419c96d commit d129157

File tree

4 files changed

+142
-13
lines changed

4 files changed

+142
-13
lines changed

Cargo.lock

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

crates/crates_io_og_image/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ serde_json = "=1.0.140"
1818
tempfile = "=3.20.0"
1919
thiserror = "=2.0.12"
2020
tokio = { version = "=1.45.1", features = ["process", "fs"] }
21+
tracing = "=0.1.41"
2122

2223
[dev-dependencies]
2324
insta = "=1.43.1"
2425
tokio = { version = "=1.45.1", features = ["macros", "rt-multi-thread"] }
26+
tracing-subscriber = { version = "=0.3.19", features = ["env-filter", "fmt"] }

crates/crates_io_og_image/examples/test_generator.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
use crates_io_og_image::{OgImageAuthorData, OgImageData, OgImageGenerator};
2+
use tracing::level_filters::LevelFilter;
3+
use tracing_subscriber::{EnvFilter, fmt};
4+
5+
fn init_tracing() {
6+
let env_filter = EnvFilter::builder()
7+
.with_default_directive(LevelFilter::DEBUG.into())
8+
.from_env_lossy();
9+
10+
fmt().compact().with_env_filter(env_filter).init();
11+
}
212

313
#[tokio::main]
414
async fn main() -> Result<(), Box<dyn std::error::Error>> {
15+
init_tracing();
16+
517
println!("Testing OgImageGenerator...");
618

719
let generator = OgImageGenerator::from_environment()?;

crates/crates_io_og_image/src/lib.rs

Lines changed: 126 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use std::path::PathBuf;
1414
use tempfile::NamedTempFile;
1515
use tokio::fs;
1616
use tokio::process::Command;
17+
use tracing::{debug, error, info, instrument, warn};
1718

1819
/// Data structure containing information needed to generate an OpenGraph image
1920
/// for a crates.io crate.
@@ -108,20 +109,27 @@ impl OgImageGenerator {
108109
/// let generator = OgImageGenerator::from_environment()?;
109110
/// # Ok::<(), crates_io_og_image::OgImageError>(())
110111
/// ```
112+
#[instrument]
111113
pub fn from_environment() -> Result<Self, OgImageError> {
112114
let typst_path = var("TYPST_PATH").map_err(OgImageError::EnvVarError)?;
113115
let font_path = var("TYPST_FONT_PATH").map_err(OgImageError::EnvVarError)?;
114116

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");
116119
Self::new(PathBuf::from(path))
117120
} else {
121+
debug!("Using default Typst binary path (assumes 'typst' in PATH)");
118122
Self::default()
119123
};
120124

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");
122127
let current_dir = std::env::current_dir()?;
123128
let font_path = current_dir.join(font_path).canonicalize()?;
129+
debug!(resolved_font_path = %font_path.display(), "Resolved font path");
124130
generator = generator.with_font_path(font_path);
131+
} else {
132+
debug!("No custom font path specified, using Typst default font discovery");
125133
}
126134

127135
Ok(generator)
@@ -153,6 +161,7 @@ impl OgImageGenerator {
153161
/// This method handles both asset-based avatars (which are copied from the bundled assets)
154162
/// and URL-based avatars (which are downloaded from the internet).
155163
/// Returns a mapping from avatar source to the local filename.
164+
#[instrument(skip(self, data), fields(crate.name = %data.name))]
156165
async fn process_avatars<'a>(
157166
&self,
158167
data: &'a OgImageData<'_>,
@@ -166,34 +175,71 @@ impl OgImageGenerator {
166175
let filename = format!("avatar_{index}.png");
167176
let avatar_path = assets_dir.join(&filename);
168177

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+
169185
// Get the bytes either from the included asset or download from URL
170186
let bytes = if *avatar == "test-avatar" {
187+
debug!("Using bundled test avatar");
171188
// Copy directly from included bytes
172189
Bytes::from_static(include_bytes!("../template/assets/test-avatar.png"))
173190
} else {
191+
debug!(url = %avatar, "Downloading avatar from URL: {avatar}");
174192
// 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");
176218
OgImageError::AvatarDownloadError {
177219
url: (*avatar).to_string(),
178220
source: err,
179221
}
180222
})?;
181223

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
187226
};
188227

189228
// 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| {
191230
OgImageError::AvatarWriteError {
192231
path: avatar_path.clone(),
193232
source: err,
194233
}
195234
})?;
196235

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+
197243
// Store the mapping from the avatar source to the numbered filename
198244
avatar_map.insert(*avatar, filename);
199245
}
@@ -232,19 +278,32 @@ impl OgImageGenerator {
232278
/// # Ok(())
233279
/// # }
234280
/// ```
281+
#[instrument(skip(self, data), fields(
282+
crate.name = %data.name,
283+
crate.version = %data.version,
284+
author_count = data.authors.len(),
285+
))]
235286
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+
236290
// Create a temporary folder
237291
let temp_dir = tempfile::tempdir().map_err(OgImageError::TempDirError)?;
292+
debug!(temp_dir = %temp_dir.path().display(), "Created temporary directory");
238293

239294
// Create assets directory and copy logo and icons
240295
let assets_dir = temp_dir.path().join("assets");
296+
debug!(assets_dir = %assets_dir.display(), "Creating assets directory");
241297
fs::create_dir(&assets_dir).await?;
298+
299+
debug!("Copying bundled assets to temporary directory");
242300
let cargo_logo = include_bytes!("../template/assets/cargo.png");
243301
fs::write(assets_dir.join("cargo.png"), cargo_logo).await?;
244302
let rust_logo_svg = include_bytes!("../template/assets/rust-logo.svg");
245303
fs::write(assets_dir.join("rust-logo.svg"), rust_logo_svg).await?;
246304

247305
// Copy SVG icons
306+
debug!("Copying SVG icon assets");
248307
let code_branch_svg = include_bytes!("../template/assets/code-branch.svg");
249308
fs::write(assets_dir.join("code-branch.svg"), code_branch_svg).await?;
250309
let code_svg = include_bytes!("../template/assets/code.svg");
@@ -257,24 +316,36 @@ impl OgImageGenerator {
257316
fs::write(assets_dir.join("weight-hanging.svg"), weight_hanging_svg).await?;
258317

259318
// Process avatars - download URLs and copy assets
319+
let avatar_start_time = std::time::Instant::now();
320+
info!("Processing avatars");
260321
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+
);
261328

262329
// Copy the static Typst template file
263330
let template_content = include_str!("../template/og-image.typ");
264331
let typ_file_path = temp_dir.path().join("og-image.typ");
332+
debug!(template_path = %typ_file_path.display(), "Copying Typst template");
265333
fs::write(&typ_file_path, template_content).await?;
266334

267335
// Create a named temp file for the output PNG
268336
let output_file = NamedTempFile::new().map_err(OgImageError::TempFileError)?;
337+
debug!(output_path = %output_file.path().display(), "Created output file");
269338

270339
// 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)?;
273343

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)?;
276346

277347
// Run typst compile command with input data
348+
info!("Running Typst compilation command");
278349
let mut command = Command::new(&self.typst_binary_path);
279350
command.arg("compile").arg("--format").arg("png");
280351

@@ -286,8 +357,11 @@ impl OgImageGenerator {
286357

287358
// Pass in the font path if specified
288359
if let Some(font_path) = &self.typst_font_path {
360+
debug!(font_path = %font_path.display(), "Using custom font path");
289361
command.arg("--font-path").arg(font_path);
290362
command.arg("--ignore-system-fonts");
363+
} else {
364+
debug!("Using system font discovery");
291365
}
292366

293367
// Pass input and output file paths
@@ -304,19 +378,41 @@ impl OgImageGenerator {
304378
command.env("HOME", home);
305379
}
306380

381+
let compilation_start_time = std::time::Instant::now();
307382
let output = command.output().await;
308383
let output = output.map_err(OgImageError::TypstNotFound)?;
384+
let compilation_duration = compilation_start_time.elapsed();
309385

310386
if !output.status.success() {
311387
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
312388
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+
);
313396
return Err(OgImageError::TypstCompilationError {
314397
stderr,
315398
stdout,
316399
exit_code: output.status.code(),
317400
});
318401
}
319402

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+
);
320416
Ok(output_file)
321417
}
322418
}
@@ -332,6 +428,19 @@ impl Default for OgImageGenerator {
332428
#[cfg(test)]
333429
mod tests {
334430
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+
}
335444

336445
const fn author(name: &str) -> OgImageAuthorData<'_> {
337446
OgImageAuthorData::new(name, None)
@@ -464,6 +573,7 @@ mod tests {
464573

465574
#[tokio::test]
466575
async fn test_generate_og_image_snapshot() {
576+
let _guard = init_tracing();
467577
let data = create_simple_test_data();
468578

469579
if let Some(image_data) = generate_image(data).await {
@@ -473,6 +583,7 @@ mod tests {
473583

474584
#[tokio::test]
475585
async fn test_generate_og_image_overflow_snapshot() {
586+
let _guard = init_tracing();
476587
let data = create_overflow_test_data();
477588

478589
if let Some(image_data) = generate_image(data).await {
@@ -482,6 +593,7 @@ mod tests {
482593

483594
#[tokio::test]
484595
async fn test_generate_og_image_minimal_snapshot() {
596+
let _guard = init_tracing();
485597
let data = create_minimal_test_data();
486598

487599
if let Some(image_data) = generate_image(data).await {
@@ -491,6 +603,7 @@ mod tests {
491603

492604
#[tokio::test]
493605
async fn test_generate_og_image_escaping_snapshot() {
606+
let _guard = init_tracing();
494607
let data = create_escaping_test_data();
495608

496609
if let Some(image_data) = generate_image(data).await {

0 commit comments

Comments
 (0)