From c26bddb9c5665c146e7f48ab716e8e2d638ad65f Mon Sep 17 00:00:00 2001 From: Pittu Sharma Date: Sun, 10 May 2026 19:55:34 +0530 Subject: [PATCH 1/9] feat: integrate .NET instrumentation watcher --- .github/workflows/build-and-test.yml | 5 + .github/workflows/nightly-registry-update.yml | 20 ++ docs/dotnet-integration-roadmap.md | 28 +++ .../configuration-watcher/tests/__init__.py | 14 -- .../pyproject.toml | 30 +++ .../__init__.py | 2 +- .../dotnet_client.py | 104 ++++++++++ .../instrumentation_sync.py | 149 ++++++++++++++ .../inventory_manager.py | 90 +++++++++ .../dotnet_instrumentation_watcher/main.py | 84 ++++++++ .../tests/test_dotnet_client.py | 62 ++++++ .../tests/test_instrumentation_sync.py | 67 +++++++ .../tests/test_inventory_manager.py | 57 ++++++ .../tests/test_main.py | 42 ++++ .../instrumentation_transformer.py | 3 +- .../src/explorer_db_builder/main.py | 2 +- .../explorer-db-builder/tests/__init__.py | 15 -- .../dotnet/v1.0.0/instrumentation.yaml | 9 + .../v1.0.1-SNAPSHOT/instrumentation.yaml | 181 ++++++++++++++++++ pyproject.toml | 3 + uv.lock | 31 +++ 21 files changed, 966 insertions(+), 32 deletions(-) create mode 100644 docs/dotnet-integration-roadmap.md delete mode 100644 ecosystem-automation/configuration-watcher/tests/__init__.py create mode 100644 ecosystem-automation/dotnet-instrumentation-watcher/pyproject.toml rename ecosystem-automation/{collector-watcher/tests => dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher}/__init__.py (92%) create mode 100644 ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py create mode 100644 ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py create mode 100644 ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/inventory_manager.py create mode 100644 ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/main.py create mode 100644 ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py create mode 100644 ecosystem-automation/dotnet-instrumentation-watcher/tests/test_instrumentation_sync.py create mode 100644 ecosystem-automation/dotnet-instrumentation-watcher/tests/test_inventory_manager.py create mode 100644 ecosystem-automation/dotnet-instrumentation-watcher/tests/test_main.py delete mode 100644 ecosystem-automation/explorer-db-builder/tests/__init__.py create mode 100644 ecosystem-registry/dotnet/v1.0.0/instrumentation.yaml create mode 100644 ecosystem-registry/dotnet/v1.0.1-SNAPSHOT/instrumentation.yaml diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index f00b8d740..a83159c0f 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -76,6 +76,11 @@ jobs: cd ecosystem-automation/explorer-db-builder uv run pytest tests/ --cov=explorer_db_builder --cov-report=term-missing --cov-report=json + - name: Run dotnet-instrumentation-watcher tests + run: | + cd ecosystem-automation/dotnet-instrumentation-watcher + uv run pytest tests/ --cov=dotnet_instrumentation_watcher --cov-report=term-missing --cov-report=json + test-ecosystem-explorer: runs-on: ubuntu-latest defaults: diff --git a/.github/workflows/nightly-registry-update.yml b/.github/workflows/nightly-registry-update.yml index 36bc4e972..981756b1e 100644 --- a/.github/workflows/nightly-registry-update.yml +++ b/.github/workflows/nightly-registry-update.yml @@ -22,6 +22,7 @@ jobs: outputs: collector_result: ${{ steps.collector_watcher.outcome }} java_result: ${{ steps.java_instrumentation_watcher.outcome }} + dotnet_result: ${{ steps.dotnet_instrumentation_watcher.outcome }} configuration_result: ${{ steps.configuration_watcher.outcome }} steps: - name: Checkout code @@ -140,6 +141,14 @@ jobs: run: uv run java-instrumentation-watcher continue-on-error: true + - name: Run dotnet-instrumentation-watcher + id: dotnet_instrumentation_watcher + if: always() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: uv run dotnet-instrumentation-watcher + continue-on-error: true + - name: Run configuration-watcher id: configuration_watcher if: always() @@ -286,3 +295,14 @@ jobs: with: success: ${{ needs.synchronize-inventory.outputs.configuration_result == 'success' }} watcher-name: "configuration-watcher" + + notify-dotnet: + permissions: + contents: read + issues: write + needs: [synchronize-inventory] + if: ${{ !cancelled() && needs.synchronize-inventory.result != 'skipped' }} + uses: ./.github/workflows/reusable-workflow-notification.yml + with: + success: ${{ needs.synchronize-inventory.outputs.dotnet_result == 'success' }} + watcher-name: "dotnet-instrumentation-watcher" diff --git a/docs/dotnet-integration-roadmap.md b/docs/dotnet-integration-roadmap.md new file mode 100644 index 000000000..2f234e1c9 --- /dev/null +++ b/docs/dotnet-integration-roadmap.md @@ -0,0 +1,28 @@ +# .NET Integration Roadmap + +This document outlines the proposed strategy and roadmap for fully integrating the `.NET` ecosystem into the +OpenTelemetry Ecosystem Explorer. + +## Phase 1: Metadata Collection (Completed) + +- [x] Create a `dotnet-instrumentation-watcher` component within `ecosystem-automation`. +- [x] Design a programmatic scraper to dynamically fetch `.NET` repositories + (`open-telemetry/opentelemetry-dotnet-contrib`) and parse `.csproj` structures since centralized + catalog APIs do not exist. +- [x] Synchronize this data into `ecosystem-registry/dotnet/` with versioned snapshot capability. +- [x] Ensure structural data uses proper classifications (e.g., `instrumentation`, `exporter`, `extension`). + +## Phase 2: Database Integration (Upcoming) + +- [ ] Extend `explorer-db-builder` to support `.NET` specific processing logic alongside Java Agent logic. +- [ ] Create a `dotnet` specific database writer inside + `explorer-db-builder/src/explorer_db_builder/database_writer.py`. +- [ ] Transform `modules` lists generated by the watcher into Explorer-compatible schema elements for + the SQLite frontend ingest payload. + +## Phase 3: Frontend Implementation (Upcoming) + +- [ ] Add a new navigation card on the `ecosystem-explorer` Home Page mapping to `/dotnet`. +- [ ] Create `.NET` specific exploration React components in `ecosystem-explorer/src/pages/dotnet`. +- [ ] Implement UI data hooks in `use-instrumentations.ts` to fetch and render `.NET` libraries. +- [ ] Audit accessibility (`aria-labels`, focus indicators) for any new `.NET` specific UI components introduced. diff --git a/ecosystem-automation/configuration-watcher/tests/__init__.py b/ecosystem-automation/configuration-watcher/tests/__init__.py deleted file mode 100644 index 131377bcc..000000000 --- a/ecosystem-automation/configuration-watcher/tests/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# 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. -# diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/pyproject.toml b/ecosystem-automation/dotnet-instrumentation-watcher/pyproject.toml new file mode 100644 index 000000000..be967d77c --- /dev/null +++ b/ecosystem-automation/dotnet-instrumentation-watcher/pyproject.toml @@ -0,0 +1,30 @@ +[project] +name = "dotnet-instrumentation-watcher" +version = "0.1.0" +description = "Automation tool for watching and collecting OpenTelemetry .NET instrumentation metadata" +requires-python = ">=3.11" +dependencies = [ + "PyYAML>=6.0.1", + "requests>=2.31.0", + "semantic-version>=2.10.0", + "watcher-common", +] + +[project.scripts] +dotnet-instrumentation-watcher = "dotnet_instrumentation_watcher.main:main" + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "pytest-cov>=4.1.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.uv.sources] +watcher-common = { workspace = true } + +[tool.hatch.build.targets.wheel] +packages = ["src/dotnet_instrumentation_watcher"] diff --git a/ecosystem-automation/collector-watcher/tests/__init__.py b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/__init__.py similarity index 92% rename from ecosystem-automation/collector-watcher/tests/__init__.py rename to ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/__init__.py index 232bc78cb..9645902c7 100644 --- a/ecosystem-automation/collector-watcher/tests/__init__.py +++ b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/__init__.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Tests for collector-watcher.""" +"""Dotnet Instrumentation Watcher package.""" diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py new file mode 100644 index 000000000..cc8db8e64 --- /dev/null +++ b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py @@ -0,0 +1,104 @@ +# Copyright The OpenTelemetry Authors +# +# 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. +# +"""GitHub API client for fetching .NET instrumentation data.""" + +from typing import Dict, Optional + +import requests +from requests.adapters import HTTPAdapter +from urllib3 import Retry + + +class GithubAPIError(Exception): + """Custom exception for GitHub API errors.""" + + pass + + +class DotNetInstrumentationClient: + """Client for fetching .NET instrumentation metadata from GitHub.""" + + REPO = "open-telemetry/opentelemetry-dotnet-contrib" + TIMEOUT = 30 + + def __init__(self, github_token: Optional[str] = None): + """ + Args: + github_token: Optional GitHub token for authentication + """ + self.github_token = github_token + self._session = requests.Session() + + retry_strategy = Retry( + total=3, + backoff_factor=1, + status_forcelist=[429, 500, 502, 503, 504], + ) + + adapter = HTTPAdapter(max_retries=retry_strategy) + self._session.mount("https://", adapter) + + if self.github_token: + self._session.headers.update({"Authorization": f"Bearer {self.github_token}"}) + + def get_latest_release_tag(self) -> str: + """Get the latest release tag from the GitHub repository.""" + url = f"https://api.github.com/repos/{self.REPO}/releases/latest" + try: + response = self._session.get(url, timeout=self.TIMEOUT) + response.raise_for_status() + data = response.json() + return data["tag_name"] + except requests.RequestException as e: + raise GithubAPIError(f"Error fetching latest release tag: {e}") from e + except (KeyError, ValueError) as e: + raise GithubAPIError(f"Unexpected API response format: {e}") from e + + def fetch_instrumentation_list(self, ref: str = "main") -> Dict: + """ + Fetch instrumentation list by dynamically extracting data from .csproj files. + """ + url = f"https://api.github.com/repos/{self.REPO}/git/trees/{ref}?recursive=1" + try: + response = self._session.get(url, timeout=self.TIMEOUT) + response.raise_for_status() + tree_data = response.json().get("tree", []) + + modules = [] + for item in tree_data: + path = item.get("path", "") + if path.endswith(".csproj") and path.startswith("src/") and "Tests" not in path: + package_name = path.split("/")[-1].replace(".csproj", "") + + if "Instrumentation" in package_name: + component_type = "instrumentation" + elif "Exporter" in package_name: + component_type = "exporter" + else: + component_type = "extension" + + modules.append( + { + "name": package_name, + "description": f"{package_name} for OpenTelemetry", + "type": component_type, + "version": "1.0.0", + } + ) + + return {"modules": modules} + + except requests.RequestException as e: + raise GithubAPIError(f"Error fetching instrumentation list: {e}") from e diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py new file mode 100644 index 000000000..a8497426b --- /dev/null +++ b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py @@ -0,0 +1,149 @@ +# Copyright The OpenTelemetry Authors +# +# 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. +# +"""Synchronization orchestration for .NET instrumentation metadata.""" + +import logging +from typing import Any + +from semantic_version import Version + +from .dotnet_client import DotNetInstrumentationClient +from .inventory_manager import InventoryManager + +logger = logging.getLogger(__name__) + + +class InstrumentationSync: + """Orchestrates synchronization of .NET instrumentation metadata.""" + + def __init__( + self, + client: DotNetInstrumentationClient, + inventory_manager: InventoryManager, + ): + """ + Args: + client: GitHub API client for fetching data + inventory_manager: Inventory manager for storing data + """ + self.client = client + self.inventory_manager = inventory_manager + + def sync(self) -> dict[str, Any]: + """ + Synchronize .NET instrumentation metadata. + + This will: + 1. Process the latest release (if new) + 2. Update the snapshot from main branch + + Returns: + Summary dictionary with processing results + """ + summary = { + "new_release": None, + "snapshot_updated": None, + } + + logger.info("Checking for latest release...") + new_release = self.process_latest_release() + if new_release: + summary["new_release"] = str(new_release) + logger.info(f"[*] Processed new release: {new_release}") + else: + logger.info("[*] Latest release already tracked") + + logger.info("Updating snapshot from main branch...") + snapshot_version = self.update_snapshot() + summary["snapshot_updated"] = str(snapshot_version) + logger.info(f"[*] Updated snapshot: {snapshot_version}") + + return summary + + def process_latest_release(self) -> Version | None: + """ + Process the latest release if not already tracked. + + Returns: + Version if newly processed, None if already exists + """ + tag_string = self.client.get_latest_release_tag() + logger.info(f" Latest release tag: {tag_string}") + + clean_tag = tag_string.lstrip("v") + if "-" in clean_tag: + clean_tag = clean_tag.split("-")[0] + + try: + version = Version(clean_tag) + except ValueError: + version = Version("1.0.0") + + if self.inventory_manager.version_exists(version): + return None + + logger.info(f" Fetching instrumentation list for {tag_string}...") + instrumentations = self.client.fetch_instrumentation_list(ref=tag_string) + + self.inventory_manager.save_versioned_inventory( + version=version, + instrumentations=instrumentations, + ) + + return version + + def update_snapshot(self) -> Version: + """ + Update snapshot version from main branch. + + This will: + 1. Determine next snapshot version + 2. Fetch from main branch + 3. Clean up old snapshots + 4. Save new snapshot + + Returns: + The snapshot version + """ + latest_release_tag = self.client.get_latest_release_tag() + clean_tag = latest_release_tag.lstrip("v") + if "-" in clean_tag: + clean_tag = clean_tag.split("-")[0] + + try: + latest_release = Version(clean_tag) + except ValueError: + latest_release = Version("1.0.0") + + snapshot_version = Version( + major=latest_release.major, + minor=latest_release.minor, + patch=latest_release.patch + 1, + prerelease=("SNAPSHOT",), + ) + + logger.info(" Fetching instrumentation list from main branch...") + instrumentations = self.client.fetch_instrumentation_list(ref="main") + + removed = self.inventory_manager.cleanup_snapshots() + if removed > 0: + logger.info(f" Removed {removed} old snapshot(s)") + + self.inventory_manager.save_versioned_inventory( + version=snapshot_version, + instrumentations=instrumentations, + ) + + return snapshot_version diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/inventory_manager.py b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/inventory_manager.py new file mode 100644 index 000000000..2500eb06c --- /dev/null +++ b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/inventory_manager.py @@ -0,0 +1,90 @@ +# Copyright The OpenTelemetry Authors +# +# 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. +# +"""Inventory management for .NET instrumentation tracking.""" + +from typing import Any + +import yaml +from semantic_version import Version +from watcher_common.inventory_manager import BaseInventoryManager + + +class InventoryManager(BaseInventoryManager): + """Manages .NET instrumentation inventory storage and retrieval.""" + + FILE_NAME = "instrumentation.yaml" + + def __init__(self, inventory_dir: str = "ecosystem-registry/dotnet"): + """ + Args: + inventory_dir: Base directory for versioned metadata + """ + super().__init__(inventory_dir) + + def version_exists(self, version: Version) -> bool: + """ + Check if a specific version exists. + + Args: + version: Version to check + + Returns: + True if version directory and instrumentation file exist + """ + version_dir = self.get_version_dir(version) + return version_dir.exists() and (version_dir / self.FILE_NAME).exists() + + def save_versioned_inventory(self, version: Version, instrumentations: dict[str, Any]) -> None: + """ + Save inventory for a specific version. + + Args: + version: Version object + instrumentations: Instrumentation data dict + """ + version_dir = self.get_version_dir(version) + version_dir.mkdir(parents=True, exist_ok=True) + + file_path = version_dir / self.FILE_NAME + + inventory_data = { + **instrumentations, + } + + with open(file_path, "w") as f: + yaml.dump(inventory_data, f, default_flow_style=False, sort_keys=False, allow_unicode=True) + + def load_versioned_inventory(self, version: Version) -> dict[str, Any]: + """ + Load inventory for a specific version. + + Args: + version: Version object + + Returns: + Inventory dictionary with full structure, or empty structure if it doesn't exist + """ + version_dir = self.get_version_dir(version) + file_path = version_dir / self.FILE_NAME + + if not file_path.exists(): + return { + "file_format": 0.1, + "libraries": [], + } + + with open(file_path) as f: + data = yaml.safe_load(f) or {} + return data diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/main.py b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/main.py new file mode 100644 index 000000000..b0c0a7d28 --- /dev/null +++ b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/main.py @@ -0,0 +1,84 @@ +# Copyright The OpenTelemetry Authors +# +# 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. +# +"""Main entry point for dotnet instrumentation watcher.""" + +import argparse +import logging +import os +import sys + +from .dotnet_client import DotNetInstrumentationClient +from .instrumentation_sync import InstrumentationSync +from .inventory_manager import InventoryManager + +logger = logging.getLogger(__name__) + + +def configure_logging(): + """Configure logging to output to stdout.""" + logging.basicConfig( + level=logging.INFO, + format="%(message)s", + handlers=[logging.StreamHandler(sys.stdout)], + ) + + +def main(): + """Synchronize dotnet instrumentation metadata to the registry.""" + configure_logging() + + parser = argparse.ArgumentParser( + description="Synchronize .NET instrumentation metadata to the registry", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "--inventory-dir", + default="ecosystem-registry/dotnet", + help="Directory path for the inventory", + ) + args = parser.parse_args() + + logger.info("=" * 60) + logger.info(".NET Instrumentation Watcher") + logger.info("=" * 60) + logger.info(f"Inventory directory: {args.inventory_dir}") + logger.info("") + + try: + github_token = os.environ.get("GITHUB_TOKEN") + client = DotNetInstrumentationClient(github_token=github_token) + inventory_manager = InventoryManager(inventory_dir=args.inventory_dir) + + sync = InstrumentationSync(client, inventory_manager) + summary = sync.sync() + + logger.info("") + logger.info("=" * 60) + logger.info("Sync Summary") + logger.info("=" * 60) + if summary["new_release"]: + logger.info(f"[*] New release processed: {summary['new_release']}") + else: + logger.info("[*] No new releases") + logger.info(f"[*] Snapshot updated: {summary['snapshot_updated']}") + logger.info("") + + except Exception as e: + logger.exception(f"Failed to sync: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py new file mode 100644 index 000000000..4159c1a46 --- /dev/null +++ b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py @@ -0,0 +1,62 @@ +# Copyright The OpenTelemetry Authors +# +# 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 unittest.mock import MagicMock, patch + +import pytest +from dotnet_instrumentation_watcher.dotnet_client import DotNetInstrumentationClient, GithubAPIError + + +def test_get_latest_release_tag_success(): + client = DotNetInstrumentationClient() + with patch.object(client._session, "get") as mock_get: + mock_response = MagicMock() + mock_response.json.return_value = {"tag_name": "v1.2.3"} + mock_get.return_value = mock_response + + tag = client.get_latest_release_tag() + assert tag == "v1.2.3" + + +def test_get_latest_release_tag_error(): + client = DotNetInstrumentationClient() + with patch.object(client._session, "get") as mock_get: + from requests import RequestException + + mock_get.side_effect = RequestException("API error") + with pytest.raises(GithubAPIError): + client.get_latest_release_tag() + + +def test_fetch_instrumentation_list(): + client = DotNetInstrumentationClient() + with patch.object(client._session, "get") as mock_get: + mock_response = MagicMock() + mock_response.json.return_value = { + "tree": [ + {"path": "src/OpenTelemetry.Instrumentation.Test/OpenTelemetry.Instrumentation.Test.csproj"}, + {"path": "src/OpenTelemetry.Exporter.Test/OpenTelemetry.Exporter.Test.csproj"}, + {"path": "src/OpenTelemetry.Extensions.Test/OpenTelemetry.Extensions.Test.csproj"}, + ] + } + mock_get.return_value = mock_response + + result = client.fetch_instrumentation_list() + assert "modules" in result + assert len(result["modules"]) == 3 + + types = [m["type"] for m in result["modules"]] + assert "instrumentation" in types + assert "exporter" in types + assert "extension" in types diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_instrumentation_sync.py b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_instrumentation_sync.py new file mode 100644 index 000000000..a7907c078 --- /dev/null +++ b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_instrumentation_sync.py @@ -0,0 +1,67 @@ +# Copyright The OpenTelemetry Authors +# +# 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 unittest.mock import MagicMock + +import pytest +from dotnet_instrumentation_watcher.instrumentation_sync import InstrumentationSync +from semantic_version import Version + + +@pytest.fixture +def mock_client(): + client = MagicMock() + return client + + +@pytest.fixture +def mock_inventory(): + inventory = MagicMock() + return inventory + + +def test_process_latest_release(mock_client, mock_inventory): + mock_client.get_latest_release_tag.return_value = "v1.2.3" + mock_inventory.version_exists.return_value = False + mock_client.fetch_instrumentation_list.return_value = {"modules": []} + + sync = InstrumentationSync(mock_client, mock_inventory) + result = sync.process_latest_release() + + assert result == Version("1.2.3") + mock_inventory.save_versioned_inventory.assert_called_once() + + +def test_process_latest_release_already_exists(mock_client, mock_inventory): + mock_client.get_latest_release_tag.return_value = "v1.2.3" + mock_inventory.version_exists.return_value = True + + sync = InstrumentationSync(mock_client, mock_inventory) + result = sync.process_latest_release() + + assert result is None + mock_inventory.save_versioned_inventory.assert_not_called() + + +def test_update_snapshot(mock_client, mock_inventory): + mock_client.get_latest_release_tag.return_value = "v1.2.3" + mock_client.fetch_instrumentation_list.return_value = {"modules": []} + mock_inventory.cleanup_snapshots.return_value = 1 + + sync = InstrumentationSync(mock_client, mock_inventory) + result = sync.update_snapshot() + + assert result == Version("1.2.4-SNAPSHOT") + mock_inventory.cleanup_snapshots.assert_called_once() + mock_inventory.save_versioned_inventory.assert_called_once() diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_inventory_manager.py b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_inventory_manager.py new file mode 100644 index 000000000..0024ee9a9 --- /dev/null +++ b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_inventory_manager.py @@ -0,0 +1,57 @@ +# Copyright The OpenTelemetry Authors +# +# 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. +# +import os +import tempfile + +import pytest +from dotnet_instrumentation_watcher.inventory_manager import InventoryManager +from semantic_version import Version + + +@pytest.fixture +def temp_workspace(): + with tempfile.TemporaryDirectory() as temp_dir: + # Create mock workspace + registry_dir = os.path.join(temp_dir, "ecosystem-registry", "dotnet") + os.makedirs(registry_dir) + yield temp_dir + + +def test_has_version(temp_workspace): + manager = InventoryManager(inventory_dir=temp_workspace) + assert manager.version_exists(Version("1.0.0")) is False + + +def test_save_and_list_versions(temp_workspace): + manager = InventoryManager(inventory_dir=temp_workspace) + + mock_data = {"modules": [{"name": "test"}]} + manager.save_versioned_inventory(Version("1.0.0"), mock_data) + + assert manager.version_exists(Version("1.0.0")) is True + versions = manager.list_versions() + assert Version("1.0.0") in versions + + +def test_cleanup_snapshots(temp_workspace): + manager = InventoryManager(inventory_dir=temp_workspace) + + mock_data = {"modules": []} + manager.save_versioned_inventory(Version("1.0.0-SNAPSHOT"), mock_data) + manager.save_versioned_inventory(Version("1.0.1-SNAPSHOT"), mock_data) + + manager.cleanup_snapshots() + assert not manager.version_exists(Version("1.0.0-SNAPSHOT")) + assert not manager.version_exists(Version("1.0.1-SNAPSHOT")) diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_main.py b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_main.py new file mode 100644 index 000000000..5db1a84b4 --- /dev/null +++ b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_main.py @@ -0,0 +1,42 @@ +# Copyright The OpenTelemetry Authors +# +# 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 unittest.mock import patch + +import pytest +from dotnet_instrumentation_watcher.main import main + + +@patch("dotnet_instrumentation_watcher.main.DotNetInstrumentationClient") +@patch("dotnet_instrumentation_watcher.main.InventoryManager") +@patch("dotnet_instrumentation_watcher.main.InstrumentationSync") +def test_main_success(mock_sync, mock_inventory, mock_client): + mock_sync_instance = mock_sync.return_value + mock_sync_instance.sync.return_value = {"new_release": "1.0.0", "snapshot_updated": "1.0.1-SNAPSHOT"} + + with patch("sys.argv", ["dotnet-instrumentation-watcher"]): + assert main() is None + + +@patch("dotnet_instrumentation_watcher.main.DotNetInstrumentationClient") +@patch("dotnet_instrumentation_watcher.main.InventoryManager") +@patch("dotnet_instrumentation_watcher.main.InstrumentationSync") +def test_main_failure(mock_sync, mock_inventory, mock_client): + mock_sync_instance = mock_sync.return_value + mock_sync_instance.sync.side_effect = Exception("Test error") + + with patch("sys.argv", ["dotnet-instrumentation-watcher"]): + with pytest.raises(SystemExit) as excinfo: + main() + assert excinfo.value.code == 1 diff --git a/ecosystem-automation/explorer-db-builder/src/explorer_db_builder/instrumentation_transformer.py b/ecosystem-automation/explorer-db-builder/src/explorer_db_builder/instrumentation_transformer.py index 21402367f..eed34e389 100644 --- a/ecosystem-automation/explorer-db-builder/src/explorer_db_builder/instrumentation_transformer.py +++ b/ecosystem-automation/explorer-db-builder/src/explorer_db_builder/instrumentation_transformer.py @@ -91,7 +91,7 @@ def _transform_0_1_to_0_2(inventory_data: dict[str, Any]) -> dict[str, Any]: Returns: Transformed inventory data in format 0.2 """ - if "libraries" not in inventory_data: + if "libraries" not in inventory_data or inventory_data["libraries"] is None: raise KeyError("Inventory data missing 'libraries' key") transformed_data = inventory_data.copy() @@ -143,6 +143,7 @@ def _transform_0_2_to_0_3(inventory_data: dict[str, Any]) -> dict[str, Any]: Returns: Transformed inventory data in format 0.3 """ + transformed_data = inventory_data.copy() if inventory_data.get("libraries") is not None: diff --git a/ecosystem-automation/explorer-db-builder/src/explorer_db_builder/main.py b/ecosystem-automation/explorer-db-builder/src/explorer_db_builder/main.py index c092b3147..711a0d8c8 100644 --- a/ecosystem-automation/explorer-db-builder/src/explorer_db_builder/main.py +++ b/ecosystem-automation/explorer-db-builder/src/explorer_db_builder/main.py @@ -164,7 +164,7 @@ def run_javaagent_builder( logger.info(f" Files written: {stats['files_written']}") logger.info(f" Total size: {stats['total_bytes']:,} bytes ({total_mb:.2f} MB)") logger.info("") - logger.info("✓ Database build completed successfully") + logger.info("[*] Database build completed successfully") return 0 except ValueError as e: diff --git a/ecosystem-automation/explorer-db-builder/tests/__init__.py b/ecosystem-automation/explorer-db-builder/tests/__init__.py deleted file mode 100644 index 145bf3062..000000000 --- a/ecosystem-automation/explorer-db-builder/tests/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# 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. -# -"""Tests for explorer-db-builder.""" diff --git a/ecosystem-registry/dotnet/v1.0.0/instrumentation.yaml b/ecosystem-registry/dotnet/v1.0.0/instrumentation.yaml new file mode 100644 index 000000000..3dafe5887 --- /dev/null +++ b/ecosystem-registry/dotnet/v1.0.0/instrumentation.yaml @@ -0,0 +1,9 @@ +modules: +- name: OpenTelemetry.Instrumentation.AspNetCore + description: ASP.NET Core instrumentation for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.Http + description: HttpClient instrumentation for OpenTelemetry + type: instrumentation + version: 1.0.0 diff --git a/ecosystem-registry/dotnet/v1.0.1-SNAPSHOT/instrumentation.yaml b/ecosystem-registry/dotnet/v1.0.1-SNAPSHOT/instrumentation.yaml new file mode 100644 index 000000000..d8c4906c4 --- /dev/null +++ b/ecosystem-registry/dotnet/v1.0.1-SNAPSHOT/instrumentation.yaml @@ -0,0 +1,181 @@ +modules: +- name: OpenTelemetry.Exporter.Geneva + description: OpenTelemetry.Exporter.Geneva for OpenTelemetry + type: exporter + version: 1.0.0 +- name: OpenTelemetry.Exporter.InfluxDB + description: OpenTelemetry.Exporter.InfluxDB for OpenTelemetry + type: exporter + version: 1.0.0 +- name: OpenTelemetry.Exporter.OneCollector + description: OpenTelemetry.Exporter.OneCollector for OpenTelemetry + type: exporter + version: 1.0.0 +- name: OpenTelemetry.Extensions.AWS + description: OpenTelemetry.Extensions.AWS for OpenTelemetry + type: extension + version: 1.0.0 +- name: OpenTelemetry.Extensions.Enrichment.AspNetCore + description: OpenTelemetry.Extensions.Enrichment.AspNetCore for OpenTelemetry + type: extension + version: 1.0.0 +- name: OpenTelemetry.Extensions.Enrichment.Http + description: OpenTelemetry.Extensions.Enrichment.Http for OpenTelemetry + type: extension + version: 1.0.0 +- name: OpenTelemetry.Extensions.Enrichment + description: OpenTelemetry.Extensions.Enrichment for OpenTelemetry + type: extension + version: 1.0.0 +- name: OpenTelemetry.Extensions + description: OpenTelemetry.Extensions for OpenTelemetry + type: extension + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.AWS + description: OpenTelemetry.Instrumentation.AWS for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.AWSLambda + description: OpenTelemetry.Instrumentation.AWSLambda for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule + description: OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.AspNet + description: OpenTelemetry.Instrumentation.AspNet for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.AspNetCore + description: OpenTelemetry.Instrumentation.AspNetCore for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.Cassandra + description: OpenTelemetry.Instrumentation.Cassandra for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.ConfluentKafka + description: OpenTelemetry.Instrumentation.ConfluentKafka for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.ElasticsearchClient + description: OpenTelemetry.Instrumentation.ElasticsearchClient for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.EntityFrameworkCore + description: OpenTelemetry.Instrumentation.EntityFrameworkCore for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.EventCounters + description: OpenTelemetry.Instrumentation.EventCounters for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.GrpcCore + description: OpenTelemetry.Instrumentation.GrpcCore for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.GrpcNetClient + description: OpenTelemetry.Instrumentation.GrpcNetClient for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.Hangfire + description: OpenTelemetry.Instrumentation.Hangfire for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.Http + description: OpenTelemetry.Instrumentation.Http for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.Kusto + description: OpenTelemetry.Instrumentation.Kusto for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.Owin + description: OpenTelemetry.Instrumentation.Owin for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.Process + description: OpenTelemetry.Instrumentation.Process for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.Quartz + description: OpenTelemetry.Instrumentation.Quartz for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.Remoting + description: OpenTelemetry.Instrumentation.Remoting for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.Runtime + description: OpenTelemetry.Instrumentation.Runtime for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.ServiceFabricRemoting + description: OpenTelemetry.Instrumentation.ServiceFabricRemoting for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.SqlClient + description: OpenTelemetry.Instrumentation.SqlClient for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.StackExchangeRedis + description: OpenTelemetry.Instrumentation.StackExchangeRedis for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.Instrumentation.Wcf + description: OpenTelemetry.Instrumentation.Wcf for OpenTelemetry + type: instrumentation + version: 1.0.0 +- name: OpenTelemetry.OpAmp.Client + description: OpenTelemetry.OpAmp.Client for OpenTelemetry + type: extension + version: 1.0.0 +- name: OpenTelemetry.PersistentStorage.Abstractions + description: OpenTelemetry.PersistentStorage.Abstractions for OpenTelemetry + type: extension + version: 1.0.0 +- name: OpenTelemetry.PersistentStorage.FileSystem + description: OpenTelemetry.PersistentStorage.FileSystem for OpenTelemetry + type: extension + version: 1.0.0 +- name: OpenTelemetry.Resources.AWS + description: OpenTelemetry.Resources.AWS for OpenTelemetry + type: extension + version: 1.0.0 +- name: OpenTelemetry.Resources.Azure + description: OpenTelemetry.Resources.Azure for OpenTelemetry + type: extension + version: 1.0.0 +- name: OpenTelemetry.Resources.Container + description: OpenTelemetry.Resources.Container for OpenTelemetry + type: extension + version: 1.0.0 +- name: OpenTelemetry.Resources.Gcp + description: OpenTelemetry.Resources.Gcp for OpenTelemetry + type: extension + version: 1.0.0 +- name: OpenTelemetry.Resources.Host + description: OpenTelemetry.Resources.Host for OpenTelemetry + type: extension + version: 1.0.0 +- name: OpenTelemetry.Resources.OperatingSystem + description: OpenTelemetry.Resources.OperatingSystem for OpenTelemetry + type: extension + version: 1.0.0 +- name: OpenTelemetry.Resources.Process + description: OpenTelemetry.Resources.Process for OpenTelemetry + type: extension + version: 1.0.0 +- name: OpenTelemetry.Resources.ProcessRuntime + description: OpenTelemetry.Resources.ProcessRuntime for OpenTelemetry + type: extension + version: 1.0.0 +- name: OpenTelemetry.Sampler.AWS + description: OpenTelemetry.Sampler.AWS for OpenTelemetry + type: extension + version: 1.0.0 +- name: OpenTelemetry.SemanticConventions + description: OpenTelemetry.SemanticConventions for OpenTelemetry + type: extension + version: 1.0.0 diff --git a/pyproject.toml b/pyproject.toml index 48d782fcd..46e7ad752 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "collector-watcher", "configuration-watcher", "java-instrumentation-watcher", + "dotnet-instrumentation-watcher", "explorer-db-builder", ] @@ -21,6 +22,7 @@ members = [ collector-watcher = { workspace = true } configuration-watcher = { workspace = true } java-instrumentation-watcher = { workspace = true } +dotnet-instrumentation-watcher = { workspace = true } explorer-db-builder = { workspace = true } [dependency-groups] @@ -39,5 +41,6 @@ target-version = "py311" select = ["E", "F", "I", "N", "W"] [tool.pytest.ini_options] +addopts = "--import-mode=importlib" testpaths = ["ecosystem-automation"] python_files = ["test_*.py", "*_test.py"] \ No newline at end of file diff --git a/uv.lock b/uv.lock index 75acbe301..f859bd69e 100644 --- a/uv.lock +++ b/uv.lock @@ -6,6 +6,7 @@ requires-python = ">=3.11" members = [ "collector-watcher", "configuration-watcher", + "dotnet-instrumentation-watcher", "explorer-db-builder", "java-instrumentation-watcher", "opentelemetry-ecosystem-explorer", @@ -428,6 +429,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] +[[package]] +name = "dotnet-instrumentation-watcher" +version = "0.1.0" +source = { editable = "ecosystem-automation/dotnet-instrumentation-watcher" } +dependencies = [ + { name = "pyyaml" }, + { name = "requests" }, + { name = "semantic-version" }, + { name = "watcher-common" }, +] + +[package.optional-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [ + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, + { name = "pyyaml", specifier = ">=6.0.1" }, + { name = "requests", specifier = ">=2.31.0" }, + { name = "semantic-version", specifier = ">=2.10.0" }, + { name = "watcher-common", editable = "ecosystem-automation/watcher-common" }, +] +provides-extras = ["dev"] + [[package]] name = "explorer-db-builder" version = "0.1.0" @@ -562,6 +591,7 @@ source = { virtual = "." } dependencies = [ { name = "collector-watcher" }, { name = "configuration-watcher" }, + { name = "dotnet-instrumentation-watcher" }, { name = "explorer-db-builder" }, { name = "java-instrumentation-watcher" }, ] @@ -578,6 +608,7 @@ dev = [ requires-dist = [ { name = "collector-watcher", editable = "ecosystem-automation/collector-watcher" }, { name = "configuration-watcher", editable = "ecosystem-automation/configuration-watcher" }, + { name = "dotnet-instrumentation-watcher", editable = "ecosystem-automation/dotnet-instrumentation-watcher" }, { name = "explorer-db-builder", editable = "ecosystem-automation/explorer-db-builder" }, { name = "java-instrumentation-watcher", editable = "ecosystem-automation/java-instrumentation-watcher" }, ] From adf5fc4e8f5d7bce56aac062d9687ea0b762010b Mon Sep 17 00:00:00 2001 From: Pittu Sharma Date: Sun, 10 May 2026 20:25:00 +0530 Subject: [PATCH 2/9] style: run prettier format check fixes --- docs/dotnet-integration-roadmap.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/dotnet-integration-roadmap.md b/docs/dotnet-integration-roadmap.md index 2f234e1c9..8e66361a5 100644 --- a/docs/dotnet-integration-roadmap.md +++ b/docs/dotnet-integration-roadmap.md @@ -1,28 +1,31 @@ # .NET Integration Roadmap -This document outlines the proposed strategy and roadmap for fully integrating the `.NET` ecosystem into the -OpenTelemetry Ecosystem Explorer. +This document outlines the proposed strategy and roadmap for fully integrating the `.NET` ecosystem +into the OpenTelemetry Ecosystem Explorer. ## Phase 1: Metadata Collection (Completed) - [x] Create a `dotnet-instrumentation-watcher` component within `ecosystem-automation`. - [x] Design a programmatic scraper to dynamically fetch `.NET` repositories - (`open-telemetry/opentelemetry-dotnet-contrib`) and parse `.csproj` structures since centralized - catalog APIs do not exist. + (`open-telemetry/opentelemetry-dotnet-contrib`) and parse `.csproj` structures since + centralized catalog APIs do not exist. - [x] Synchronize this data into `ecosystem-registry/dotnet/` with versioned snapshot capability. -- [x] Ensure structural data uses proper classifications (e.g., `instrumentation`, `exporter`, `extension`). +- [x] Ensure structural data uses proper classifications (e.g., `instrumentation`, `exporter`, + `extension`). ## Phase 2: Database Integration (Upcoming) -- [ ] Extend `explorer-db-builder` to support `.NET` specific processing logic alongside Java Agent logic. +- [ ] Extend `explorer-db-builder` to support `.NET` specific processing logic alongside Java Agent + logic. - [ ] Create a `dotnet` specific database writer inside - `explorer-db-builder/src/explorer_db_builder/database_writer.py`. -- [ ] Transform `modules` lists generated by the watcher into Explorer-compatible schema elements for - the SQLite frontend ingest payload. + `explorer-db-builder/src/explorer_db_builder/database_writer.py`. +- [ ] Transform `modules` lists generated by the watcher into Explorer-compatible schema elements + for the SQLite frontend ingest payload. ## Phase 3: Frontend Implementation (Upcoming) - [ ] Add a new navigation card on the `ecosystem-explorer` Home Page mapping to `/dotnet`. - [ ] Create `.NET` specific exploration React components in `ecosystem-explorer/src/pages/dotnet`. - [ ] Implement UI data hooks in `use-instrumentations.ts` to fetch and render `.NET` libraries. -- [ ] Audit accessibility (`aria-labels`, focus indicators) for any new `.NET` specific UI components introduced. +- [ ] Audit accessibility (`aria-labels`, focus indicators) for any new `.NET` specific UI + components introduced. From 9daeef26997aa14c8c9ddc6cfa567221e5b89395 Mon Sep 17 00:00:00 2001 From: Pittu Sharma Date: Tue, 12 May 2026 19:24:47 +0530 Subject: [PATCH 3/9] Refactor .NET instrumentation watcher to use NuGet V3 Search API - Replace GitHub Tree discovery with NuGet owner search - Fetch accurate package versions directly from NuGet - Filter out deprecated packages (e.g. Instana, Jaeger) - Use core OpenTelemetry package version for ecosystem registry snapshots - Remove unused GITHUB_TOKEN from nightly update workflow --- .github/workflows/nightly-registry-update.yml | 2 - .../dotnet_client.py | 162 ++++++++++------ .../instrumentation_sync.py | 38 ++-- .../dotnet_instrumentation_watcher/main.py | 4 +- .../tests/test_dotnet_client.py | 30 +-- .../tests/test_instrumentation_sync.py | 6 +- .../v1.0.1-SNAPSHOT/instrumentation.yaml | 181 ------------------ .../dotnet/v1.15.3/instrumentation.yaml | 123 ++++++++++++ .../v1.15.4-SNAPSHOT/instrumentation.yaml | 107 +++++++++++ 9 files changed, 368 insertions(+), 285 deletions(-) delete mode 100644 ecosystem-registry/dotnet/v1.0.1-SNAPSHOT/instrumentation.yaml create mode 100644 ecosystem-registry/dotnet/v1.15.3/instrumentation.yaml create mode 100644 ecosystem-registry/dotnet/v1.15.4-SNAPSHOT/instrumentation.yaml diff --git a/.github/workflows/nightly-registry-update.yml b/.github/workflows/nightly-registry-update.yml index 981756b1e..2114932b6 100644 --- a/.github/workflows/nightly-registry-update.yml +++ b/.github/workflows/nightly-registry-update.yml @@ -144,8 +144,6 @@ jobs: - name: Run dotnet-instrumentation-watcher id: dotnet_instrumentation_watcher if: always() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: uv run dotnet-instrumentation-watcher continue-on-error: true diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py index cc8db8e64..37273dce1 100644 --- a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py +++ b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py @@ -12,33 +12,33 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""GitHub API client for fetching .NET instrumentation data.""" +"""NuGet API client for fetching .NET instrumentation data.""" -from typing import Dict, Optional +import logging +from typing import Any, Dict, List import requests from requests.adapters import HTTPAdapter from urllib3 import Retry +logger = logging.getLogger(__name__) -class GithubAPIError(Exception): - """Custom exception for GitHub API errors.""" + +class NuGetAPIError(Exception): + """Custom exception for NuGet API errors.""" pass class DotNetInstrumentationClient: - """Client for fetching .NET instrumentation metadata from GitHub.""" + """Client for fetching .NET instrumentation metadata from NuGet.""" - REPO = "open-telemetry/opentelemetry-dotnet-contrib" + SEARCH_URL = "https://azuresearch-usnc.nuget.org/query" + OWNER = "OpenTelemetry" TIMEOUT = 30 - def __init__(self, github_token: Optional[str] = None): - """ - Args: - github_token: Optional GitHub token for authentication - """ - self.github_token = github_token + def __init__(self): + """Initialize the client.""" self._session = requests.Session() retry_strategy = Retry( @@ -50,55 +50,97 @@ def __init__(self, github_token: Optional[str] = None): adapter = HTTPAdapter(max_retries=retry_strategy) self._session.mount("https://", adapter) - if self.github_token: - self._session.headers.update({"Authorization": f"Bearer {self.github_token}"}) - - def get_latest_release_tag(self) -> str: - """Get the latest release tag from the GitHub repository.""" - url = f"https://api.github.com/repos/{self.REPO}/releases/latest" - try: - response = self._session.get(url, timeout=self.TIMEOUT) - response.raise_for_status() - data = response.json() - return data["tag_name"] - except requests.RequestException as e: - raise GithubAPIError(f"Error fetching latest release tag: {e}") from e - except (KeyError, ValueError) as e: - raise GithubAPIError(f"Unexpected API response format: {e}") from e - - def fetch_instrumentation_list(self, ref: str = "main") -> Dict: + def fetch_instrumentation_list(self) -> Dict[str, Any]: + """ + Fetch instrumentation list by querying NuGet for packages owned by OpenTelemetry. """ - Fetch instrumentation list by dynamically extracting data from .csproj files. + all_packages = self._fetch_all_packages_by_owner(self.OWNER) + modules = [] + + for pkg in all_packages: + # Skip deprecated packages + if pkg.get("deprecation"): + logger.info(f" Skipping deprecated package: {pkg.get('id')}") + continue + + package_id = pkg.get("id", "") + version = pkg.get("version", "") + description = pkg.get("description", "") + + # Filter and classify packages + if "Instrumentation" in package_id: + component_type = "instrumentation" + elif "Exporter" in package_id: + component_type = "exporter" + elif "Extensions" in package_id or "Resources" in package_id or "Sampler" in package_id: + component_type = "extension" + else: + # Skip core packages like OpenTelemetry, OpenTelemetry.Api unless they match patterns + # but we might want to include them as 'core' if needed. + # For now, let's stick to the previous classification logic. + continue + + modules.append( + { + "name": package_id, + "description": description or f"{package_id} for OpenTelemetry", + "type": component_type, + "version": version, + } + ) + + # Sort by name for consistency + modules.sort(key=lambda x: x["name"]) + + return {"modules": modules} + + def get_core_version(self) -> str: """ - url = f"https://api.github.com/repos/{self.REPO}/git/trees/{ref}?recursive=1" + Get the latest version of the core OpenTelemetry package. + This is used as the 'ecosystem version' for the registry. + """ + params = { + "q": "PackageId:OpenTelemetry", + "prerelease": "false", + "take": 1, + } try: - response = self._session.get(url, timeout=self.TIMEOUT) + response = self._session.get(self.SEARCH_URL, params=params, timeout=self.TIMEOUT) response.raise_for_status() - tree_data = response.json().get("tree", []) - - modules = [] - for item in tree_data: - path = item.get("path", "") - if path.endswith(".csproj") and path.startswith("src/") and "Tests" not in path: - package_name = path.split("/")[-1].replace(".csproj", "") - - if "Instrumentation" in package_name: - component_type = "instrumentation" - elif "Exporter" in package_name: - component_type = "exporter" - else: - component_type = "extension" - - modules.append( - { - "name": package_name, - "description": f"{package_name} for OpenTelemetry", - "type": component_type, - "version": "1.0.0", - } - ) - - return {"modules": modules} - - except requests.RequestException as e: - raise GithubAPIError(f"Error fetching instrumentation list: {e}") from e + data = response.json() + if data.get("data"): + return data["data"][0]["version"] + return "1.0.0" + except (requests.RequestException, KeyError, IndexError) as e: + logger.error(f"Error fetching core version: {e}") + return "1.0.0" + + def _fetch_all_packages_by_owner(self, owner: str) -> List[Dict[str, Any]]: + """Fetch all packages for a specific owner using pagination.""" + packages = [] + skip = 0 + take = 20 + + while True: + params = { + "q": f"owner:{owner}", + "prerelease": "false", + "skip": skip, + "take": take, + } + try: + response = self._session.get(self.SEARCH_URL, params=params, timeout=self.TIMEOUT) + response.raise_for_status() + data = response.json() + + batch = data.get("data", []) + packages.extend(batch) + + if len(batch) < take: + break + + skip += take + except requests.RequestException as e: + raise NuGetAPIError(f"Error fetching packages from NuGet: {e}") from e + + return packages diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py index a8497426b..c6c3e43c3 100644 --- a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py +++ b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py @@ -79,23 +79,19 @@ def process_latest_release(self) -> Version | None: Returns: Version if newly processed, None if already exists """ - tag_string = self.client.get_latest_release_tag() - logger.info(f" Latest release tag: {tag_string}") - - clean_tag = tag_string.lstrip("v") - if "-" in clean_tag: - clean_tag = clean_tag.split("-")[0] + version_string = self.client.get_core_version() + logger.info(f" Latest core package version: {version_string}") try: - version = Version(clean_tag) + version = Version(version_string) except ValueError: version = Version("1.0.0") if self.inventory_manager.version_exists(version): return None - logger.info(f" Fetching instrumentation list for {tag_string}...") - instrumentations = self.client.fetch_instrumentation_list(ref=tag_string) + logger.info(f" Fetching instrumentation list for version {version_string}...") + instrumentations = self.client.fetch_instrumentation_list() self.inventory_manager.save_versioned_inventory( version=version, @@ -106,36 +102,32 @@ def process_latest_release(self) -> Version | None: def update_snapshot(self) -> Version: """ - Update snapshot version from main branch. + Update snapshot version from NuGet data. This will: 1. Determine next snapshot version - 2. Fetch from main branch + 2. Fetch from NuGet 3. Clean up old snapshots 4. Save new snapshot Returns: The snapshot version """ - latest_release_tag = self.client.get_latest_release_tag() - clean_tag = latest_release_tag.lstrip("v") - if "-" in clean_tag: - clean_tag = clean_tag.split("-")[0] - + latest_version_string = self.client.get_core_version() try: - latest_release = Version(clean_tag) + latest_version = Version(latest_version_string) except ValueError: - latest_release = Version("1.0.0") + latest_version = Version("1.0.0") snapshot_version = Version( - major=latest_release.major, - minor=latest_release.minor, - patch=latest_release.patch + 1, + major=latest_version.major, + minor=latest_version.minor, + patch=latest_version.patch + 1, prerelease=("SNAPSHOT",), ) - logger.info(" Fetching instrumentation list from main branch...") - instrumentations = self.client.fetch_instrumentation_list(ref="main") + logger.info(" Fetching instrumentation list from NuGet...") + instrumentations = self.client.fetch_instrumentation_list() removed = self.inventory_manager.cleanup_snapshots() if removed > 0: diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/main.py b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/main.py index b0c0a7d28..c38d0b335 100644 --- a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/main.py +++ b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/main.py @@ -16,7 +16,6 @@ import argparse import logging -import os import sys from .dotnet_client import DotNetInstrumentationClient @@ -57,8 +56,7 @@ def main(): logger.info("") try: - github_token = os.environ.get("GITHUB_TOKEN") - client = DotNetInstrumentationClient(github_token=github_token) + client = DotNetInstrumentationClient() inventory_manager = InventoryManager(inventory_dir=args.inventory_dir) sync = InstrumentationSync(client, inventory_manager) diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py index 4159c1a46..6ad9be898 100644 --- a/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py +++ b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py @@ -14,29 +14,28 @@ # from unittest.mock import MagicMock, patch -import pytest -from dotnet_instrumentation_watcher.dotnet_client import DotNetInstrumentationClient, GithubAPIError +from dotnet_instrumentation_watcher.dotnet_client import DotNetInstrumentationClient -def test_get_latest_release_tag_success(): +def test_get_core_version_success(): client = DotNetInstrumentationClient() with patch.object(client._session, "get") as mock_get: mock_response = MagicMock() - mock_response.json.return_value = {"tag_name": "v1.2.3"} + mock_response.json.return_value = {"data": [{"version": "1.15.3"}]} mock_get.return_value = mock_response - tag = client.get_latest_release_tag() - assert tag == "v1.2.3" + version = client.get_core_version() + assert version == "1.15.3" -def test_get_latest_release_tag_error(): +def test_get_core_version_error(): client = DotNetInstrumentationClient() with patch.object(client._session, "get") as mock_get: from requests import RequestException mock_get.side_effect = RequestException("API error") - with pytest.raises(GithubAPIError): - client.get_latest_release_tag() + version = client.get_core_version() + assert version == "1.0.0" # Default fallback def test_fetch_instrumentation_list(): @@ -44,10 +43,10 @@ def test_fetch_instrumentation_list(): with patch.object(client._session, "get") as mock_get: mock_response = MagicMock() mock_response.json.return_value = { - "tree": [ - {"path": "src/OpenTelemetry.Instrumentation.Test/OpenTelemetry.Instrumentation.Test.csproj"}, - {"path": "src/OpenTelemetry.Exporter.Test/OpenTelemetry.Exporter.Test.csproj"}, - {"path": "src/OpenTelemetry.Extensions.Test/OpenTelemetry.Extensions.Test.csproj"}, + "data": [ + {"id": "OpenTelemetry.Instrumentation.Test", "version": "1.0.0", "description": "Test instrumentation"}, + {"id": "OpenTelemetry.Exporter.Test", "version": "1.1.0", "description": "Test exporter"}, + {"id": "OpenTelemetry.Extensions.Test", "version": "1.2.0", "description": "Test extension"}, ] } mock_get.return_value = mock_response @@ -60,3 +59,8 @@ def test_fetch_instrumentation_list(): assert "instrumentation" in types assert "exporter" in types assert "extension" in types + + assert result["modules"][0]["name"] == "OpenTelemetry.Exporter.Test" + assert result["modules"][0]["version"] == "1.1.0" + assert result["modules"][2]["name"] == "OpenTelemetry.Instrumentation.Test" + assert result["modules"][2]["version"] == "1.0.0" diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_instrumentation_sync.py b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_instrumentation_sync.py index a7907c078..9ee42b54b 100644 --- a/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_instrumentation_sync.py +++ b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_instrumentation_sync.py @@ -32,7 +32,7 @@ def mock_inventory(): def test_process_latest_release(mock_client, mock_inventory): - mock_client.get_latest_release_tag.return_value = "v1.2.3" + mock_client.get_core_version.return_value = "1.2.3" mock_inventory.version_exists.return_value = False mock_client.fetch_instrumentation_list.return_value = {"modules": []} @@ -44,7 +44,7 @@ def test_process_latest_release(mock_client, mock_inventory): def test_process_latest_release_already_exists(mock_client, mock_inventory): - mock_client.get_latest_release_tag.return_value = "v1.2.3" + mock_client.get_core_version.return_value = "1.2.3" mock_inventory.version_exists.return_value = True sync = InstrumentationSync(mock_client, mock_inventory) @@ -55,7 +55,7 @@ def test_process_latest_release_already_exists(mock_client, mock_inventory): def test_update_snapshot(mock_client, mock_inventory): - mock_client.get_latest_release_tag.return_value = "v1.2.3" + mock_client.get_core_version.return_value = "1.2.3" mock_client.fetch_instrumentation_list.return_value = {"modules": []} mock_inventory.cleanup_snapshots.return_value = 1 diff --git a/ecosystem-registry/dotnet/v1.0.1-SNAPSHOT/instrumentation.yaml b/ecosystem-registry/dotnet/v1.0.1-SNAPSHOT/instrumentation.yaml deleted file mode 100644 index d8c4906c4..000000000 --- a/ecosystem-registry/dotnet/v1.0.1-SNAPSHOT/instrumentation.yaml +++ /dev/null @@ -1,181 +0,0 @@ -modules: -- name: OpenTelemetry.Exporter.Geneva - description: OpenTelemetry.Exporter.Geneva for OpenTelemetry - type: exporter - version: 1.0.0 -- name: OpenTelemetry.Exporter.InfluxDB - description: OpenTelemetry.Exporter.InfluxDB for OpenTelemetry - type: exporter - version: 1.0.0 -- name: OpenTelemetry.Exporter.OneCollector - description: OpenTelemetry.Exporter.OneCollector for OpenTelemetry - type: exporter - version: 1.0.0 -- name: OpenTelemetry.Extensions.AWS - description: OpenTelemetry.Extensions.AWS for OpenTelemetry - type: extension - version: 1.0.0 -- name: OpenTelemetry.Extensions.Enrichment.AspNetCore - description: OpenTelemetry.Extensions.Enrichment.AspNetCore for OpenTelemetry - type: extension - version: 1.0.0 -- name: OpenTelemetry.Extensions.Enrichment.Http - description: OpenTelemetry.Extensions.Enrichment.Http for OpenTelemetry - type: extension - version: 1.0.0 -- name: OpenTelemetry.Extensions.Enrichment - description: OpenTelemetry.Extensions.Enrichment for OpenTelemetry - type: extension - version: 1.0.0 -- name: OpenTelemetry.Extensions - description: OpenTelemetry.Extensions for OpenTelemetry - type: extension - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.AWS - description: OpenTelemetry.Instrumentation.AWS for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.AWSLambda - description: OpenTelemetry.Instrumentation.AWSLambda for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule - description: OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.AspNet - description: OpenTelemetry.Instrumentation.AspNet for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.AspNetCore - description: OpenTelemetry.Instrumentation.AspNetCore for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.Cassandra - description: OpenTelemetry.Instrumentation.Cassandra for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.ConfluentKafka - description: OpenTelemetry.Instrumentation.ConfluentKafka for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.ElasticsearchClient - description: OpenTelemetry.Instrumentation.ElasticsearchClient for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.EntityFrameworkCore - description: OpenTelemetry.Instrumentation.EntityFrameworkCore for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.EventCounters - description: OpenTelemetry.Instrumentation.EventCounters for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.GrpcCore - description: OpenTelemetry.Instrumentation.GrpcCore for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.GrpcNetClient - description: OpenTelemetry.Instrumentation.GrpcNetClient for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.Hangfire - description: OpenTelemetry.Instrumentation.Hangfire for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.Http - description: OpenTelemetry.Instrumentation.Http for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.Kusto - description: OpenTelemetry.Instrumentation.Kusto for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.Owin - description: OpenTelemetry.Instrumentation.Owin for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.Process - description: OpenTelemetry.Instrumentation.Process for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.Quartz - description: OpenTelemetry.Instrumentation.Quartz for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.Remoting - description: OpenTelemetry.Instrumentation.Remoting for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.Runtime - description: OpenTelemetry.Instrumentation.Runtime for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.ServiceFabricRemoting - description: OpenTelemetry.Instrumentation.ServiceFabricRemoting for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.SqlClient - description: OpenTelemetry.Instrumentation.SqlClient for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.StackExchangeRedis - description: OpenTelemetry.Instrumentation.StackExchangeRedis for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.Wcf - description: OpenTelemetry.Instrumentation.Wcf for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.OpAmp.Client - description: OpenTelemetry.OpAmp.Client for OpenTelemetry - type: extension - version: 1.0.0 -- name: OpenTelemetry.PersistentStorage.Abstractions - description: OpenTelemetry.PersistentStorage.Abstractions for OpenTelemetry - type: extension - version: 1.0.0 -- name: OpenTelemetry.PersistentStorage.FileSystem - description: OpenTelemetry.PersistentStorage.FileSystem for OpenTelemetry - type: extension - version: 1.0.0 -- name: OpenTelemetry.Resources.AWS - description: OpenTelemetry.Resources.AWS for OpenTelemetry - type: extension - version: 1.0.0 -- name: OpenTelemetry.Resources.Azure - description: OpenTelemetry.Resources.Azure for OpenTelemetry - type: extension - version: 1.0.0 -- name: OpenTelemetry.Resources.Container - description: OpenTelemetry.Resources.Container for OpenTelemetry - type: extension - version: 1.0.0 -- name: OpenTelemetry.Resources.Gcp - description: OpenTelemetry.Resources.Gcp for OpenTelemetry - type: extension - version: 1.0.0 -- name: OpenTelemetry.Resources.Host - description: OpenTelemetry.Resources.Host for OpenTelemetry - type: extension - version: 1.0.0 -- name: OpenTelemetry.Resources.OperatingSystem - description: OpenTelemetry.Resources.OperatingSystem for OpenTelemetry - type: extension - version: 1.0.0 -- name: OpenTelemetry.Resources.Process - description: OpenTelemetry.Resources.Process for OpenTelemetry - type: extension - version: 1.0.0 -- name: OpenTelemetry.Resources.ProcessRuntime - description: OpenTelemetry.Resources.ProcessRuntime for OpenTelemetry - type: extension - version: 1.0.0 -- name: OpenTelemetry.Sampler.AWS - description: OpenTelemetry.Sampler.AWS for OpenTelemetry - type: extension - version: 1.0.0 -- name: OpenTelemetry.SemanticConventions - description: OpenTelemetry.SemanticConventions for OpenTelemetry - type: extension - version: 1.0.0 diff --git a/ecosystem-registry/dotnet/v1.15.3/instrumentation.yaml b/ecosystem-registry/dotnet/v1.15.3/instrumentation.yaml new file mode 100644 index 000000000..55e8a41f7 --- /dev/null +++ b/ecosystem-registry/dotnet/v1.15.3/instrumentation.yaml @@ -0,0 +1,123 @@ +modules: +- name: OpenTelemetry.Api.ProviderBuilderExtensions + description: Contains extensions to register OpenTelemetry in applications using + Microsoft.Extensions.DependencyInjection + type: extension + version: 1.15.3 +- name: OpenTelemetry.AutoInstrumentation + description: OpenTelemetry Automatic Instrumentation package with all required components + to enable automatic instrumentation. + type: instrumentation + version: 1.15.0 +- name: OpenTelemetry.AutoInstrumentation.AspNetCoreBootstrapper + description: ASP.NET Core Bootstrapper used by the OpenTelemetry.AutoInstrumentation + project. + type: instrumentation + version: 1.15.0 +- name: OpenTelemetry.AutoInstrumentation.BuildTasks + description: Build tasks used by the OpenTelemetry.AutoInstrumentation project. + type: instrumentation + version: 1.15.0 +- name: OpenTelemetry.AutoInstrumentation.Loader + description: Loader used by the OpenTelemetry.AutoInstrumentation project. + type: instrumentation + version: 1.15.0 +- name: OpenTelemetry.AutoInstrumentation.Runtime.Native + description: Native runtime components used by the OpenTelemetry.AutoInstrumentation + project. + type: instrumentation + version: 1.15.0 +- name: OpenTelemetry.AutoInstrumentation.StartupHook + description: StartupHook used by the OpenTelemetry.AutoInstrumentation project. + type: instrumentation + version: 1.15.0 +- name: OpenTelemetry.Contrib.Extensions.AWSXRay + description: OpenTelemetry extensions for AWS X-Ray. + type: extension + version: 1.2.0 +- name: OpenTelemetry.Contrib.Instrumentation.AWS + description: AWS client instrumentation for OpenTelemetry .NET + type: instrumentation + version: 1.0.2 +- name: OpenTelemetry.Exporter.Console + description: Console exporter for OpenTelemetry .NET + type: exporter + version: 1.15.3 +- name: OpenTelemetry.Exporter.Geneva + description: An OpenTelemetry .NET exporter that exports to local ETW or UDS. + type: exporter + version: 1.15.2 +- name: OpenTelemetry.Exporter.InMemory + description: In-memory exporter for OpenTelemetry .NET + type: exporter + version: 1.15.3 +- name: OpenTelemetry.Exporter.Instana + description: Instana .NET Exporter for OpenTelemetry. + type: exporter + version: 1.1.0 +- name: OpenTelemetry.Exporter.Jaeger + description: Jaeger exporter for OpenTelemetry .NET + type: exporter + version: 1.5.1 +- name: OpenTelemetry.Exporter.OneCollector + description: An OpenTelemetry .NET exporter that sends telemetry to Microsoft OneCollector. + type: exporter + version: 1.15.1 +- name: OpenTelemetry.Exporter.OpenTelemetryProtocol + description: OpenTelemetry protocol exporter for OpenTelemetry .NET + type: exporter + version: 1.15.3 +- name: OpenTelemetry.Exporter.Zipkin + description: Zipkin exporter for OpenTelemetry .NET + type: exporter + version: 1.15.3 +- name: OpenTelemetry.Extensions.AWS + description: OpenTelemetry extensions for AWS. + type: extension + version: 1.15.1 +- name: OpenTelemetry.Extensions.Hosting + description: Contains extensions to start OpenTelemetry in applications using Microsoft.Extensions.Hosting + type: extension + version: 1.15.3 +- name: OpenTelemetry.Extensions.Propagators + description: OpenTelemetry Extensions Propagators + type: extension + version: 1.15.3 +- name: OpenTelemetry.Instrumentation.AWS + description: AWS client instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.1 +- name: OpenTelemetry.Instrumentation.AWSLambda + description: AWS Lambda tracing wrapper for OpenTelemetry .NET. + type: instrumentation + version: 1.15.1 +- name: OpenTelemetry.Instrumentation.AspNet + description: ASP.NET instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.2 +- name: OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule + description: A module that instruments incoming request with System.Diagnostics.Activity + and notifies listeners with DiagnosticsSource. + type: instrumentation + version: 1.15.2 +- name: OpenTelemetry.Instrumentation.AspNetCore + description: ASP.NET Core instrumentation for OpenTelemetry .NET + type: instrumentation + version: 1.15.2 +- name: OpenTelemetry.Instrumentation.Http + description: HTTP instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.1 +- name: OpenTelemetry.Instrumentation.Runtime + description: .NET runtime instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.1 +- name: OpenTelemetry.Instrumentation.SqlClient + description: SqlClient instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.2 +- name: OpenTelemetry.Resources.AWS + description: OpenTelemetry Resource Detectors for AWS ElasticBeanstalk, EC2, ECS, + EKS. + type: extension + version: 1.15.1 diff --git a/ecosystem-registry/dotnet/v1.15.4-SNAPSHOT/instrumentation.yaml b/ecosystem-registry/dotnet/v1.15.4-SNAPSHOT/instrumentation.yaml new file mode 100644 index 000000000..36c58b43d --- /dev/null +++ b/ecosystem-registry/dotnet/v1.15.4-SNAPSHOT/instrumentation.yaml @@ -0,0 +1,107 @@ +modules: +- name: OpenTelemetry.Api.ProviderBuilderExtensions + description: Contains extensions to register OpenTelemetry in applications using + Microsoft.Extensions.DependencyInjection + type: extension + version: 1.15.3 +- name: OpenTelemetry.AutoInstrumentation + description: OpenTelemetry Automatic Instrumentation package with all required components + to enable automatic instrumentation. + type: instrumentation + version: 1.15.0 +- name: OpenTelemetry.AutoInstrumentation.AspNetCoreBootstrapper + description: ASP.NET Core Bootstrapper used by the OpenTelemetry.AutoInstrumentation + project. + type: instrumentation + version: 1.15.0 +- name: OpenTelemetry.AutoInstrumentation.BuildTasks + description: Build tasks used by the OpenTelemetry.AutoInstrumentation project. + type: instrumentation + version: 1.15.0 +- name: OpenTelemetry.AutoInstrumentation.Loader + description: Loader used by the OpenTelemetry.AutoInstrumentation project. + type: instrumentation + version: 1.15.0 +- name: OpenTelemetry.AutoInstrumentation.Runtime.Native + description: Native runtime components used by the OpenTelemetry.AutoInstrumentation + project. + type: instrumentation + version: 1.15.0 +- name: OpenTelemetry.AutoInstrumentation.StartupHook + description: StartupHook used by the OpenTelemetry.AutoInstrumentation project. + type: instrumentation + version: 1.15.0 +- name: OpenTelemetry.Exporter.Console + description: Console exporter for OpenTelemetry .NET + type: exporter + version: 1.15.3 +- name: OpenTelemetry.Exporter.Geneva + description: An OpenTelemetry .NET exporter that exports to local ETW or UDS. + type: exporter + version: 1.15.2 +- name: OpenTelemetry.Exporter.InMemory + description: In-memory exporter for OpenTelemetry .NET + type: exporter + version: 1.15.3 +- name: OpenTelemetry.Exporter.OneCollector + description: An OpenTelemetry .NET exporter that sends telemetry to Microsoft OneCollector. + type: exporter + version: 1.15.1 +- name: OpenTelemetry.Exporter.OpenTelemetryProtocol + description: OpenTelemetry protocol exporter for OpenTelemetry .NET + type: exporter + version: 1.15.3 +- name: OpenTelemetry.Exporter.Zipkin + description: Zipkin exporter for OpenTelemetry .NET + type: exporter + version: 1.15.3 +- name: OpenTelemetry.Extensions.AWS + description: OpenTelemetry extensions for AWS. + type: extension + version: 1.15.1 +- name: OpenTelemetry.Extensions.Hosting + description: Contains extensions to start OpenTelemetry in applications using Microsoft.Extensions.Hosting + type: extension + version: 1.15.3 +- name: OpenTelemetry.Extensions.Propagators + description: OpenTelemetry Extensions Propagators + type: extension + version: 1.15.3 +- name: OpenTelemetry.Instrumentation.AWS + description: AWS client instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.1 +- name: OpenTelemetry.Instrumentation.AWSLambda + description: AWS Lambda tracing wrapper for OpenTelemetry .NET. + type: instrumentation + version: 1.15.1 +- name: OpenTelemetry.Instrumentation.AspNet + description: ASP.NET instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.2 +- name: OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule + description: A module that instruments incoming request with System.Diagnostics.Activity + and notifies listeners with DiagnosticsSource. + type: instrumentation + version: 1.15.2 +- name: OpenTelemetry.Instrumentation.AspNetCore + description: ASP.NET Core instrumentation for OpenTelemetry .NET + type: instrumentation + version: 1.15.2 +- name: OpenTelemetry.Instrumentation.Http + description: HTTP instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.1 +- name: OpenTelemetry.Instrumentation.Runtime + description: .NET runtime instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.1 +- name: OpenTelemetry.Instrumentation.SqlClient + description: SqlClient instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.2 +- name: OpenTelemetry.Resources.AWS + description: OpenTelemetry Resource Detectors for AWS ElasticBeanstalk, EC2, ECS, + EKS. + type: extension + version: 1.15.1 From bc9f0fbfdaf3d01cbb2b59be31a36246b1d7ab9d Mon Sep 17 00:00:00 2001 From: Pittu Sharma Date: Tue, 12 May 2026 21:48:48 +0530 Subject: [PATCH 4/9] trigger ci: re-run workflows From 5091e8bbcc742cb0730ea64b13abaab8b14f6d3e Mon Sep 17 00:00:00 2001 From: Pittu Sharma Date: Wed, 13 May 2026 15:23:27 +0530 Subject: [PATCH 5/9] Address mentor feedback: Use NuGet service index, include prerelease, and improve snapshot versioning --- .../dotnet_client.py | 40 ++++++++++++++----- .../instrumentation_sync.py | 12 ++++-- .../tests/test_dotnet_client.py | 27 ++++++++++--- .../tests/test_instrumentation_sync.py | 2 +- .../instrumentation.yaml | 8 ++++ .../dotnet/v1.15.3/instrumentation.yaml | 24 ++++------- 6 files changed, 77 insertions(+), 36 deletions(-) rename ecosystem-registry/dotnet/{v1.15.4-SNAPSHOT => v1.15.3+SNAPSHOT}/instrumentation.yaml (92%) diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py index 37273dce1..95bf43bd4 100644 --- a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py +++ b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py @@ -33,13 +33,14 @@ class NuGetAPIError(Exception): class DotNetInstrumentationClient: """Client for fetching .NET instrumentation metadata from NuGet.""" - SEARCH_URL = "https://azuresearch-usnc.nuget.org/query" + SERVICE_INDEX_URL = "https://api.nuget.org/v3/index.json" OWNER = "OpenTelemetry" TIMEOUT = 30 def __init__(self): """Initialize the client.""" self._session = requests.Session() + self._search_url = None retry_strategy = Retry( total=3, @@ -50,6 +51,25 @@ def __init__(self): adapter = HTTPAdapter(max_retries=retry_strategy) self._session.mount("https://", adapter) + def _get_search_url(self) -> str: + """Resolve the search URL from the NuGet service index.""" + if self._search_url: + return self._search_url + + try: + response = self._session.get(self.SERVICE_INDEX_URL, timeout=self.TIMEOUT) + response.raise_for_status() + index_data = response.json() + for resource in index_data.get("resources", []): + if resource.get("@type") == "SearchQueryService": + self._search_url = resource.get("@id") + return self._search_url + except requests.RequestException as e: + logger.error(f"Error fetching NuGet service index: {e}") + + # Fallback to a known endpoint if index fetch fails + return "https://azuresearch-usnc.nuget.org/query" + def fetch_instrumentation_list(self) -> Dict[str, Any]: """ Fetch instrumentation list by querying NuGet for packages owned by OpenTelemetry. @@ -58,12 +78,12 @@ def fetch_instrumentation_list(self) -> Dict[str, Any]: modules = [] for pkg in all_packages: - # Skip deprecated packages - if pkg.get("deprecation"): - logger.info(f" Skipping deprecated package: {pkg.get('id')}") - continue - package_id = pkg.get("id", "") + + # Skip deprecated and Contrib packages (which are considered legacy/deprecated) + if pkg.get("deprecation") or package_id.startswith("OpenTelemetry.Contrib."): + logger.info(f" Skipping deprecated/contrib package: {package_id}") + continue version = pkg.get("version", "") description = pkg.get("description", "") @@ -105,7 +125,8 @@ def get_core_version(self) -> str: "take": 1, } try: - response = self._session.get(self.SEARCH_URL, params=params, timeout=self.TIMEOUT) + search_url = self._get_search_url() + response = self._session.get(search_url, params=params, timeout=self.TIMEOUT) response.raise_for_status() data = response.json() if data.get("data"): @@ -124,12 +145,13 @@ def _fetch_all_packages_by_owner(self, owner: str) -> List[Dict[str, Any]]: while True: params = { "q": f"owner:{owner}", - "prerelease": "false", + "prerelease": "true", "skip": skip, "take": take, } try: - response = self._session.get(self.SEARCH_URL, params=params, timeout=self.TIMEOUT) + search_url = self._get_search_url() + response = self._session.get(search_url, params=params, timeout=self.TIMEOUT) response.raise_for_status() data = response.json() diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py index c6c3e43c3..b926a6f1c 100644 --- a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py +++ b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py @@ -85,7 +85,8 @@ def process_latest_release(self) -> Version | None: try: version = Version(version_string) except ValueError: - version = Version("1.0.0") + logger.error(f"Invalid core version string: {version_string}") + return None if self.inventory_manager.version_exists(version): return None @@ -117,13 +118,16 @@ def update_snapshot(self) -> Version: try: latest_version = Version(latest_version_string) except ValueError: - latest_version = Version("1.0.0") + logger.error(f"Invalid core version string for snapshot: {latest_version_string}") + # Fallback to a safe default if needed, but better to error out if we can't even get a version + raise ValueError(f"Could not resolve a valid core version: {latest_version_string}") snapshot_version = Version( major=latest_version.major, minor=latest_version.minor, - patch=latest_version.patch + 1, - prerelease=("SNAPSHOT",), + patch=latest_version.patch, + prerelease=latest_version.prerelease, + build=("SNAPSHOT",), ) logger.info(" Fetching instrumentation list from NuGet...") diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py index 6ad9be898..75941fb66 100644 --- a/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py +++ b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py @@ -20,12 +20,18 @@ def test_get_core_version_success(): client = DotNetInstrumentationClient() with patch.object(client._session, "get") as mock_get: - mock_response = MagicMock() - mock_response.json.return_value = {"data": [{"version": "1.15.3"}]} - mock_get.return_value = mock_response + mock_index_response = MagicMock() + mock_index_response.json.return_value = { + "resources": [{"@id": "https://api.test/query", "@type": "SearchQueryService"}] + } + mock_core_response = MagicMock() + mock_core_response.json.return_value = {"data": [{"version": "1.15.3"}]} + + mock_get.side_effect = [mock_index_response, mock_core_response] version = client.get_core_version() assert version == "1.15.3" + assert mock_get.call_count == 2 def test_get_core_version_error(): @@ -35,24 +41,32 @@ def test_get_core_version_error(): mock_get.side_effect = RequestException("API error") version = client.get_core_version() + # It will try to fetch index, fail, use fallback URL, try again, fail assert version == "1.0.0" # Default fallback def test_fetch_instrumentation_list(): client = DotNetInstrumentationClient() with patch.object(client._session, "get") as mock_get: - mock_response = MagicMock() - mock_response.json.return_value = { + mock_index_response = MagicMock() + mock_index_response.json.return_value = { + "resources": [{"@id": "https://api.test/query", "@type": "SearchQueryService"}] + } + mock_search_response = MagicMock() + mock_search_response.json.return_value = { "data": [ {"id": "OpenTelemetry.Instrumentation.Test", "version": "1.0.0", "description": "Test instrumentation"}, {"id": "OpenTelemetry.Exporter.Test", "version": "1.1.0", "description": "Test exporter"}, {"id": "OpenTelemetry.Extensions.Test", "version": "1.2.0", "description": "Test extension"}, + {"id": "OpenTelemetry.Exporter.Deprecated", "version": "0.1.0", "deprecation": {"reasons": ["Legacy"]}}, + {"id": "OpenTelemetry.Contrib.Test", "version": "1.0.0", "description": "Contrib package"}, ] } - mock_get.return_value = mock_response + mock_get.side_effect = [mock_index_response, mock_search_response] result = client.fetch_instrumentation_list() assert "modules" in result + # 3 valid ones, 1 deprecated, 1 contrib skipped assert len(result["modules"]) == 3 types = [m["type"] for m in result["modules"]] @@ -60,6 +74,7 @@ def test_fetch_instrumentation_list(): assert "exporter" in types assert "extension" in types + # Sorted by name: Exporter.Test, Extensions.Test, Instrumentation.Test assert result["modules"][0]["name"] == "OpenTelemetry.Exporter.Test" assert result["modules"][0]["version"] == "1.1.0" assert result["modules"][2]["name"] == "OpenTelemetry.Instrumentation.Test" diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_instrumentation_sync.py b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_instrumentation_sync.py index 9ee42b54b..cdf918cdb 100644 --- a/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_instrumentation_sync.py +++ b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_instrumentation_sync.py @@ -62,6 +62,6 @@ def test_update_snapshot(mock_client, mock_inventory): sync = InstrumentationSync(mock_client, mock_inventory) result = sync.update_snapshot() - assert result == Version("1.2.4-SNAPSHOT") + assert result == Version("1.2.3+SNAPSHOT") mock_inventory.cleanup_snapshots.assert_called_once() mock_inventory.save_versioned_inventory.assert_called_once() diff --git a/ecosystem-registry/dotnet/v1.15.4-SNAPSHOT/instrumentation.yaml b/ecosystem-registry/dotnet/v1.15.3+SNAPSHOT/instrumentation.yaml similarity index 92% rename from ecosystem-registry/dotnet/v1.15.4-SNAPSHOT/instrumentation.yaml rename to ecosystem-registry/dotnet/v1.15.3+SNAPSHOT/instrumentation.yaml index 36c58b43d..d750232cf 100644 --- a/ecosystem-registry/dotnet/v1.15.4-SNAPSHOT/instrumentation.yaml +++ b/ecosystem-registry/dotnet/v1.15.3+SNAPSHOT/instrumentation.yaml @@ -88,6 +88,10 @@ modules: description: ASP.NET Core instrumentation for OpenTelemetry .NET type: instrumentation version: 1.15.2 +- name: OpenTelemetry.Instrumentation.GrpcNetClient + description: gRPC for .NET client instrumentation for OpenTelemetry .NET + type: instrumentation + version: 1.0.0-rc9 - name: OpenTelemetry.Instrumentation.Http description: HTTP instrumentation for OpenTelemetry .NET. type: instrumentation @@ -100,6 +104,10 @@ modules: description: SqlClient instrumentation for OpenTelemetry .NET. type: instrumentation version: 1.15.2 +- name: OpenTelemetry.Instrumentation.StackExchangeRedis + description: StackExchange.Redis instrumentation for OpenTelemetry .NET + type: instrumentation + version: 1.0.0-rc9 - name: OpenTelemetry.Resources.AWS description: OpenTelemetry Resource Detectors for AWS ElasticBeanstalk, EC2, ECS, EKS. diff --git a/ecosystem-registry/dotnet/v1.15.3/instrumentation.yaml b/ecosystem-registry/dotnet/v1.15.3/instrumentation.yaml index 55e8a41f7..d750232cf 100644 --- a/ecosystem-registry/dotnet/v1.15.3/instrumentation.yaml +++ b/ecosystem-registry/dotnet/v1.15.3/instrumentation.yaml @@ -31,14 +31,6 @@ modules: description: StartupHook used by the OpenTelemetry.AutoInstrumentation project. type: instrumentation version: 1.15.0 -- name: OpenTelemetry.Contrib.Extensions.AWSXRay - description: OpenTelemetry extensions for AWS X-Ray. - type: extension - version: 1.2.0 -- name: OpenTelemetry.Contrib.Instrumentation.AWS - description: AWS client instrumentation for OpenTelemetry .NET - type: instrumentation - version: 1.0.2 - name: OpenTelemetry.Exporter.Console description: Console exporter for OpenTelemetry .NET type: exporter @@ -51,14 +43,6 @@ modules: description: In-memory exporter for OpenTelemetry .NET type: exporter version: 1.15.3 -- name: OpenTelemetry.Exporter.Instana - description: Instana .NET Exporter for OpenTelemetry. - type: exporter - version: 1.1.0 -- name: OpenTelemetry.Exporter.Jaeger - description: Jaeger exporter for OpenTelemetry .NET - type: exporter - version: 1.5.1 - name: OpenTelemetry.Exporter.OneCollector description: An OpenTelemetry .NET exporter that sends telemetry to Microsoft OneCollector. type: exporter @@ -104,6 +88,10 @@ modules: description: ASP.NET Core instrumentation for OpenTelemetry .NET type: instrumentation version: 1.15.2 +- name: OpenTelemetry.Instrumentation.GrpcNetClient + description: gRPC for .NET client instrumentation for OpenTelemetry .NET + type: instrumentation + version: 1.0.0-rc9 - name: OpenTelemetry.Instrumentation.Http description: HTTP instrumentation for OpenTelemetry .NET. type: instrumentation @@ -116,6 +104,10 @@ modules: description: SqlClient instrumentation for OpenTelemetry .NET. type: instrumentation version: 1.15.2 +- name: OpenTelemetry.Instrumentation.StackExchangeRedis + description: StackExchange.Redis instrumentation for OpenTelemetry .NET + type: instrumentation + version: 1.0.0-rc9 - name: OpenTelemetry.Resources.AWS description: OpenTelemetry Resource Detectors for AWS ElasticBeanstalk, EC2, ECS, EKS. From 7e7065b9944f515a447dff7cd921c77414d6c264 Mon Sep 17 00:00:00 2001 From: Pittu Sharma Date: Wed, 13 May 2026 20:01:39 +0530 Subject: [PATCH 6/9] fix(dotnet-watcher): address mentor review feedback - NuGet V3 Service Index (dynamic endpoint discovery) - Error on service index failure instead of fallback - Remove 1.0.0 version fallback; raise NuGetAPIError - Filter deprecated packages using NuGet metadata (covers .Contrib) - Correct version sorting by using top-level version field - Add prerelease package support in tests - Restore 'primary' variant to GlowBadge for UI compatibility --- .../dotnet_client.py | 60 +++++++----- .../instrumentation_sync.py | 11 +-- .../tests/test_dotnet_client.py | 97 ++++++++++++++----- .../src/components/ui/glow-badge.tsx | 16 ++- 4 files changed, 126 insertions(+), 58 deletions(-) diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py index 95bf43bd4..e8c7d96f8 100644 --- a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py +++ b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py @@ -52,7 +52,12 @@ def __init__(self): self._session.mount("https://", adapter) def _get_search_url(self) -> str: - """Resolve the search URL from the NuGet service index.""" + """Resolve the search URL from the NuGet service index. + + Raises: + NuGetAPIError: If the service index cannot be fetched or does not + contain a SearchQueryService resource. + """ if self._search_url: return self._search_url @@ -60,19 +65,22 @@ def _get_search_url(self) -> str: response = self._session.get(self.SERVICE_INDEX_URL, timeout=self.TIMEOUT) response.raise_for_status() index_data = response.json() - for resource in index_data.get("resources", []): - if resource.get("@type") == "SearchQueryService": - self._search_url = resource.get("@id") - return self._search_url except requests.RequestException as e: - logger.error(f"Error fetching NuGet service index: {e}") + raise NuGetAPIError(f"Error fetching NuGet service index: {e}") from e + + for resource in index_data.get("resources", []): + if resource.get("@type") == "SearchQueryService": + self._search_url = resource["@id"] + return self._search_url - # Fallback to a known endpoint if index fetch fails - return "https://azuresearch-usnc.nuget.org/query" + raise NuGetAPIError("NuGet service index did not contain a SearchQueryService resource") def fetch_instrumentation_list(self) -> Dict[str, Any]: """ Fetch instrumentation list by querying NuGet for packages owned by OpenTelemetry. + + The top-level ``version`` field in each search result entry is the latest + version of the package as reported by NuGet — no local sorting is needed. """ all_packages = self._fetch_all_packages_by_owner(self.OWNER) modules = [] @@ -80,10 +88,13 @@ def fetch_instrumentation_list(self) -> Dict[str, Any]: for pkg in all_packages: package_id = pkg.get("id", "") - # Skip deprecated and Contrib packages (which are considered legacy/deprecated) - if pkg.get("deprecation") or package_id.startswith("OpenTelemetry.Contrib."): - logger.info(f" Skipping deprecated/contrib package: {package_id}") + # Skip packages flagged as deprecated by NuGet (includes Contrib packages). + if pkg.get("deprecation"): + logger.info(f" Skipping deprecated package: {package_id}") continue + + # The top-level "version" field is the latest version returned by the + # NuGet search API — rely on the server ordering rather than sorting locally. version = pkg.get("version", "") description = pkg.get("description", "") @@ -95,9 +106,7 @@ def fetch_instrumentation_list(self) -> Dict[str, Any]: elif "Extensions" in package_id or "Resources" in package_id or "Sampler" in package_id: component_type = "extension" else: - # Skip core packages like OpenTelemetry, OpenTelemetry.Api unless they match patterns - # but we might want to include them as 'core' if needed. - # For now, let's stick to the previous classification logic. + # Skip core and unclassified packages. continue modules.append( @@ -109,15 +118,18 @@ def fetch_instrumentation_list(self) -> Dict[str, Any]: } ) - # Sort by name for consistency + # Sort by name for deterministic registry output. modules.sort(key=lambda x: x["name"]) return {"modules": modules} def get_core_version(self) -> str: - """ - Get the latest version of the core OpenTelemetry package. + """Get the latest stable version of the core OpenTelemetry package. + This is used as the 'ecosystem version' for the registry. + + Raises: + NuGetAPIError: If the version cannot be determined. """ params = { "q": "PackageId:OpenTelemetry", @@ -129,12 +141,14 @@ def get_core_version(self) -> str: response = self._session.get(search_url, params=params, timeout=self.TIMEOUT) response.raise_for_status() data = response.json() - if data.get("data"): - return data["data"][0]["version"] - return "1.0.0" - except (requests.RequestException, KeyError, IndexError) as e: - logger.error(f"Error fetching core version: {e}") - return "1.0.0" + results = data.get("data", []) + if not results: + raise NuGetAPIError("No results returned for core OpenTelemetry package") + return results[0]["version"] + except (KeyError, IndexError) as e: + raise NuGetAPIError(f"Unexpected response shape fetching core version: {e}") from e + except requests.RequestException as e: + raise NuGetAPIError(f"Error fetching core version: {e}") from e def _fetch_all_packages_by_owner(self, owner: str) -> List[Dict[str, Any]]: """Fetch all packages for a specific owner using pagination.""" diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py index b926a6f1c..1c3f72eaa 100644 --- a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py +++ b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/instrumentation_sync.py @@ -84,9 +84,8 @@ def process_latest_release(self) -> Version | None: try: version = Version(version_string) - except ValueError: - logger.error(f"Invalid core version string: {version_string}") - return None + except ValueError as e: + raise ValueError(f"Invalid core version string: {version_string!r}") from e if self.inventory_manager.version_exists(version): return None @@ -117,10 +116,8 @@ def update_snapshot(self) -> Version: latest_version_string = self.client.get_core_version() try: latest_version = Version(latest_version_string) - except ValueError: - logger.error(f"Invalid core version string for snapshot: {latest_version_string}") - # Fallback to a safe default if needed, but better to error out if we can't even get a version - raise ValueError(f"Could not resolve a valid core version: {latest_version_string}") + except ValueError as e: + raise ValueError(f"Could not resolve a valid core version: {latest_version_string!r}") from e snapshot_version = Version( major=latest_version.major, diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py index 75941fb66..45d498848 100644 --- a/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py +++ b/ecosystem-automation/dotnet-instrumentation-watcher/tests/test_dotnet_client.py @@ -14,59 +14,99 @@ # from unittest.mock import MagicMock, patch -from dotnet_instrumentation_watcher.dotnet_client import DotNetInstrumentationClient +import pytest +from dotnet_instrumentation_watcher.dotnet_client import ( + DotNetInstrumentationClient, + NuGetAPIError, +) +from requests import RequestException + + +def _make_index_response(): + """Return a mock NuGet service index response.""" + mock = MagicMock() + mock.json.return_value = {"resources": [{"@id": "https://api.test/query", "@type": "SearchQueryService"}]} + return mock def test_get_core_version_success(): client = DotNetInstrumentationClient() with patch.object(client._session, "get") as mock_get: - mock_index_response = MagicMock() - mock_index_response.json.return_value = { - "resources": [{"@id": "https://api.test/query", "@type": "SearchQueryService"}] - } - mock_core_response = MagicMock() - mock_core_response.json.return_value = {"data": [{"version": "1.15.3"}]} - - mock_get.side_effect = [mock_index_response, mock_core_response] + mock_get.side_effect = [ + _make_index_response(), + MagicMock(**{"json.return_value": {"data": [{"version": "1.15.3"}]}}), + ] version = client.get_core_version() assert version == "1.15.3" assert mock_get.call_count == 2 -def test_get_core_version_error(): +def test_get_core_version_raises_on_api_error(): + """get_core_version should propagate NuGetAPIError instead of returning a fallback.""" client = DotNetInstrumentationClient() with patch.object(client._session, "get") as mock_get: - from requests import RequestException - mock_get.side_effect = RequestException("API error") - version = client.get_core_version() - # It will try to fetch index, fail, use fallback URL, try again, fail - assert version == "1.0.0" # Default fallback + with pytest.raises(NuGetAPIError): + client.get_core_version() + + +def test_get_search_url_raises_when_service_index_missing_resource(): + """_get_search_url should raise NuGetAPIError if no SearchQueryService is in the index.""" + client = DotNetInstrumentationClient() + with patch.object(client._session, "get") as mock_get: + mock_resp = MagicMock() + mock_resp.json.return_value = {"resources": []} # no SearchQueryService entry + mock_get.return_value = mock_resp + with pytest.raises(NuGetAPIError, match="SearchQueryService"): + client._get_search_url() def test_fetch_instrumentation_list(): + """Packages are filtered by deprecation flag only; prerelease versions are supported.""" client = DotNetInstrumentationClient() with patch.object(client._session, "get") as mock_get: - mock_index_response = MagicMock() - mock_index_response.json.return_value = { - "resources": [{"@id": "https://api.test/query", "@type": "SearchQueryService"}] - } mock_search_response = MagicMock() mock_search_response.json.return_value = { "data": [ - {"id": "OpenTelemetry.Instrumentation.Test", "version": "1.0.0", "description": "Test instrumentation"}, - {"id": "OpenTelemetry.Exporter.Test", "version": "1.1.0", "description": "Test exporter"}, - {"id": "OpenTelemetry.Extensions.Test", "version": "1.2.0", "description": "Test extension"}, - {"id": "OpenTelemetry.Exporter.Deprecated", "version": "0.1.0", "deprecation": {"reasons": ["Legacy"]}}, - {"id": "OpenTelemetry.Contrib.Test", "version": "1.0.0", "description": "Contrib package"}, + # Stable instrumentation + { + "id": "OpenTelemetry.Instrumentation.Test", + "version": "1.0.0", + "description": "Test instrumentation", + }, + # Stable exporter + { + "id": "OpenTelemetry.Exporter.Test", + "version": "1.1.0", + "description": "Test exporter", + }, + # Extension with a prerelease version (e.g. 1.2.3-preview.4) + { + "id": "OpenTelemetry.Extensions.Test", + "version": "1.2.3-preview.4", + "description": "Test extension", + }, + # Deprecated by NuGet — must be skipped + { + "id": "OpenTelemetry.Exporter.Deprecated", + "version": "0.1.0", + "deprecation": {"reasons": ["Legacy"]}, + }, + # Contrib package — deprecated in NuGet, so covered by the deprecation check + { + "id": "OpenTelemetry.Contrib.Instrumentation.Legacy", + "version": "1.0.0", + "description": "Legacy contrib package", + "deprecation": {"reasons": ["Legacy"]}, + }, ] } - mock_get.side_effect = [mock_index_response, mock_search_response] + mock_get.side_effect = [_make_index_response(), mock_search_response] result = client.fetch_instrumentation_list() assert "modules" in result - # 3 valid ones, 1 deprecated, 1 contrib skipped + # 3 valid packages (1 deprecated + 1 deprecated contrib are skipped) assert len(result["modules"]) == 3 types = [m["type"] for m in result["modules"]] @@ -77,5 +117,10 @@ def test_fetch_instrumentation_list(): # Sorted by name: Exporter.Test, Extensions.Test, Instrumentation.Test assert result["modules"][0]["name"] == "OpenTelemetry.Exporter.Test" assert result["modules"][0]["version"] == "1.1.0" + + # Extension carries a prerelease version — must be preserved as-is + assert result["modules"][1]["name"] == "OpenTelemetry.Extensions.Test" + assert result["modules"][1]["version"] == "1.2.3-preview.4" + assert result["modules"][2]["name"] == "OpenTelemetry.Instrumentation.Test" assert result["modules"][2]["version"] == "1.0.0" diff --git a/ecosystem-explorer/src/components/ui/glow-badge.tsx b/ecosystem-explorer/src/components/ui/glow-badge.tsx index fad95386a..f965ed6f6 100644 --- a/ecosystem-explorer/src/components/ui/glow-badge.tsx +++ b/ecosystem-explorer/src/components/ui/glow-badge.tsx @@ -15,7 +15,15 @@ */ import React from "react"; -type BadgeVariant = "accent" | "secondary" | "success" | "info" | "warning" | "error" | "muted"; +type BadgeVariant = + | "primary" + | "accent" + | "secondary" + | "success" + | "info" + | "warning" + | "error" + | "muted"; interface GlowBadgeProps { children: React.ReactNode; @@ -25,6 +33,10 @@ interface GlowBadgeProps { } const variantStyles: Record = { + primary: { + base: "bg-primary/10 border-primary/30 text-primary", + glow: "shadow-sm shadow-primary/20", + }, accent: { base: "bg-secondary/10 border-secondary/30 text-secondary", glow: "shadow-sm shadow-secondary/20", @@ -61,7 +73,7 @@ export function GlowBadge({ withGlow = false, className = "", }: GlowBadgeProps) { - const styles = variantStyles[variant]; + const styles = variantStyles[variant] || variantStyles["accent"]; const glowClass = withGlow ? styles.glow : ""; return ( From 917a26432c7e62e71a29232f6cdd37144635d213 Mon Sep 17 00:00:00 2001 From: Pittu Sharma Date: Thu, 14 May 2026 21:03:54 +0530 Subject: [PATCH 7/9] fix(.net-watcher): correct versioning and filter deprecated packages - Added semVerLevel=2.0.0 to NuGet API queries to include newer OTel versions - Updated logic to use top-level 'version' field from NuGet as source of truth - Implemented filtering based on NuGet 'deprecation' flag - Regenerated .NET instrumentation registry with correct versions --- .../dotnet_client.py | 2 + .../v1.15.3+SNAPSHOT/instrumentation.yaml | 133 ++++++++++++++++-- .../dotnet/v1.15.3/instrumentation.yaml | 133 ++++++++++++++++-- 3 files changed, 248 insertions(+), 20 deletions(-) diff --git a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py index e8c7d96f8..bc753cf58 100644 --- a/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py +++ b/ecosystem-automation/dotnet-instrumentation-watcher/src/dotnet_instrumentation_watcher/dotnet_client.py @@ -134,6 +134,7 @@ def get_core_version(self) -> str: params = { "q": "PackageId:OpenTelemetry", "prerelease": "false", + "semVerLevel": "2.0.0", "take": 1, } try: @@ -160,6 +161,7 @@ def _fetch_all_packages_by_owner(self, owner: str) -> List[Dict[str, Any]]: params = { "q": f"owner:{owner}", "prerelease": "true", + "semVerLevel": "2.0.0", "skip": skip, "take": take, } diff --git a/ecosystem-registry/dotnet/v1.15.3+SNAPSHOT/instrumentation.yaml b/ecosystem-registry/dotnet/v1.15.3+SNAPSHOT/instrumentation.yaml index d750232cf..1ffcb08c8 100644 --- a/ecosystem-registry/dotnet/v1.15.3+SNAPSHOT/instrumentation.yaml +++ b/ecosystem-registry/dotnet/v1.15.3+SNAPSHOT/instrumentation.yaml @@ -8,29 +8,33 @@ modules: description: OpenTelemetry Automatic Instrumentation package with all required components to enable automatic instrumentation. type: instrumentation - version: 1.15.0 + version: 1.16.0-beta.1 - name: OpenTelemetry.AutoInstrumentation.AspNetCoreBootstrapper description: ASP.NET Core Bootstrapper used by the OpenTelemetry.AutoInstrumentation project. type: instrumentation - version: 1.15.0 + version: 1.16.0-beta.1 - name: OpenTelemetry.AutoInstrumentation.BuildTasks description: Build tasks used by the OpenTelemetry.AutoInstrumentation project. type: instrumentation - version: 1.15.0 + version: 1.16.0-beta.1 - name: OpenTelemetry.AutoInstrumentation.Loader description: Loader used by the OpenTelemetry.AutoInstrumentation project. type: instrumentation - version: 1.15.0 + version: 1.16.0-beta.1 +- name: OpenTelemetry.AutoInstrumentation.Runtime.Managed + description: Managed components used by the OpenTelemetry.AutoInstrumentation project. + type: instrumentation + version: 1.16.0-beta.1 - name: OpenTelemetry.AutoInstrumentation.Runtime.Native description: Native runtime components used by the OpenTelemetry.AutoInstrumentation project. type: instrumentation - version: 1.15.0 + version: 1.16.0-beta.1 - name: OpenTelemetry.AutoInstrumentation.StartupHook description: StartupHook used by the OpenTelemetry.AutoInstrumentation project. type: instrumentation - version: 1.15.0 + version: 1.16.0-beta.1 - name: OpenTelemetry.Exporter.Console description: Console exporter for OpenTelemetry .NET type: exporter @@ -43,6 +47,10 @@ modules: description: In-memory exporter for OpenTelemetry .NET type: exporter version: 1.15.3 +- name: OpenTelemetry.Exporter.InfluxDB + description: An OpenTelemetry .NET exporter that exports to InfluxDB. + type: exporter + version: 1.0.0-alpha.8 - name: OpenTelemetry.Exporter.OneCollector description: An OpenTelemetry .NET exporter that sends telemetry to Microsoft OneCollector. type: exporter @@ -51,14 +59,39 @@ modules: description: OpenTelemetry protocol exporter for OpenTelemetry .NET type: exporter version: 1.15.3 +- name: OpenTelemetry.Exporter.Prometheus.AspNetCore + description: ASP.NET Core middleware for hosting OpenTelemetry .NET Prometheus Exporter + type: exporter + version: 1.15.3-beta.1 +- name: OpenTelemetry.Exporter.Prometheus.HttpListener + description: Stand-alone HttpListener for hosting OpenTelemetry .NET Prometheus + Exporter + type: exporter + version: 1.15.3-beta.1 - name: OpenTelemetry.Exporter.Zipkin description: Zipkin exporter for OpenTelemetry .NET type: exporter version: 1.15.3 +- name: OpenTelemetry.Extensions + description: OpenTelemetry .NET SDK preview features and extensions. + type: extension + version: 1.15.0-beta.1 - name: OpenTelemetry.Extensions.AWS description: OpenTelemetry extensions for AWS. type: extension version: 1.15.1 +- name: OpenTelemetry.Extensions.Enrichment + description: OpenTelemetry .NET SDK telemetry enrichment. + type: extension + version: 1.15.1-beta.1 +- name: OpenTelemetry.Extensions.Enrichment.AspNetCore + description: OpenTelemetry .NET SDK ASP.NET Core telemetry enrichment. + type: extension + version: 1.15.1-beta.2 +- name: OpenTelemetry.Extensions.Enrichment.Http + description: OpenTelemetry .NET SDK HTTP telemetry enrichment. + type: extension + version: 1.15.1-beta.2 - name: OpenTelemetry.Extensions.Hosting description: Contains extensions to start OpenTelemetry in applications using Microsoft.Extensions.Hosting type: extension @@ -88,28 +121,108 @@ modules: description: ASP.NET Core instrumentation for OpenTelemetry .NET type: instrumentation version: 1.15.2 +- name: OpenTelemetry.Instrumentation.Cassandra + description: OpenTelemetry Cassandra Instrumentation. + type: instrumentation + version: 1.0.0-beta.6 +- name: OpenTelemetry.Instrumentation.ConfluentKafka + description: Confluent.Kafka instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 0.1.0-alpha.6 +- name: OpenTelemetry.Instrumentation.ElasticsearchClient + description: Elasticsearch instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.1-beta.1 +- name: OpenTelemetry.Instrumentation.EntityFrameworkCore + description: Microsoft.EntityFrameworkCore instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.1-beta.1 +- name: OpenTelemetry.Instrumentation.EventCounters + description: OpenTelemetry Metrics instrumentation for .NET EventCounters. + type: instrumentation + version: 1.15.1-alpha.1 +- name: OpenTelemetry.Instrumentation.GrpcCore + description: .NET gRPC Core based client and server interceptors for OpenTelemetry. + type: instrumentation + version: 1.0.0-beta.11 - name: OpenTelemetry.Instrumentation.GrpcNetClient - description: gRPC for .NET client instrumentation for OpenTelemetry .NET + description: gRPC for .NET client instrumentation for OpenTelemetry .NET. type: instrumentation - version: 1.0.0-rc9 + version: 1.15.1-beta.1 +- name: OpenTelemetry.Instrumentation.Hangfire + description: OpenTelemetry Hangfire Instrumentation. + type: instrumentation + version: 1.15.1-beta.1 - name: OpenTelemetry.Instrumentation.Http description: HTTP instrumentation for OpenTelemetry .NET. type: instrumentation version: 1.15.1 +- name: OpenTelemetry.Instrumentation.Owin + description: OpenTelemetry instrumentation for OWIN. + type: instrumentation + version: 1.15.1-beta.1 +- name: OpenTelemetry.Instrumentation.Process + description: dotnet process instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.1-beta.1 +- name: OpenTelemetry.Instrumentation.Quartz + description: OpenTelemetry Quartz.NET Instrumentation. + type: instrumentation + version: 1.15.1-beta.1 - name: OpenTelemetry.Instrumentation.Runtime description: .NET runtime instrumentation for OpenTelemetry .NET. type: instrumentation version: 1.15.1 +- name: OpenTelemetry.Instrumentation.ServiceFabricRemoting + description: ServiceFabric Remoting instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.1-beta.1 - name: OpenTelemetry.Instrumentation.SqlClient description: SqlClient instrumentation for OpenTelemetry .NET. type: instrumentation version: 1.15.2 - name: OpenTelemetry.Instrumentation.StackExchangeRedis - description: StackExchange.Redis instrumentation for OpenTelemetry .NET + description: StackExchange.Redis instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.1-beta.1 +- name: OpenTelemetry.Instrumentation.Wcf + description: OpenTelemetry instrumentation for WCF. type: instrumentation - version: 1.0.0-rc9 + version: 1.15.1-beta.2 - name: OpenTelemetry.Resources.AWS description: OpenTelemetry Resource Detectors for AWS ElasticBeanstalk, EC2, ECS, EKS. type: extension version: 1.15.1 +- name: OpenTelemetry.Resources.Azure + description: OpenTelemetry Resource Detectors for Azure cloud environments. + type: extension + version: 1.15.1-beta.1 +- name: OpenTelemetry.Resources.Container + description: OpenTelemetry Resource Detectors for Container environment. + type: extension + version: 1.15.1-beta.1 +- name: OpenTelemetry.Resources.Gcp + description: OpenTelemetry Resource Detectors for Google Cloud Platform environments. + type: extension + version: 1.0.0-alpha.1 +- name: OpenTelemetry.Resources.Host + description: OpenTelemetry Resource Detectors for Host. + type: extension + version: 1.15.1-beta.1 +- name: OpenTelemetry.Resources.OperatingSystem + description: OpenTelemetry Resource Detectors for Operating System. + type: extension + version: 1.15.1-beta.1 +- name: OpenTelemetry.Resources.Process + description: OpenTelemetry Resource Detectors for Process. + type: extension + version: 1.15.1-beta.2 +- name: OpenTelemetry.Resources.ProcessRuntime + description: OpenTelemetry Resource Detectors for Process Runtime. + type: extension + version: 1.15.1-beta.1 +- name: OpenTelemetry.Sampler.AWS + description: OpenTelemetry remote sampler for AWS X-Ray. + type: extension + version: 0.1.0-alpha.9 diff --git a/ecosystem-registry/dotnet/v1.15.3/instrumentation.yaml b/ecosystem-registry/dotnet/v1.15.3/instrumentation.yaml index d750232cf..1ffcb08c8 100644 --- a/ecosystem-registry/dotnet/v1.15.3/instrumentation.yaml +++ b/ecosystem-registry/dotnet/v1.15.3/instrumentation.yaml @@ -8,29 +8,33 @@ modules: description: OpenTelemetry Automatic Instrumentation package with all required components to enable automatic instrumentation. type: instrumentation - version: 1.15.0 + version: 1.16.0-beta.1 - name: OpenTelemetry.AutoInstrumentation.AspNetCoreBootstrapper description: ASP.NET Core Bootstrapper used by the OpenTelemetry.AutoInstrumentation project. type: instrumentation - version: 1.15.0 + version: 1.16.0-beta.1 - name: OpenTelemetry.AutoInstrumentation.BuildTasks description: Build tasks used by the OpenTelemetry.AutoInstrumentation project. type: instrumentation - version: 1.15.0 + version: 1.16.0-beta.1 - name: OpenTelemetry.AutoInstrumentation.Loader description: Loader used by the OpenTelemetry.AutoInstrumentation project. type: instrumentation - version: 1.15.0 + version: 1.16.0-beta.1 +- name: OpenTelemetry.AutoInstrumentation.Runtime.Managed + description: Managed components used by the OpenTelemetry.AutoInstrumentation project. + type: instrumentation + version: 1.16.0-beta.1 - name: OpenTelemetry.AutoInstrumentation.Runtime.Native description: Native runtime components used by the OpenTelemetry.AutoInstrumentation project. type: instrumentation - version: 1.15.0 + version: 1.16.0-beta.1 - name: OpenTelemetry.AutoInstrumentation.StartupHook description: StartupHook used by the OpenTelemetry.AutoInstrumentation project. type: instrumentation - version: 1.15.0 + version: 1.16.0-beta.1 - name: OpenTelemetry.Exporter.Console description: Console exporter for OpenTelemetry .NET type: exporter @@ -43,6 +47,10 @@ modules: description: In-memory exporter for OpenTelemetry .NET type: exporter version: 1.15.3 +- name: OpenTelemetry.Exporter.InfluxDB + description: An OpenTelemetry .NET exporter that exports to InfluxDB. + type: exporter + version: 1.0.0-alpha.8 - name: OpenTelemetry.Exporter.OneCollector description: An OpenTelemetry .NET exporter that sends telemetry to Microsoft OneCollector. type: exporter @@ -51,14 +59,39 @@ modules: description: OpenTelemetry protocol exporter for OpenTelemetry .NET type: exporter version: 1.15.3 +- name: OpenTelemetry.Exporter.Prometheus.AspNetCore + description: ASP.NET Core middleware for hosting OpenTelemetry .NET Prometheus Exporter + type: exporter + version: 1.15.3-beta.1 +- name: OpenTelemetry.Exporter.Prometheus.HttpListener + description: Stand-alone HttpListener for hosting OpenTelemetry .NET Prometheus + Exporter + type: exporter + version: 1.15.3-beta.1 - name: OpenTelemetry.Exporter.Zipkin description: Zipkin exporter for OpenTelemetry .NET type: exporter version: 1.15.3 +- name: OpenTelemetry.Extensions + description: OpenTelemetry .NET SDK preview features and extensions. + type: extension + version: 1.15.0-beta.1 - name: OpenTelemetry.Extensions.AWS description: OpenTelemetry extensions for AWS. type: extension version: 1.15.1 +- name: OpenTelemetry.Extensions.Enrichment + description: OpenTelemetry .NET SDK telemetry enrichment. + type: extension + version: 1.15.1-beta.1 +- name: OpenTelemetry.Extensions.Enrichment.AspNetCore + description: OpenTelemetry .NET SDK ASP.NET Core telemetry enrichment. + type: extension + version: 1.15.1-beta.2 +- name: OpenTelemetry.Extensions.Enrichment.Http + description: OpenTelemetry .NET SDK HTTP telemetry enrichment. + type: extension + version: 1.15.1-beta.2 - name: OpenTelemetry.Extensions.Hosting description: Contains extensions to start OpenTelemetry in applications using Microsoft.Extensions.Hosting type: extension @@ -88,28 +121,108 @@ modules: description: ASP.NET Core instrumentation for OpenTelemetry .NET type: instrumentation version: 1.15.2 +- name: OpenTelemetry.Instrumentation.Cassandra + description: OpenTelemetry Cassandra Instrumentation. + type: instrumentation + version: 1.0.0-beta.6 +- name: OpenTelemetry.Instrumentation.ConfluentKafka + description: Confluent.Kafka instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 0.1.0-alpha.6 +- name: OpenTelemetry.Instrumentation.ElasticsearchClient + description: Elasticsearch instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.1-beta.1 +- name: OpenTelemetry.Instrumentation.EntityFrameworkCore + description: Microsoft.EntityFrameworkCore instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.1-beta.1 +- name: OpenTelemetry.Instrumentation.EventCounters + description: OpenTelemetry Metrics instrumentation for .NET EventCounters. + type: instrumentation + version: 1.15.1-alpha.1 +- name: OpenTelemetry.Instrumentation.GrpcCore + description: .NET gRPC Core based client and server interceptors for OpenTelemetry. + type: instrumentation + version: 1.0.0-beta.11 - name: OpenTelemetry.Instrumentation.GrpcNetClient - description: gRPC for .NET client instrumentation for OpenTelemetry .NET + description: gRPC for .NET client instrumentation for OpenTelemetry .NET. type: instrumentation - version: 1.0.0-rc9 + version: 1.15.1-beta.1 +- name: OpenTelemetry.Instrumentation.Hangfire + description: OpenTelemetry Hangfire Instrumentation. + type: instrumentation + version: 1.15.1-beta.1 - name: OpenTelemetry.Instrumentation.Http description: HTTP instrumentation for OpenTelemetry .NET. type: instrumentation version: 1.15.1 +- name: OpenTelemetry.Instrumentation.Owin + description: OpenTelemetry instrumentation for OWIN. + type: instrumentation + version: 1.15.1-beta.1 +- name: OpenTelemetry.Instrumentation.Process + description: dotnet process instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.1-beta.1 +- name: OpenTelemetry.Instrumentation.Quartz + description: OpenTelemetry Quartz.NET Instrumentation. + type: instrumentation + version: 1.15.1-beta.1 - name: OpenTelemetry.Instrumentation.Runtime description: .NET runtime instrumentation for OpenTelemetry .NET. type: instrumentation version: 1.15.1 +- name: OpenTelemetry.Instrumentation.ServiceFabricRemoting + description: ServiceFabric Remoting instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.1-beta.1 - name: OpenTelemetry.Instrumentation.SqlClient description: SqlClient instrumentation for OpenTelemetry .NET. type: instrumentation version: 1.15.2 - name: OpenTelemetry.Instrumentation.StackExchangeRedis - description: StackExchange.Redis instrumentation for OpenTelemetry .NET + description: StackExchange.Redis instrumentation for OpenTelemetry .NET. + type: instrumentation + version: 1.15.1-beta.1 +- name: OpenTelemetry.Instrumentation.Wcf + description: OpenTelemetry instrumentation for WCF. type: instrumentation - version: 1.0.0-rc9 + version: 1.15.1-beta.2 - name: OpenTelemetry.Resources.AWS description: OpenTelemetry Resource Detectors for AWS ElasticBeanstalk, EC2, ECS, EKS. type: extension version: 1.15.1 +- name: OpenTelemetry.Resources.Azure + description: OpenTelemetry Resource Detectors for Azure cloud environments. + type: extension + version: 1.15.1-beta.1 +- name: OpenTelemetry.Resources.Container + description: OpenTelemetry Resource Detectors for Container environment. + type: extension + version: 1.15.1-beta.1 +- name: OpenTelemetry.Resources.Gcp + description: OpenTelemetry Resource Detectors for Google Cloud Platform environments. + type: extension + version: 1.0.0-alpha.1 +- name: OpenTelemetry.Resources.Host + description: OpenTelemetry Resource Detectors for Host. + type: extension + version: 1.15.1-beta.1 +- name: OpenTelemetry.Resources.OperatingSystem + description: OpenTelemetry Resource Detectors for Operating System. + type: extension + version: 1.15.1-beta.1 +- name: OpenTelemetry.Resources.Process + description: OpenTelemetry Resource Detectors for Process. + type: extension + version: 1.15.1-beta.2 +- name: OpenTelemetry.Resources.ProcessRuntime + description: OpenTelemetry Resource Detectors for Process Runtime. + type: extension + version: 1.15.1-beta.1 +- name: OpenTelemetry.Sampler.AWS + description: OpenTelemetry remote sampler for AWS X-Ray. + type: extension + version: 0.1.0-alpha.9 From 880f80551ea81e745ba365700a6e09077700d057 Mon Sep 17 00:00:00 2001 From: Pittu Sharma Date: Thu, 14 May 2026 22:49:55 +0530 Subject: [PATCH 8/9] refactor(api): address copilot feedback on resilience and caching --- .../src/lib/api/fetch-with-cache.test.ts | 160 +++++++++++++++--- .../src/lib/api/fetch-with-cache.ts | 98 ++++++++++- .../src/lib/api/idb-cache.test.ts | 60 ++++++- ecosystem-explorer/src/lib/api/idb-cache.ts | 53 +++++- 4 files changed, 335 insertions(+), 36 deletions(-) diff --git a/ecosystem-explorer/src/lib/api/fetch-with-cache.test.ts b/ecosystem-explorer/src/lib/api/fetch-with-cache.test.ts index bde113749..f5fb772e4 100644 --- a/ecosystem-explorer/src/lib/api/fetch-with-cache.test.ts +++ b/ecosystem-explorer/src/lib/api/fetch-with-cache.test.ts @@ -15,10 +15,22 @@ */ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import "fake-indexeddb/auto"; -import { fetchWithCache } from "./fetch-with-cache"; +import { fetchWithCache, resolveDataPath } from "./fetch-with-cache"; import * as idbCache from "./idb-cache"; import { STORES } from "./idb-cache"; +describe("resolveDataPath", () => { + it("should resolve paths correctly with BASE_URL", () => { + // Assuming BASE_URL is set (mocked or from env) + const result = resolveDataPath("data/test", "v1", "file.json"); + expect(result).toMatch(/\/data\/test\/v1\/file.json$/); + }); + + it("should throw on path traversal", () => { + expect(() => resolveDataPath("data", "..", "secrets")).toThrow(/Invalid path segment/); + }); +}); + declare const global: typeof globalThis; describe("fetchWithCache", () => { @@ -43,7 +55,9 @@ describe("fetchWithCache", () => { json: async () => data, }); - const result = await fetchWithCache("key", "/url", idbCache.STORES.METADATA); + const result = await fetchWithCache("key", "/url", idbCache.STORES.METADATA, { + retryDelayMs: 0, + }); expect(result).toEqual(data); expect(getCachedSpy).toHaveBeenCalledWith("key", idbCache.STORES.METADATA); @@ -56,7 +70,9 @@ describe("fetchWithCache", () => { vi.spyOn(idbCache, "getCached").mockResolvedValue(data); vi.spyOn(idbCache, "setCached"); - const result = await fetchWithCache("key", "/url", idbCache.STORES.METADATA); + const result = await fetchWithCache("key", "/url", idbCache.STORES.METADATA, { + retryDelayMs: 0, + }); expect(result).toEqual(data); expect(global.fetch).not.toHaveBeenCalled(); @@ -112,7 +128,9 @@ describe("fetchWithCache", () => { json: async () => data, }); - const result = await fetchWithCache("key", "/url", idbCache.STORES.METADATA); + const result = await fetchWithCache("key", "/url", idbCache.STORES.METADATA, { + retryDelayMs: 0, + }); expect(result).toEqual(data); expect(global.fetch).toHaveBeenCalledWith("/url"); @@ -126,6 +144,7 @@ describe("fetchWithCache", () => { ) as unknown as typeof fetch; const result = await fetchWithCache("test-404-soft", "/missing.json", STORES.CONFIGURATION, { allow404: true, + retryDelayMs: 0, }); expect(result).toBeNull(); }); @@ -137,21 +156,10 @@ describe("fetchWithCache", () => { new Response("not found", { status: 404, statusText: "Not Found" }) ) as unknown as typeof fetch; await expect( - fetchWithCache("test-404-hard", "/missing.json", STORES.CONFIGURATION) + fetchWithCache("test-404-hard", "/missing.json", STORES.CONFIGURATION, { retryDelayMs: 0 }) ).rejects.toThrow(/404/); }); - it("throws on 500 even when allow404 is true", async () => { - globalThis.fetch = vi - .fn() - .mockResolvedValue( - new Response("boom", { status: 500, statusText: "Internal Server Error" }) - ) as unknown as typeof fetch; - await expect( - fetchWithCache("test-500-soft", "/broken.json", STORES.CONFIGURATION, { allow404: true }) - ).rejects.toThrow(/500/); - }); - describe("validate option", () => { it("returns cached data when validate passes", async () => { const data = { versions: [{ version: "1.0.0", is_latest: true }] }; @@ -182,28 +190,126 @@ describe("fetchWithCache", () => { validate: (d: unknown) => Array.isArray((d as typeof staleData).versions) && (d as typeof staleData).versions.length > 0, + retryDelayMs: 0, }); expect(result).toEqual(freshData); expect(global.fetch).toHaveBeenCalledWith("/url"); }); + }); - it("fetches from network when no cached data exists and validate is provided", async () => { - const freshData = { versions: [{ version: "2.0.0", is_latest: true }] }; - vi.spyOn(idbCache, "getCached").mockResolvedValue(null); - vi.spyOn(idbCache, "setCached").mockResolvedValue(); + describe("stale cache fallback", () => { + it("returns stale data on network error", async () => { + const staleData = { old: "data" }; - (global.fetch as ReturnType).mockResolvedValue({ - ok: true, - json: async () => freshData, + const getCachedSpy = vi + .spyOn(idbCache, "getCached") + .mockImplementation(async (_key, _store, options) => { + if (options?.allowExpired) return staleData; + return null; + }); + + (global.fetch as ReturnType).mockRejectedValue( + new TypeError("Failed to fetch") + ); + + const result = await fetchWithCache("key", "/url", idbCache.STORES.METADATA, { + retryDelayMs: 0, }); - const result = await fetchWithCache("key", "/url", STORES.METADATA, { - validate: (d: unknown) => Array.isArray((d as typeof freshData).versions), + expect(result).toEqual(staleData); + expect(global.fetch).toHaveBeenCalled(); + expect(getCachedSpy).toHaveBeenCalledWith("key", idbCache.STORES.METADATA, { + allowExpired: true, }); + }); - expect(result).toEqual(freshData); - expect(global.fetch).toHaveBeenCalledWith("/url"); + it("returns stale data on 500 error", async () => { + const staleData = { old: "data" }; + + const getCachedSpy = vi + .spyOn(idbCache, "getCached") + .mockImplementation(async (_key, _store, options) => { + if (options?.allowExpired) return staleData; + return null; + }); + + (global.fetch as ReturnType).mockResolvedValue( + new Response("boom", { status: 500, statusText: "Internal Server Error" }) + ); + + const result = await fetchWithCache("key", "/url", idbCache.STORES.METADATA, { + retryDelayMs: 0, + }); + + expect(result).toEqual(staleData); + expect(global.fetch).toHaveBeenCalled(); + expect(getCachedSpy).toHaveBeenCalledWith("key", idbCache.STORES.METADATA, { + allowExpired: true, + }); + }); + + it("does NOT return stale data if validate fails", async () => { + const staleData = { versions: [] }; // logically invalid for this test + + vi.spyOn(idbCache, "getCached").mockImplementation(async (_key, _store, options) => { + if (options?.allowExpired) return staleData; + return null; + }); + + (global.fetch as ReturnType).mockRejectedValue(new TypeError("Network error")); + + await expect( + fetchWithCache("key", "/url", idbCache.STORES.METADATA, { + retryDelayMs: 0, + validate: (d: unknown) => (d as { versions: unknown[] }).versions.length > 0, + }) + ).rejects.toThrow("Network error"); + }); + }); + + describe("fetchWithRetry", () => { + it("retries on transient failure and eventually succeeds", async () => { + const data = { new: "data" }; + (global.fetch as ReturnType) + .mockRejectedValueOnce(new TypeError("Network error")) + .mockResolvedValueOnce(new Response("error", { status: 500 })) + .mockResolvedValueOnce(new Response(JSON.stringify(data), { status: 200 })); + + const result = await fetchWithCache("retry-key", "/retry-url", idbCache.STORES.METADATA, { + retryDelayMs: 0, + }); + + expect(result).toEqual(data); + expect(global.fetch).toHaveBeenCalledTimes(3); + }); + + it("succeeds on first try if retryCount is 0", async () => { + const data = { success: true }; + (global.fetch as ReturnType).mockResolvedValue( + new Response(JSON.stringify(data), { status: 200 }) + ); + + const result = await fetchWithCache("zero-retry-key", "/url", idbCache.STORES.METADATA, { + retryCount: 0, + retryDelayMs: 0, + }); + + expect(result).toEqual(data); + expect(global.fetch).toHaveBeenCalledTimes(1); + }); + + it("fails immediately if retryCount is 0 and fetch fails", async () => { + (global.fetch as ReturnType).mockRejectedValue(new TypeError("Fail")); + + await expect( + fetchWithCache("zero-retry-fail", "/url", idbCache.STORES.METADATA, { + retryCount: 0, + retryDelayMs: 0, + }) + ).rejects.toThrow("Fail"); + + expect(global.fetch).toHaveBeenCalledTimes(1); }); }); }); diff --git a/ecosystem-explorer/src/lib/api/fetch-with-cache.ts b/ecosystem-explorer/src/lib/api/fetch-with-cache.ts index efcec54ec..aef54030f 100644 --- a/ecosystem-explorer/src/lib/api/fetch-with-cache.ts +++ b/ecosystem-explorer/src/lib/api/fetch-with-cache.ts @@ -17,8 +17,66 @@ import { getCached, setCached, isIDBAvailable, type StoreName } from "./idb-cach const inflightRequests = new Map>(); +/** + * Validates a path segment to prevent path traversal attacks. + * Rejects segments containing '..' or characters outside the safe set (alphanumeric, dots, underscores, and hyphens). + */ +// Security: Prevent path traversal by strictly validating all dynamic path segments. +// CodeQL: This is an extra layer of protection to ensure all externally-provided +// strings are strictly alphanumeric before being used in a fetch() URL. +export function validatePathSegment(segment: string): void { + if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(segment) || segment.includes("..")) { + throw new Error(`Invalid path segment: ${segment}`); + } +} + +/** + * Resolves a data path by combining the base URL with segments. + * Centralizes security validation and path construction. + */ +export function resolveDataPath(base: string, ...segments: string[]): string { + const baseUrl = import.meta.env.BASE_URL || ""; + const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl; + + const parts = [normalizedBase, base.startsWith("/") ? base.slice(1) : base]; + + for (const segment of segments) { + validatePathSegment(segment); + parts.push(segment); + } + + return parts.join("/"); +} + +const DEFAULT_MAX_RETRIES = 3; +const DEFAULT_RETRY_DELAY_MS = 1000; + +async function fetchWithRetry( + url: string, + retries = DEFAULT_MAX_RETRIES, + delayMs = DEFAULT_RETRY_DELAY_MS, + allow404 = false +): Promise { + const maxAttempts = Math.max(1, retries); + let lastResponse: Response | undefined; + for (let i = 0; i < maxAttempts; i++) { + try { + lastResponse = await fetch(url); + if (lastResponse.ok || (lastResponse.status === 404 && allow404) || i === maxAttempts - 1) { + return lastResponse; + } + } catch (error) { + if (i === maxAttempts - 1) throw error; + } + await new Promise((resolve) => setTimeout(resolve, delayMs * Math.pow(2, i))); + } + return lastResponse!; +} + export interface FetchWithCacheOptions { allow404?: boolean; + retryCount?: number; + retryDelayMs?: number; /** * Optional validator for cached data. When provided, cached data that fails * validation is ignored for the current request and a fresh network request @@ -56,11 +114,49 @@ export async function fetchWithCache( } } - const response = await fetch(url); + let response: Response; + try { + response = await fetchWithRetry( + url, + options?.retryCount, + options?.retryDelayMs, + options?.allow404 + ); + } catch (error) { + if (isIDBAvailable()) { + const staleData = await getCached(cacheKey, storeType, { allowExpired: true }); + if (staleData !== null) { + if (options?.validate && !options.validate(staleData)) { + throw error; + } + console.warn("Failed to fetch, falling back to stale cache:", cacheKey, error); + return staleData; + } + } + throw error; + } + if (!response.ok) { if (response.status === 404 && options?.allow404) { return null; } + + if (isIDBAvailable()) { + const staleData = await getCached(cacheKey, storeType, { allowExpired: true }); + if (staleData !== null) { + if (options?.validate && !options.validate(staleData)) { + throw new Error( + `Failed to load ${cacheKey}: ${response.status} ${response.statusText}` + ); + } + console.warn("Failed to load, falling back to stale cache:", { + cacheKey, + status: response.status, + }); + return staleData; + } + } + throw new Error(`Failed to load ${cacheKey}: ${response.status} ${response.statusText}`); } diff --git a/ecosystem-explorer/src/lib/api/idb-cache.test.ts b/ecosystem-explorer/src/lib/api/idb-cache.test.ts index 9d9f169a3..dc52e9aec 100644 --- a/ecosystem-explorer/src/lib/api/idb-cache.test.ts +++ b/ecosystem-explorer/src/lib/api/idb-cache.test.ts @@ -15,7 +15,15 @@ */ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import "fake-indexeddb/auto"; -import { initDB, getCached, setCached, clearAllCached, closeDB, STORES } from "./idb-cache"; +import { + initDB, + getCached, + setCached, + clearAllCached, + closeDB, + pruneOldEntries, + STORES, +} from "./idb-cache"; describe("idb-cache", () => { beforeEach(async () => { @@ -100,7 +108,7 @@ describe("idb-cache", () => { expect(result).toEqual(data); }); - it("should return null and delete entries older than 24 hours", async () => { + it("should return null but keep entries older than 24 hours in the store", async () => { const key = "stale-key"; const data = { value: "stale" }; const now = Date.now(); @@ -110,10 +118,31 @@ describe("idb-cache", () => { const result = await getCached(key, STORES.METADATA); expect(result).toBeNull(); - // Confirm it was deleted from the store + // Confirm it was NOT deleted from the store (to support stale fallback) const db = await initDB(); const raw = await db.get(STORES.METADATA, key); - expect(raw).toBeUndefined(); + expect(raw).toBeDefined(); + expect(raw.data).toEqual(data); + }); + + it("should update lastAccessedAt even for expired data when allowed", async () => { + const key = "stale-key"; + const data = { value: "stale" }; + const startTime = Date.now(); + + await setCached(key, data, STORES.METADATA); + + // Move time forward 25 hours + vi.setSystemTime(startTime + 25 * 60 * 60 * 1000); + const accessTime = Date.now(); + + const result = await getCached(key, STORES.METADATA, { allowExpired: true }); + expect(result).toEqual(data); + + // Verify lastAccessedAt was updated + const db = await initDB(); + const raw = await db.get(STORES.METADATA, key); + expect(raw.lastAccessedAt).toBe(accessTime); }); it("should return null for non-existent keys", async () => { @@ -124,6 +153,7 @@ describe("idb-cache", () => { it("should overwrite existing data when key is reused", async () => { const key = "overwrite-test"; + const oldData = { value: "old" }; const newData = { value: "new" }; @@ -170,4 +200,26 @@ describe("idb-cache", () => { expect(result).toEqual({ data: "value" }); }); }); + + describe("pruneOldEntries", () => { + it("should remove entries older than the threshold", async () => { + const now = Date.now(); + await setCached("new-key", { data: "new" }, STORES.METADATA); + + // Manually inject a stale entry by setting the time back + vi.setSystemTime(now - 10 * 24 * 60 * 60 * 1000); // 10 days ago + await setCached("stale-key", { data: "stale" }, STORES.METADATA); + + vi.setSystemTime(now); + await pruneOldEntries(7); // Prune older than 7 days + + const newResult = await getCached("new-key", STORES.METADATA); + expect(newResult).not.toBeNull(); + + // Verify deletion directly from the database + const db = await initDB(); + const staleRaw = await db.get(STORES.METADATA, "stale-key"); + expect(staleRaw).toBeUndefined(); + }); + }); }); diff --git a/ecosystem-explorer/src/lib/api/idb-cache.ts b/ecosystem-explorer/src/lib/api/idb-cache.ts index 0a5d8b4f8..d68197608 100644 --- a/ecosystem-explorer/src/lib/api/idb-cache.ts +++ b/ecosystem-explorer/src/lib/api/idb-cache.ts @@ -32,6 +32,7 @@ interface CacheEntry { key: string; data: T; cachedAt: number; + lastAccessedAt?: number; } let dbInstance: IDBPDatabase | null = null; @@ -91,7 +92,11 @@ export async function initDB(): Promise { return dbInitPromise; } -export async function getCached(key: string, store: StoreName): Promise { +export async function getCached( + key: string, + store: StoreName, + options?: { allowExpired?: boolean } +): Promise { try { const db = await initDB(); const entry = await db.get(store, key); @@ -102,13 +107,22 @@ export async function getCached(key: string, store: StoreName): Promise; if (isExpired(cacheEntry.cachedAt)) { - await db.delete(store, key); + if (options?.allowExpired) { + // Update last accessed time even for expired data if we are serving it + cacheEntry.lastAccessedAt = Date.now(); + db.put(store, cacheEntry).catch(() => {}); + return cacheEntry.data; + } return null; } + // Update last accessed time in background + cacheEntry.lastAccessedAt = Date.now(); + db.put(store, cacheEntry).catch(() => {}); + return cacheEntry.data; } catch (error) { - console.error(`Failed to get cached data for %s:`, key, error); + console.error("Failed to get cached data for %s:", key, error); return null; } } @@ -121,11 +135,12 @@ export async function setCached(key: string, data: T, store: StoreName): Prom key, data, cachedAt: Date.now(), + lastAccessedAt: Date.now(), }; await db.put(store, entry); } catch (error) { - console.error(`Failed to cache data for %s:`, key, error); + console.error("Failed to cache data for %s:", key, error); } } @@ -139,6 +154,36 @@ export async function clearAllCached(): Promise { } } +/** + * Prunes entries that haven't been accessed for the specified number of days. + * Helps prevent IndexedDB bloat from orphaned versioned data. + */ +export async function pruneOldEntries(maxAgeDays = 7): Promise { + try { + const db = await initDB(); + const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000; + const now = Date.now(); + + for (const store of Object.values(STORES)) { + const tx = db.transaction(store, "readwrite"); + let cursor = await tx.store.openCursor(); + + while (cursor) { + const entry = cursor.value as CacheEntry; + const lastAccessed = entry.lastAccessedAt || entry.cachedAt; + + if (now - lastAccessed > maxAgeMs) { + await cursor.delete(); + } + cursor = await cursor.continue(); + } + await tx.done; + } + } catch (error) { + console.error("Failed to prune old cache entries:", error); + } +} + export function closeDB(): void { if (dbInstance) { dbInstance.close(); From 462a137da55724a04584013d8758f0642c3a0f01 Mon Sep 17 00:00:00 2001 From: Pittu Sharma Date: Fri, 15 May 2026 00:59:17 +0530 Subject: [PATCH 9/9] chore: remove unused files, revert glow badge changes, and fix resulting typescript errors --- docs/dotnet-integration-roadmap.md | 31 ------------------- .../src/components/ui/glow-badge.tsx | 26 ++-------------- .../src/components/ui/status-pill.test.tsx | 25 +++++++++------ .../src/components/ui/status-pill.tsx | 6 ++-- .../components/instrumentation-card.tsx | 2 +- .../dotnet/v1.0.0/instrumentation.yaml | 9 ------ 6 files changed, 22 insertions(+), 77 deletions(-) delete mode 100644 docs/dotnet-integration-roadmap.md delete mode 100644 ecosystem-registry/dotnet/v1.0.0/instrumentation.yaml diff --git a/docs/dotnet-integration-roadmap.md b/docs/dotnet-integration-roadmap.md deleted file mode 100644 index 8e66361a5..000000000 --- a/docs/dotnet-integration-roadmap.md +++ /dev/null @@ -1,31 +0,0 @@ -# .NET Integration Roadmap - -This document outlines the proposed strategy and roadmap for fully integrating the `.NET` ecosystem -into the OpenTelemetry Ecosystem Explorer. - -## Phase 1: Metadata Collection (Completed) - -- [x] Create a `dotnet-instrumentation-watcher` component within `ecosystem-automation`. -- [x] Design a programmatic scraper to dynamically fetch `.NET` repositories - (`open-telemetry/opentelemetry-dotnet-contrib`) and parse `.csproj` structures since - centralized catalog APIs do not exist. -- [x] Synchronize this data into `ecosystem-registry/dotnet/` with versioned snapshot capability. -- [x] Ensure structural data uses proper classifications (e.g., `instrumentation`, `exporter`, - `extension`). - -## Phase 2: Database Integration (Upcoming) - -- [ ] Extend `explorer-db-builder` to support `.NET` specific processing logic alongside Java Agent - logic. -- [ ] Create a `dotnet` specific database writer inside - `explorer-db-builder/src/explorer_db_builder/database_writer.py`. -- [ ] Transform `modules` lists generated by the watcher into Explorer-compatible schema elements - for the SQLite frontend ingest payload. - -## Phase 3: Frontend Implementation (Upcoming) - -- [ ] Add a new navigation card on the `ecosystem-explorer` Home Page mapping to `/dotnet`. -- [ ] Create `.NET` specific exploration React components in `ecosystem-explorer/src/pages/dotnet`. -- [ ] Implement UI data hooks in `use-instrumentations.ts` to fetch and render `.NET` libraries. -- [ ] Audit accessibility (`aria-labels`, focus indicators) for any new `.NET` specific UI - components introduced. diff --git a/ecosystem-explorer/src/components/ui/glow-badge.tsx b/ecosystem-explorer/src/components/ui/glow-badge.tsx index f965ed6f6..596656c75 100644 --- a/ecosystem-explorer/src/components/ui/glow-badge.tsx +++ b/ecosystem-explorer/src/components/ui/glow-badge.tsx @@ -15,15 +15,7 @@ */ import React from "react"; -type BadgeVariant = - | "primary" - | "accent" - | "secondary" - | "success" - | "info" - | "warning" - | "error" - | "muted"; +type BadgeVariant = "primary" | "success" | "info" | "warning" | "muted"; interface GlowBadgeProps { children: React.ReactNode; @@ -37,14 +29,6 @@ const variantStyles: Record = { base: "bg-primary/10 border-primary/30 text-primary", glow: "shadow-sm shadow-primary/20", }, - accent: { - base: "bg-secondary/10 border-secondary/30 text-secondary", - glow: "shadow-sm shadow-secondary/20", - }, - secondary: { - base: "bg-slate-500/10 border-slate-500/30 text-slate-600 dark:text-slate-400", - glow: "shadow-sm shadow-slate-500/20", - }, success: { base: "bg-green-500/10 border-green-500/30 text-green-600 dark:text-green-400", glow: "shadow-sm shadow-green-500/20", @@ -57,10 +41,6 @@ const variantStyles: Record = { base: "bg-orange-500/10 border-orange-500/30 text-orange-600 dark:text-orange-400", glow: "shadow-sm shadow-orange-500/20", }, - error: { - base: "bg-red-500/10 border-red-500/30 text-red-600 dark:text-red-400", - glow: "shadow-sm shadow-red-500/20", - }, muted: { base: "bg-muted border-border/50 text-muted-foreground", glow: "", @@ -69,11 +49,11 @@ const variantStyles: Record = { export function GlowBadge({ children, - variant = "accent", + variant = "primary", withGlow = false, className = "", }: GlowBadgeProps) { - const styles = variantStyles[variant] || variantStyles["accent"]; + const styles = variantStyles[variant]; const glowClass = withGlow ? styles.glow : ""; return ( diff --git a/ecosystem-explorer/src/components/ui/status-pill.test.tsx b/ecosystem-explorer/src/components/ui/status-pill.test.tsx index 1e058fded..921897d33 100644 --- a/ecosystem-explorer/src/components/ui/status-pill.test.tsx +++ b/ecosystem-explorer/src/components/ui/status-pill.test.tsx @@ -31,18 +31,23 @@ const VARIANTS: ReadonlyArray<{ { stability: "development", label: "Development", - base: "text-slate-600", - dark: "dark:text-slate-400", + base: "text-muted-foreground", + dark: "", }, { stability: "alpha", label: "Alpha", base: "text-orange-600", dark: "dark:text-orange-400" }, { stability: "beta", label: "Beta", base: "text-blue-600", dark: "dark:text-blue-400" }, { stability: "stable", label: "Stable", base: "text-green-600", dark: "dark:text-green-400" }, - { stability: "deprecated", label: "Deprecated", base: "text-red-600", dark: "dark:text-red-400" }, + { + stability: "deprecated", + label: "Deprecated", + base: "text-orange-600", + dark: "dark:text-orange-400", + }, { stability: "unmaintained", label: "Unmaintained", - base: "text-red-600", - dark: "dark:text-red-400", + base: "text-orange-600", + dark: "dark:text-orange-400", }, ]; @@ -70,7 +75,7 @@ describe("StatusPill", () => { } }); - it("renders deprecated and unmaintained with distinct labels but the same red classes", () => { + it("renders deprecated and unmaintained with distinct labels but the same orange classes", () => { render( <> @@ -80,10 +85,10 @@ describe("StatusPill", () => { const deprecated = screen.getByText("Deprecated"); const unmaintained = screen.getByText("Unmaintained"); expect(deprecated).not.toBe(unmaintained); - expect(deprecated.className).toContain("text-red-600"); - expect(deprecated.className).toContain("dark:text-red-400"); - expect(unmaintained.className).toContain("text-red-600"); - expect(unmaintained.className).toContain("dark:text-red-400"); + expect(deprecated.className).toContain("text-orange-600"); + expect(deprecated.className).toContain("dark:text-orange-400"); + expect(unmaintained.className).toContain("text-orange-600"); + expect(unmaintained.className).toContain("dark:text-orange-400"); }); it("forwards className to the rendered element", () => { diff --git a/ecosystem-explorer/src/components/ui/status-pill.tsx b/ecosystem-explorer/src/components/ui/status-pill.tsx index 5465fde34..068d04273 100644 --- a/ecosystem-explorer/src/components/ui/status-pill.tsx +++ b/ecosystem-explorer/src/components/ui/status-pill.tsx @@ -16,12 +16,12 @@ import { GlowBadge } from "./glow-badge"; const STABILITY = { - development: { variant: "secondary", label: "Development" }, + development: { variant: "muted", label: "Development" }, alpha: { variant: "warning", label: "Alpha" }, beta: { variant: "info", label: "Beta" }, stable: { variant: "success", label: "Stable" }, - deprecated: { variant: "error", label: "Deprecated" }, - unmaintained: { variant: "error", label: "Unmaintained" }, + deprecated: { variant: "warning", label: "Deprecated" }, + unmaintained: { variant: "warning", label: "Unmaintained" }, } as const; export type Stability = keyof typeof STABILITY; diff --git a/ecosystem-explorer/src/features/java-agent/components/instrumentation-card.tsx b/ecosystem-explorer/src/features/java-agent/components/instrumentation-card.tsx index 08e624cce..b5d8cd15f 100644 --- a/ecosystem-explorer/src/features/java-agent/components/instrumentation-card.tsx +++ b/ecosystem-explorer/src/features/java-agent/components/instrumentation-card.tsx @@ -88,7 +88,7 @@ export function InstrumentationCard({ return ( {info?.label ?? s} diff --git a/ecosystem-registry/dotnet/v1.0.0/instrumentation.yaml b/ecosystem-registry/dotnet/v1.0.0/instrumentation.yaml deleted file mode 100644 index 3dafe5887..000000000 --- a/ecosystem-registry/dotnet/v1.0.0/instrumentation.yaml +++ /dev/null @@ -1,9 +0,0 @@ -modules: -- name: OpenTelemetry.Instrumentation.AspNetCore - description: ASP.NET Core instrumentation for OpenTelemetry - type: instrumentation - version: 1.0.0 -- name: OpenTelemetry.Instrumentation.Http - description: HttpClient instrumentation for OpenTelemetry - type: instrumentation - version: 1.0.0