SERVER-120535: Dump Merge Queue Metrics to Honeycomb (#48729)

GitOrigin-RevId: 4a4c2d17c9929eae8c764fb4088682994a9814a0
This commit is contained in:
Zack Winter
2026-02-27 08:24:38 -08:00
committed by MongoDB Bot
parent 0c9e25b469
commit 9c021c1d4d
5 changed files with 154 additions and 3 deletions

View File

@@ -535,5 +535,17 @@ py_binary(
"requests",
group = "core",
),
dependency(
"opentelemetry-api",
group = "testing",
),
dependency(
"opentelemetry-sdk",
group = "testing",
),
dependency(
"opentelemetry-exporter-otlp-proto-http",
group = "testing",
),
],
)

View File

@@ -10,9 +10,101 @@ from zoneinfo import ZoneInfo
import requests
# Optional OTEL imports for Honeycomb integration
try:
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.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
OTEL_AVAILABLE = True
except ImportError:
OTEL_AVAILABLE = False
EST = ZoneInfo("America/New_York")
CACHE_FILE = Path.home() / ".github_merge_queue_metrics.json"
# Honeycomb OTEL endpoint
HONEYCOMB_OTEL_ENDPOINT = "https://api.honeycomb.io/v1/traces"
def setup_otel_tracer(honeycomb_api_key, honeycomb_dataset):
"""Set up OpenTelemetry tracer with Honeycomb HTTP exporter."""
if not OTEL_AVAILABLE:
print(
"OpenTelemetry is not available. "
"Install opentelemetry packages to enable Honeycomb export."
)
return None
resource = Resource(attributes={SERVICE_NAME: "github-merge-queue-metrics"})
# Configure OTLP HTTP exporter for Honeycomb
headers = {
"x-honeycomb-team": honeycomb_api_key,
"x-honeycomb-dataset": honeycomb_dataset,
}
exporter = OTLPSpanExporter(
endpoint=HONEYCOMB_OTEL_ENDPOINT,
headers=headers,
)
provider = TracerProvider(resource=resource)
processor = BatchSpanProcessor(exporter)
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
return trace.get_tracer("github-merge-queue-metrics")
def export_pr_metrics_to_honeycomb(tracer, results, repo_owner, repo_name):
"""Export PR merge queue metrics to Honeycomb as OTEL spans."""
if tracer is None:
return
print(f"\nExporting {len(results)} PR metrics to Honeycomb...")
for pull_number, started_at, merged_at, time_difference in results:
# Create a span for each PR merge event
# Use the actual timestamps from the PR for accurate timing
span = tracer.start_span(
"merge_queue_pr",
start_time=int(started_at.timestamp() * 1e9), # Convert to nanoseconds
)
duration_seconds = time_difference.total_seconds()
# Set span attributes with PR details
span.set_attribute("pr.number", pull_number)
span.set_attribute("pr.repo_owner", repo_owner)
span.set_attribute("pr.repo_name", repo_name)
pr_url = f"https://github.com/{repo_owner}/{repo_name}/pull/{pull_number}"
span.set_attribute("pr.url", pr_url)
span.set_attribute("pr.merge_queue_started_at", started_at.isoformat())
span.set_attribute("pr.merged_at", merged_at.isoformat())
span.set_attribute("pr.merge_queue_duration_seconds", duration_seconds)
span.set_attribute("pr.merge_queue_duration_minutes", duration_seconds / 60)
# Add day of week and time of day for analysis
merged_at_est = merged_at.astimezone(EST)
span.set_attribute("pr.merged_day_of_week", merged_at_est.strftime("%A"))
span.set_attribute("pr.merged_hour", merged_at_est.hour)
span.set_attribute("pr.is_weekend", merged_at_est.weekday() >= 5)
# End the span at the actual merge time
span.end(end_time=int(merged_at.timestamp() * 1e9))
print("Export complete.")
def shutdown_otel():
"""Shutdown the OTEL tracer provider to flush pending spans."""
if OTEL_AVAILABLE:
provider = trace.get_tracer_provider()
if hasattr(provider, "shutdown"):
provider.shutdown()
def load_cache():
"""Load the cache from disk."""
@@ -135,11 +227,30 @@ def main():
action="store_true",
help="Print a list of PRs that were removed from the merge queue (not merged)",
)
parser.add_argument(
"--honeycomb-api-key",
default=os.environ.get("HONEYCOMB_API_KEY"),
help="Honeycomb API key for exporting metrics (default: HONEYCOMB_API_KEY env var)",
)
parser.add_argument(
"--honeycomb-dataset",
default=os.environ.get("HONEYCOMB_DATASET", "merge-queue-metrics"),
help="Honeycomb dataset name (default: HONEYCOMB_DATASET env var or 'merge-queue-metrics')",
)
args = parser.parse_args()
if not args.token:
parser.error("--token is required or set MERGE_QUEUE_ANALYTICS_GITHUB_TOKEN env var")
# Set up OTEL tracer for Honeycomb export if API key is provided
tracer = None
if args.honeycomb_api_key:
tracer = setup_otel_tracer(args.honeycomb_api_key, args.honeycomb_dataset)
if tracer:
print(f"Honeycomb export enabled (dataset: {args.honeycomb_dataset})")
else:
print("Honeycomb export disabled (no API key provided)")
repo_owner = args.owner
repo_name = args.repo
token = args.token
@@ -307,6 +418,11 @@ def main():
else:
print("No PRs were removed from the merge queue in this range.")
# Export to Honeycomb if tracer is configured
if tracer and results:
export_pr_metrics_to_honeycomb(tracer, results, repo_owner, repo_name)
shutdown_otel()
if __name__ == "__main__":
main()

