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
16 changes: 16 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# A .ignore file for Docker
target/
46 changes: 46 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 @@ -60,6 +60,7 @@ default-members = [
]

members = [
"demos/cloud-run-o11y",
"guide/samples",
"src/auth",
"src/bigquery",
Expand Down Expand Up @@ -374,9 +375,11 @@ http-body = { default-features = false, version = "1" }
humantime = { default-features = false, version = "2" }
hyper = { default-features = false, version = "1.6" }
jsonwebtoken = { default-features = false, version = "10.2" }
markdown = { default-features = false, version = "1.0" }
opentelemetry = { default-features = false, version = "0.31" }
opentelemetry-proto = { default-features = false, version = "0.31" }
opentelemetry_sdk = { default-features = false, version = "0.31" }
opentelemetry-http = { default-features = false, version = "0.31" }
opentelemetry-otlp = { default-features = false, version = "0.31" }
parse-size = { default-features = false, version = "1.1" }
percent-encoding = { default-features = false, version = "2.3" }
Expand Down
54 changes: 54 additions & 0 deletions demos/cloud-run-o11y/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

[package]
name = "demo-cloud-run-o11y"
version = "0.0.0"
publish = false
description = "Use Rust on Cloud Run with observability."
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
keywords.workspace = true
categories.workspace = true
rust-version.workspace = true

[dependencies]
anyhow.workspace = true
axum = { workspace = true, features = ["http1", "tokio"] }
chrono = { workspace = true, features = ["std"] }
clap = { workspace = true, features = ["env", "std"] }
google-cloud-aiplatform-v1 = { workspace = true, features = ["default-rustls-provider", "prediction-service"] }
google-cloud-auth = { workspace = true, features = ["default", "idtoken"] }
google-cloud-gax = { workspace = true }
google-cloud-storage = { workspace = true, features = ["default"] }
integration-tests-o11y = { path = "../../tests/o11y" }
markdown.workspace = true
opentelemetry = { workspace = true, features = ["trace"] }
opentelemetry-http = { workspace = true }
opentelemetry_sdk = { workspace = true }
rand.workspace = true
serde = { workspace = true }
serde_json.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["full", "macros"] }
tracing-opentelemetry = { workspace = true, default-features = true }
tracing-serde = { workspace = true }
tracing-subscriber = { workspace = true, features = ["json", "std"] }
tracing.workspace = true
uuid = { workspace = true, features = ["v4"] }

[lints]
workspace = true
34 changes: 34 additions & 0 deletions demos/cloud-run-o11y/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

FROM rust:1.93-slim-bookworm AS builder

WORKDIR /usr/src/workspace
COPY . .
RUN env RUSTFLAGS="--cfg google_cloud_unstable_tracing" cargo build -p demo-cloud-run-o11y --release

# Use a lightweight Debian image for the runtime
FROM debian:bookworm-slim

# Install CA certificates needed for HTTPS/GCP API calls
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*

# Copy the compiled binary from the builder stage
COPY --from=builder /usr/src/workspace/target/release/demo-cloud-run-o11y /usr/local/bin/demo-cloud-run-o11y

# Expose standard Cloud Run port
EXPOSE 8080

# Run the application
ENTRYPOINT ["/usr/local/bin/demo-cloud-run-o11y"]
54 changes: 54 additions & 0 deletions demos/cloud-run-o11y/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Highlights the observability features in the Rust SDK

This directory contains a demo application demonstrating how to deploy Rust
applications to Cloud Run and monitor them with Google Cloud AppHub.

## Building and Deploying

Because this application relies on other crates in the Rust workspace, you must
build the Docker image from the root of the workspace.

1. Ensure you are authenticated with Google Cloud:

```bash
gcloud auth login
gcloud config set project YOUR_PROJECT_ID
GOOGLE_CLOUD_PROJECT="$(gcloud config get project)"
```

