GCP and OTEL refactor

This commit is contained in:
Jason Ross
2026-05-13 20:20:31 -05:00
parent 78c20162b4
commit 446e5444b8
12 changed files with 1909 additions and 43 deletions
+25
View File
@@ -0,0 +1,25 @@
# Use a custom Containerfile to include restic and gsutil
FROM python:3.11-slim
# Suppress debconf warnings and pip root warnings
ENV DEBIAN_FRONTEND=noninteractive \
PIP_ROOT_USER_ACTION=ignore
# Install restic, curl, and gnupg
RUN apt-get update && apt-get install -y --no-install-recommends restic curl gnupg \
&& curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] http://packages.cloud.google.com/apt cloud-sdk main" | tee /etc/apt/sources.list.d/google-cloud-sdk.list \
&& apt-get update && apt-get install -y --no-install-recommends google-cloud-cli \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
COPY . .
# Cloud Run functions expect a specific entrypoint
ENTRYPOINT ["functions-framework", "--target", "run_backup", "--signature-type", "cloudevent"]
+2 -1
View File
@@ -1,5 +1,5 @@
# Use a custom Dockerfile to include restic and gsutil
FROM python:3.11-slim
FROM python:3.14-slim-trixie
# Install restic and curl (to get cloud-sdk)
RUN apt-get update && apt-get install -y restic curl gnupg \
@@ -9,6 +9,7 @@ RUN apt-get update && apt-get install -y restic curl gnupg \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /app
RUN pip install --upgrade pip
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
Binary file not shown.
+57 -15
View File
@@ -1,28 +1,68 @@
import os
import subprocess
import tempfile
import logging
import functions_framework
from opentelemetry import _logs
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from opentelemetry.sdk.resources import Resource
# ---------------------------------------------------------------------------
# OTEL Logging Setup
# ---------------------------------------------------------------------------
resource = Resource.create({
"service.name": "dev-blog-backup",
"deployment.environment": os.environ.get("ENVIRONMENT", "production")
})
logger_provider = LoggerProvider(resource=resource)
_logs.set_logger_provider(logger_provider)
# Export logs via OTLP (async/non-blocking via BatchLogRecordProcessor)
exporter = OTLPLogExporter()
logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
# Attach OTEL handler to the root logger
otel_handler = LoggingHandler(level=logging.INFO, logger_provider=logger_provider)
root_logger = logging.getLogger()
root_logger.addHandler(otel_handler)
# Create a logger for this module
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
# Disable the default stream handler for GCP to minimize billable logs
# By setting the root logger to WARNING, we ensure that standard output
# (captured by GCP) only contains high-priority logs.
# We also want to make sure we don't duplicate logs.
for handler in root_logger.handlers:
if not isinstance(handler, LoggingHandler):
handler.setLevel(logging.WARNING)
root_logger.setLevel(logging.WARNING)
@functions_framework.cloud_event
def run_backup(cloud_event):
print(f"Triggered by event: {cloud_event['id']}")
logger.info(f"Triggered by event: {cloud_event['id']}")
# Restic environments are expected to be set via Secret Manager / Env vars
# Required: RESTIC_REPOSITORY, RESTIC_PASSWORD, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
source_bucket = os.environ.get('SOURCE_BUCKET')
r2_account_id = os.environ.get('R2_ACCOUNT_ID')
r2_bucket = os.environ.get('R2_BUCKET')
source_bucket = os.environ.get('SOURCE_BUCKET') # e.g. gs://my-bucket
# Construct the Restic repository URL for Cloudflare R2
os.environ['RESTIC_REPOSITORY'] = f"s3:https://{r2_account_id}.r2.cloudflarestorage.com/{r2_bucket}"
with tempfile.TemporaryDirectory() as tmpdir:
# 1. Sync bucket to local temp dir (restic works best on local files for GCS source)
# Alternatively, restic can use rclone as a backend, but for a small blog,
# syncing to a temp dir is simpler.
print(f"Syncing {source_bucket} to {tmpdir}...")
subprocess.run(['gsutil', '-m', 'rsync', '-r', source_bucket, tmpdir], check=True)
logger.info(f"Syncing {source_bucket} to {tmpdir}...")
try:
subprocess.run(['gsutil', '-m', 'rsync', '-r', source_bucket, tmpdir], check=True, capture_output=True, text=True)
except subprocess.CalledProcessError as e:
logger.error(f"Sync failed: {e.stderr}")
raise e
# 2. Run restic backup
print("Starting restic backup to R2...")
# Note: In a real environment, you'd ensure the restic binary is in the path.
# We'll use a wrapper or ensure it's in the container.
logger.info("Starting restic backup to R2...")
try:
result = subprocess.run(
['restic', 'backup', tmpdir, '--tag', 'gcs-trigger'],
@@ -30,9 +70,11 @@ def run_backup(cloud_event):
text=True,
check=True
)
print(result.stdout)
logger.info(result.stdout)
except subprocess.CalledProcessError as e:
print(f"Restic failed: {e.stderr}")
logger.error(f"Restic failed: {e.stderr}")
raise e
print("Backup completed successfully.")
logger.info("Backup completed successfully.")
# Ensure logs are flushed before the function exits
logger_provider.force_flush()
+4
View File
@@ -1 +1,5 @@
functions-framework==3.8.1
opentelemetry-api
opentelemetry-sdk
opentelemetry-exporter-otlp
opentelemetry-instrumentation-logging