View File

@@ -2403,7 +2403,7 @@ tasks:
- func: "bazel run"
vars:
target: //buildscripts:github_merge_queue_metrics -- --owner=10gen --repo=mms
env: MERGE_QUEUE_ANALYTICS_GITHUB_TOKEN=${MERGE_QUEUE_ANALYTICS_GITHUB_TOKEN}
env: MERGE_QUEUE_ANALYTICS_GITHUB_TOKEN=${MERGE_QUEUE_ANALYTICS_GITHUB_TOKEN} HONEYCOMB_API_KEY=${MERGE_QUEUE_ANALYTICS_HONEYCOMB_API_KEY} HONEYCOMB_DATASET=Zack-Merge-Queue
- name: merge_queue_metrics_mms
tags: ["assigned_to_jira_team_devprod_correctness", "auxiliary"]
@@ -2421,4 +2421,4 @@ tasks:
- func: "bazel run"
vars:
target: //buildscripts:github_merge_queue_metrics -- --owner=10gen --repo=mms
env: MERGE_QUEUE_ANALYTICS_GITHUB_TOKEN=${MERGE_QUEUE_ANALYTICS_GITHUB_TOKEN}
env: MERGE_QUEUE_ANALYTICS_GITHUB_TOKEN=${MERGE_QUEUE_ANALYTICS_GITHUB_TOKEN} HONEYCOMB_API_KEY=${MERGE_QUEUE_ANALYTICS_HONEYCOMB_API_KEY} HONEYCOMB_DATASET=Zack-Merge-Queue

24
poetry.lock generated
View File

@@ -2713,6 +2713,28 @@ opentelemetry-proto = "1.35.0"
opentelemetry-sdk = ">=1.35.0,<1.36.0"
typing-extensions = ">=4.6.0"
[[package]]
name = "opentelemetry-exporter-otlp-proto-http"
version = "1.35.0"
description = "OpenTelemetry Collector Protobuf over HTTP Exporter"
optional = false
python-versions = ">=3.9"
groups = ["testing"]
markers = "platform_machine != \"s390x\" and platform_machine != \"ppc64le\""
files = [
{file = "opentelemetry_exporter_otlp_proto_http-1.35.0-py3-none-any.whl", hash = "sha256:9a001e3df3c7f160fb31056a28ed7faa2de7df68877ae909516102ae36a54e1d"},
{file = "opentelemetry_exporter_otlp_proto_http-1.35.0.tar.gz", hash = "sha256:cf940147f91b450ef5f66e9980d40eb187582eed399fa851f4a7a45bb880de79"},
]
[package.dependencies]
googleapis-common-protos = ">=1.52,<2.0"
opentelemetry-api = ">=1.15,<2.0"
opentelemetry-exporter-otlp-proto-common = "1.35.0"
opentelemetry-proto = "1.35.0"
opentelemetry-sdk = ">=1.35.0,<1.36.0"
requests = ">=2.7,<3.0"
typing-extensions = ">=4.5.0"
[[package]]
name = "opentelemetry-proto"
version = "1.35.0"
@@ -5896,4 +5918,4 @@ libdeps = ["cxxfilt", "eventlet", "flask", "flask-cors", "gevent", "lxml", "prog
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<4.0"
content-hash = "33a897a6d5a0482f8d4d9378582c1f3a732d74fae6068e9b80a9ffb6cc7a3873"
content-hash = "c1531f3ed6e9a5a5492221184f226041f98cfefc2b0928e5ad87e49a4d707e76"

View File

@@ -187,6 +187,7 @@ opentelemetry-api = "*"
opentelemetry-sdk = "*"
opentelemetry-exporter-otlp-proto-common = "*"
opentelemetry-exporter-otlp-proto-grpc = { version = "*", markers = "platform_machine != 's390x' and platform_machine != 'ppc64le'" }
opentelemetry-exporter-otlp-proto-http = { version = "*", markers = "platform_machine != 's390x' and platform_machine != 'ppc64le'" }
timeout-decorator = "0.5.0"
# This can be installed with "poetry install -E libdeps"