Repository navigation
fix: isolate incompatible OpenTelemetry API linkage failures #780
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
b0a33bc
fix: isolate incompatible OpenTelemetry API linkage failures
zhongkechen d5ef07f
Merge branch 'main' into fix/otel-api-linkage-compat-763
zhongkechen 10e8f11
test: verify installed OpenTelemetry API compatibility
zhongkechen 3a49c5a
ci: run OTel conformance on validated CodeBuild Java 21
7c50701
ci: trigger OTel compatibility checks for module and harness changes
f4e4e1d
ci: use read-only resolver for review-comment intake
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| #!/usr/bin/env python3 | ||
| # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| """Exercise real released/candidate core and plugin artifacts with two visible OTel APIs. | ||
|
|
||
| Dependency resolution and every probe are required: a network, compilation, or case | ||
| failure returns nonzero. No production dependency versions are modified. | ||
| """ | ||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import hashlib | ||
| import json | ||
| import os | ||
| from pathlib import Path | ||
| import shutil | ||
| import subprocess | ||
| import sys | ||
| import xml.etree.ElementTree as ET | ||
|
|
||
| API_VERSIONS = ("1.49.0", "1.66.0") | ||
| RELEASED_VERSION = "2.2.1" | ||
| CORE = "aws-durable-execution-sdk-java" | ||
| PLUGIN = "aws-durable-execution-sdk-java-plugin-otel" | ||
| PROBE = "software.amazon.lambda.durable.otel.InstalledApiProbe" | ||
|
|
||
|
|
||
| def execute(command: list[str], log: Path, *, env: dict[str, str] | None = None, timeout: int = 300) -> None: | ||
| with log.open("w") as output: | ||
| try: | ||
| result = subprocess.run(command, stdout=output, stderr=subprocess.STDOUT, env=env, | ||
| check=False, timeout=timeout) | ||
| except subprocess.TimeoutExpired as error: | ||
| raise RuntimeError(f"Command timed out after {timeout}s; log={log}") from error | ||
| if result.returncode: | ||
| tail = "\n".join(log.read_text(errors="replace").splitlines()[-35:]) | ||
| raise RuntimeError(f"Command failed ({result.returncode}); log={log}\n{tail}") | ||
|
|
||
|
|
||
| def artifact(entries: list[Path], name: str, version: str) -> Path: | ||
| matches = [p for p in entries if p.name == f"{name}-{version}.jar"] | ||
| if len(matches) != 1: | ||
| raise RuntimeError(f"Expected exactly one {name}:{version}, got {matches}") | ||
| if not matches[0].is_file(): | ||
| raise RuntimeError(f"Resolved artifact is absent: {matches[0]}") | ||
| return matches[0].resolve() | ||
|
|
||
|
|
||
| def jar_facts(path: Path) -> dict[str, str]: | ||
| return {"path": str(path), "sha256": hashlib.sha256(path.read_bytes()).hexdigest()} | ||
|
|
||
|
|
||
| def snapshot_candidate(path: Path, output: Path) -> Path: | ||
| expected = jar_facts(path)["sha256"] | ||
| directory = output / "candidate-artifacts" | ||
| directory.mkdir(exist_ok=True) | ||
| target = directory / path.name | ||
| if path.resolve() != target.resolve(): | ||
| shutil.copyfile(path, target) | ||
| if jar_facts(target)["sha256"] != expected or jar_facts(path)["sha256"] != expected: | ||
| raise RuntimeError(f"Candidate changed while being snapshotted: {path}") | ||
| return target.resolve() | ||
|
|
||
|
|
||
| def candidate_jar(root: Path, module: str, name: str) -> Path: | ||
| pom = ET.parse(root / "pom.xml") | ||
| version = pom.findtext("{http://maven.apache.org/POM/4.0.0}version") | ||
| if not version: | ||
| raise RuntimeError("Cannot resolve the reactor version from pom.xml") | ||
| path = root / module / "target" / f"{name}-{version}.jar" | ||
| if not path.is_file(): | ||
| raise RuntimeError(f"Build the candidate first; artifact missing: {path}") | ||
| return path.resolve() | ||
|
|
||
|
|
||
| def resolve_classpaths(fixture: Path, output: Path, maven: str) -> dict[str, list[Path]]: | ||
| classpaths: dict[str, list[Path]] = {} | ||
| for version in API_VERSIONS: | ||
| target = output / f"dependencies-{version}.txt" | ||
| execute([ | ||
| maven, "-B", "-f", str(fixture / "pom.xml"), | ||
| "org.apache.maven.plugins:maven-dependency-plugin:3.11.0:build-classpath", | ||
| f"-Dotel.api.version={version}", f"-Dmdep.outputFile={target}", | ||
| ], output / f"resolve-{version}.log") | ||
| classpaths[version] = [Path(p).resolve() for p in target.read_text().strip().split(os.pathsep)] | ||
| artifact(classpaths[version], "opentelemetry-api", version) | ||
| artifact(classpaths[version], "opentelemetry-context", version) | ||
| return classpaths | ||
|
|
||
|
|
||
| def probe_environment(view: str) -> dict[str, str]: | ||
| env = os.environ.copy() | ||
| # The fixture sets its own plugin registration/global provider. Do not inherit | ||
| # Lambda-hosted CI tracing or a developer's auto-agent/plugin configuration. | ||
| for key in ("_X_AMZN_TRACE_ID", "DURABLE_EXECUTION_PLUGINS", "JAVA_TOOL_OPTIONS", | ||
| "JDK_JAVA_OPTIONS", "OTEL_JAVAAGENT_EXTENSIONS", "AWS_LAMBDA_EXEC_WRAPPER"): | ||
| env.pop(key, None) | ||
| env["DURABLE_EXECUTION_PLUGINS"] = f"{view},compat-healthy" | ||
| return env | ||
|
|
||
|
|
||
| def run_matrix(args: argparse.Namespace) -> int: | ||
| root = args.root.resolve() | ||
| output = args.output.resolve() | ||
| output.mkdir(parents=True, exist_ok=True) | ||
| fixture = root / "otel-plugin/src/test/compatibility/b1" | ||
| cp = resolve_classpaths(fixture, output, args.maven) | ||
| released_core = artifact(cp["1.66.0"], CORE, RELEASED_VERSION) | ||
| released_plugin = artifact(cp["1.66.0"], PLUGIN, RELEASED_VERSION) | ||
| new_core = args.new_core.resolve() if args.new_core else candidate_jar(root, "sdk", CORE) | ||
| new_plugin = args.new_plugin.resolve() if args.new_plugin else candidate_jar(root, "otel-plugin", PLUGIN) | ||
| for jar in (new_core, new_plugin): | ||
| if not jar.is_file(): | ||
| raise RuntimeError(f"Candidate artifact missing: {jar}") | ||
| candidate_inputs = {"core": jar_facts(new_core), "plugin": jar_facts(new_plugin)} | ||
| new_core = snapshot_candidate(new_core, output) | ||
| new_plugin = snapshot_candidate(new_plugin, output) | ||
| classes = output / "classes" | ||
| classes.mkdir(exist_ok=True) | ||
| execute([args.javac, "--release", "17", "-classpath", os.pathsep.join(map(str, cp["1.66.0"])), | ||
| "-d", str(classes), str(fixture / "InstalledApiProbe.java")], output / "compile.log") | ||
| services = classes / "META-INF/services" | ||
| services.mkdir(parents=True, exist_ok=True) | ||
| (services / "software.amazon.lambda.durable.plugin.DurableExecutionPluginProvider").write_text( | ||
| PROBE + "$HealthyProvider\n") | ||
| report: dict[str, object] = { | ||
| "released_core": jar_facts(released_core), "released_plugin": jar_facts(released_plugin), | ||
| "new_core": jar_facts(new_core), "new_plugin": jar_facts(new_plugin), | ||
| "candidate_inputs": candidate_inputs, | ||
| "cases": [], "agent_coverage": "This matrix is visible-API skew, not a deployed Java-agent test.", | ||
| } | ||
| cases: list[dict[str, object]] = report["cases"] # type: ignore[assignment] | ||
| failures = 0 | ||
| pairs = {"old-old": (released_core, released_plugin), "new-old": (new_core, released_plugin), | ||
| "old-new": (released_core, new_plugin), "new-new": (new_core, new_plugin)} | ||
| for version in API_VERSIONS: | ||
| api = artifact(cp[version], "opentelemetry-api", version) | ||
| context = artifact(cp[version], "opentelemetry-context", version) | ||
| dependencies = [p for p in cp[version] if p.name not in | ||
| (f"{CORE}-{RELEASED_VERSION}.jar", f"{PLUGIN}-{RELEASED_VERSION}.jar")] | ||
| for label, (core, plugin) in pairs.items(): | ||
| for view in ("otel-invocation", "otel-execution"): | ||
| name = f"{label}-api{version}-{view}" | ||
| negative = label == "old-old" and version == "1.49.0" | ||
| case: dict[str, object] = {"name": name, "expected_negative_control": negative, | ||
| "api": jar_facts(api), "context": jar_facts(context)} | ||
| command = [args.java, "-cp", os.pathsep.join(map(str, [classes, core, plugin, *dependencies])), | ||
| PROBE, str(core), str(plugin), str(api), str(context), view, | ||
| str(negative).lower(), str(version == "1.66.0").lower()] | ||
| try: | ||
| log = output / f"{name}.log" | ||
| execute(command, log, env=probe_environment(view), timeout=90) | ||
| contents = log.read_text(errors="replace") | ||
| if "COMPAT_PASS " not in contents: | ||
| raise RuntimeError("Probe did not report successful completion") | ||
| if negative and "NEGATIVE_CONTROL_REPRODUCED" not in contents: | ||
| raise RuntimeError("Released negative control did not reproduce the reported failure") | ||
| case["passed"] = True | ||
| except RuntimeError as error: | ||
| failures += 1 | ||
| case.update(passed=False, error=str(error)) | ||
| cases.append(case) | ||
| (output / "results.json").write_text(json.dumps(report, indent=2) + "\n") | ||
| print(f"{'PASS' if case['passed'] else 'FAIL'} {name}", flush=True) | ||
| report["passed"] = failures == 0 | ||
| report["failure_count"] = failures | ||
| (output / "results.json").write_text(json.dumps(report, indent=2) + "\n") | ||
| print(f"Installed artifact matrix: {len(cases) - failures}/{len(cases)} passed; {output / 'results.json'}") | ||
| return 1 if failures else 0 | ||
|
|
||
|
|
||
| def main() -> int: | ||
| parser = argparse.ArgumentParser(description=__doc__) | ||
| parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[2]) | ||
| parser.add_argument("--output", type=Path, default=Path("target/otel-api-compatibility")) | ||
| parser.add_argument("--new-core", type=Path) | ||
| parser.add_argument("--new-plugin", type=Path) | ||
| parser.add_argument("--maven", default=shutil.which("mvn") or "mvn") | ||
| parser.add_argument("--java", default=shutil.which("java") or "java") | ||
| parser.add_argument("--javac", default=shutil.which("javac") or "javac") | ||
| try: | ||
| return run_matrix(parser.parse_args()) | ||
| except (RuntimeError, OSError, subprocess.SubprocessError) as error: | ||
| print(f"Compatibility harness failed: {error}", file=sys.stderr) | ||
| return 1 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.