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
29 changes: 29 additions & 0 deletions run/mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,32 @@ You should see the following output:

You have successfully deployed a remote MCP server to Cloud Run and tested it
using the FastMCP client.

## Observability with OpenTelemetry

This sample includes integration with OpenTelemetry to send traces, logs, and metrics to Google
Cloud Observability (Cloud Trace, Cloud Logging, and Cloud Monitoring).

[FastMCP is natively instrumented for
OpenTelemetry](https://gofastmcp.com/servers/telemetry#opentelemetry), so simply setting up the
SDK is enough to get telemetry data. Learn more about OpenTelemetry instrumentation
[here](https://docs.cloud.google.com/stackdriver/docs/instrumentation/overview).


### Setup Observability

1. **Ensure APIs are enabled**:
Make sure you have enabled the Telemetry (OTLP) API, Cloud Logging API, and Cloud Monitoring API in your Google Cloud project.

```bash
gcloud services enable logging.googleapis.com monitoring.googleapis.com telemetry.googleapis.com
```

1. **Run the server**:
The sample is pre-configured to use OpenTelemetry.

* **Locally**: Run `uv run server.py`. You can test it with the client: `uv run test_server.py`.
* **Cloud Run**: Deploy using the instructions in the [Deploy](#deploy) section. The default `Dockerfile` is already set up to run the instrumented server.

1. **View Traces**:
After interacting with the server to generate traces, you can view them in the Google Cloud Console. For detailed instructions, see the [Google Cloud Trace documentation on finding traces](https://docs.cloud.google.com/trace/docs/finding-traces).
67 changes: 67 additions & 0 deletions run/mcp-server/otel_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# 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
#
# http://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.

# [START cloudrun_mcpserver_setup_otel]
import logging
import google.auth
import google.auth.transport.requests
import grpc
from google.auth.transport.grpc import AuthMetadataPlugin
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
OTLPSpanExporter,
)
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

logger = logging.getLogger(__name__)


def setup_opentelemetry(service_name: str) -> None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a try...except block and catch not just the credentials issue but whether it successfully was able to connect to the telemetry endpoint?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately there's not a great way to catch it, the connection happens lazily/asynchronously.

Would you prefer if I split this into another sample duplicating most of the code to decouple the samples?

"""Sets up OpenTelemetry to send traces to Google Cloud Observability."""
credentials, project_id = google.auth.default()
if not project_id:
raise Exception("Could not determine Google Cloud project ID.")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we refine the error handling here? It would be great to add import google.api_core.exceptions and catch the more specific google.auth.exceptions.DefaultCredentialsError. This helps make the error handling more robust and explicit."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this check because the returned project_id can be None even without an exception being raised IIUC.

I think it's OK to let DefaultCredentialsError bubble up from the sample right?


resource = Resource.create(
attributes={
SERVICE_NAME: service_name,
"gcp.project_id": project_id,
}
)

# Set up OTLP auth
request = google.auth.transport.requests.Request()
auth_metadata_plugin = AuthMetadataPlugin(credentials=credentials, request=request)
channel_creds = grpc.composite_channel_credentials(
grpc.ssl_channel_credentials(),
grpc.metadata_call_credentials(auth_metadata_plugin),
)

# Set up OpenTelemetry Python SDK
tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
credentials=channel_creds,
endpoint="https://telemetry.googleapis.com:443/v1/traces",
)
)
)
trace.set_tracer_provider(tracer_provider)
logger.info("OpenTelemetry successfully initialized.")


# [END cloudrun_mcpserver_setup_otel]
6 changes: 6 additions & 0 deletions run/mcp-server/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,10 @@ readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"fastmcp==3.2.0",
"opentelemetry-api==1.40.0",
"opentelemetry-sdk==1.40.0",
"opentelemetry-exporter-otlp-proto-grpc==1.40.0",
"google-auth==2.49.1",
"grpcio==1.80.0",
]

5 changes: 5 additions & 0 deletions run/mcp-server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.

# [START cloudrun_mcpserver_otel]
from otel_setup import setup_opentelemetry
setup_opentelemetry("mcp-server")

# [START cloudrun_mcpserver]
import asyncio
import logging
Expand Down Expand Up @@ -64,3 +68,4 @@ def subtract(a: int, b: int) -> int:
)

# [END cloudrun_mcpserver]
# [END cloudrun_mcpserver_otel]
11 changes: 9 additions & 2 deletions run/mcp-server/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.

# [START cloudrun_mcpserver_test_otel]
from otel_setup import setup_opentelemetry
setup_opentelemetry("test-server")

# [START cloudrun_mcpserver_test]
import asyncio

from fastmcp import Client


async def test_server():
# Test the MCP server using streamable-http transport.
# Use "/sse" endpoint if using sse transport.
Expand All @@ -28,12 +33,14 @@ async def test_server():
# Call add tool
print(">>> 🪛 Calling add tool for 1 + 2")
result = await client.call_tool("add", {"a": 1, "b": 2})
print(f"<<< ✅ Result: {result[0].text}")
print(f"<<< ✅ Result: {result.content[0].text}")
# Call subtract tool
print(">>> 🪛 Calling subtract tool for 10 - 3")
result = await client.call_tool("subtract", {"a": 10, "b": 3})
print(f"<<< ✅ Result: {result[0].text}")
print(f"<<< ✅ Result: {result.content[0].text}")


if __name__ == "__main__":
asyncio.run(test_server())
# [END cloudrun_mcpserver_test]
# [END cloudrun_mcpserver_test_otel]
Loading