1. Create an Artifact Registry repository (if you don't already have one):

```bash
gcloud artifacts repositories create cloud-run-apps \
--repository-format=docker \
--location=us-central1 \
--description="Docker repository for Cloud Run apps"
```

1. Grant Cloud Run permission to read from the repository (using the default
Compute Engine service account):

```bash
PROJECT_NUMBER=$(gcloud projects describe ${GOOGLE_CLOUD_PROJECT} --format="value(projectNumber)")
gcloud artifacts repositories add-iam-policy-binding cloud-run-apps \
--location=us-central1 \
--member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
--role="roles/artifactregistry.reader"
```

1. Build the Docker image using Google Cloud Build (run from the workspace
root):

```bash
gcloud builds submit . --config demos/cloud-run-o11y/cloudbuild.yaml
```

1. Deploy the built image to Cloud Run:

```bash
gcloud run deploy cloud-run-o11y \
--image us-central1-docker.pkg.dev/${GOOGLE_CLOUD_PROJECT}/cloud-run-apps/demo-cloud-run-o11y \
--allow-unauthenticated \
--region us-central1 \
--set-env-vars=GOOGLE_CLOUD_PROJECT=${GOOGLE_CLOUD_PROJECT}
```
24 changes: 24 additions & 0 deletions demos/cloud-run-o11y/cloudbuild.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

steps:
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', 'us-central1-docker.pkg.dev/${PROJECT_ID}/cloud-run-apps/demo-cloud-run-o11y', '-f', 'demos/cloud-run-o11y/Dockerfile', '.']
automapSubstitutions: true

images:
- 'us-central1-docker.pkg.dev/${PROJECT_ID}/cloud-run-apps/demo-cloud-run-o11y'

options:
machineType: 'E2_HIGHCPU_32'
36 changes: 36 additions & 0 deletions demos/cloud-run-o11y/src/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use clap::Parser;

/// Command-line arguments.
#[derive(Clone, Debug, Parser)]
#[command(version, about, long_about = super::DESCRIPTION)]
pub struct Args {
/// The default project name, if not found via resource discovery.
#[arg(long, env = "GOOGLE_CLOUD_PROJECT", default_value = "")]
pub project_id: String,

/// The default project name, if not found via resource discovery.
#[arg(long, env = "K_SERVICE", default_value = "")]
pub service_name: String,

/// The default port.
#[arg(long, env = "PORT", default_value = "8080")]
pub port: String,

/// Use the regional version of Vertex AI.
#[arg(long)]
pub regional: Option<String>,
}
69 changes: 69 additions & 0 deletions demos/cloud-run-o11y/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use google_cloud_auth::build_errors::Error as BuildError;
use google_cloud_gax::client_builder::Error as ClientBuilderError;
use google_cloud_gax::error::Error;

#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("the backend reported an error: {0}")]
Backend(#[source] Error),
#[error("the backend response had an unexpected format: {0}")]
BadResponseFormat(String),
#[error("there was a problem contacting the backend: {0}")]
Request(#[source] Error),
#[error("cannot initialize the service credentials: {0}")]
Credentials(#[source] BuildError),
#[error("cannot initialize a client: {0}")]
Client(#[source] ClientBuilderError),
}

impl From<Error> for AppError {
fn from(value: Error) -> Self {
if value.status().is_some() {
Self::Backend(value)
} else {
Self::Request(value)
}
}
}

impl From<BuildError> for AppError {
fn from(value: BuildError) -> Self {
Self::Credentials(value)
}
}

impl From<ClientBuilderError> for AppError {
fn from(value: ClientBuilderError) -> Self {
Self::Client(value)
}
}

impl IntoResponse for AppError {
fn into_response(self) -> Response {
tracing::error!("internal service error: {self:?}");
let (status, message) = match self {
Self::Backend(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")),
Self::BadResponseFormat(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")),
Self::Request(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:#?}")),
Self::Credentials(e) => (StatusCode::UNAUTHORIZED, format!("{e:#?}")),
Self::Client(e) => (StatusCode::SERVICE_UNAVAILABLE, format!("{e:#?}")),
};
(status, message).into_response()
}
}
Loading
Loading