Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/add-structured-logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@googleworkspace/cli": minor
---

Add opt-in structured HTTP request logging via `tracing`

New environment variables:
- `GOOGLE_WORKSPACE_CLI_LOG`: stderr log filter (e.g., `gws=debug`)
- `GOOGLE_WORKSPACE_CLI_LOG_FILE`: directory for JSON log files with daily rotation

Logging is completely silent by default (zero overhead). Only PII-free metadata is logged: API method ID, HTTP method, status code, latency, and content-type.
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ The CLI uses a **two-phase argument parsing** strategy:
| `src/executor.rs` | HTTP request construction, response handling, schema validation |
| `src/schema.rs` | `gws schema` command — introspect API method schemas |
| `src/error.rs` | Structured JSON error output |
| `src/logging.rs` | Opt-in structured logging (stderr + file) via `tracing` |

## Demo Videos

Expand Down Expand Up @@ -202,4 +203,11 @@ Use these labels to categorize pull requests and issues:
|---|---|
| `GOOGLE_WORKSPACE_PROJECT_ID` | GCP project ID override for quota/billing and fallback for helper commands (overridden by `--project` flag) |

### Logging

| Variable | Description |
|---|---|
| `GOOGLE_WORKSPACE_CLI_LOG` | Log level filter for stderr output (e.g., `gws=debug`). Off by default. |
| `GOOGLE_WORKSPACE_CLI_LOG_FILE` | Directory for JSON-line log files with daily rotation. Off by default. |

All variables can also live in a `.env` file (loaded via `dotenvy`).
128 changes: 128 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ async-trait = "0.1.89"
serde_yaml = "0.9.34"
percent-encoding = "2.3.2"
zeroize = { version = "1.8.2", features = ["derive"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
tracing-appender = "0.2"

[target.'cfg(target_os = "macos")'.dependencies]
keyring = { version = "3.6.3", features = ["apple-native"] }
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,8 @@ All variables are optional. See [`.env.example`](.env.example) for a copy-paste
| `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` | Override config directory (default: `~/.config/gws`) |
| `GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE` | Default Model Armor template |
| `GOOGLE_WORKSPACE_CLI_SANITIZE_MODE` | `warn` (default) or `block` |
| `GOOGLE_WORKSPACE_CLI_LOG` | Log level for stderr (e.g., `gws=debug`). Off by default. |
| `GOOGLE_WORKSPACE_CLI_LOG_FILE` | Directory for JSON log files with daily rotation. Off by default. |
| `GOOGLE_WORKSPACE_PROJECT_ID` | GCP project ID override for quota/billing and fallback for helper commands |

Environment variables can also be set in a `.env` file (loaded via [dotenvy](https://crates.io/crates/dotenvy)).
Expand Down
7 changes: 7 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ pub async fn send_with_retry(
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(1 << attempt); // 1, 2, 4 seconds

tracing::debug!(
attempt = attempt + 1,
max_retries = MAX_RETRIES,
retry_after_secs = retry_after,
"Rate limited, retrying"
);

tokio::time::sleep(std::time::Duration::from_secs(retry_after)).await;
}

Expand Down
2 changes: 2 additions & 0 deletions src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ pub async fn fetch_discovery_document(
if modified.elapsed().unwrap_or_default() < std::time::Duration::from_secs(86400) {
let data = std::fs::read_to_string(&cache_file)?;
let doc: RestDescription = serde_json::from_str(&data)?;
tracing::debug!(service = %service, version = %version, "Discovery cache hit");
return Ok(doc);
}
}
Expand All @@ -219,6 +220,7 @@ pub async fn fetch_discovery_document(
crate::validate::encode_path_segment(version),
);

tracing::debug!(service = %service, version = %version, "Fetching discovery document");
let client = crate::client::build_client()?;
let resp = client.get(&url).send().await?;

Expand Down
21 changes: 21 additions & 0 deletions src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,10 @@ pub async fn execute_method(
)
.await?;

let method_id = method.id.as_deref().unwrap_or("unknown");
let start = std::time::Instant::now();
let response = request.send().await.context("HTTP request failed")?;
let latency_ms = start.elapsed().as_millis() as u64;

let status = response.status();
let content_type = response
Expand All @@ -434,9 +437,27 @@ pub async fn execute_method(

if !status.is_success() {
let error_body = response.text().await.unwrap_or_default();
tracing::warn!(
api_method = method_id,
http_method = %method.http_method,
status = status.as_u16(),
latency_ms = latency_ms,
"API error"
);
return handle_error_response(status, &error_body, &auth_method);
}

tracing::debug!(
api_method = method_id,
http_method = %method.http_method,
status = status.as_u16(),
latency_ms = latency_ms,
content_type = %content_type,
is_upload = input.is_upload,
page = pages_fetched,
"API request"
);

let is_json =
content_type.contains("application/json") || content_type.contains("text/json");

Expand Down
Loading
Loading