Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ jobs:
run: |
cd ecosystem-automation/configuration-watcher
uv run pytest tests/ --cov=configuration_watcher --cov-report=term-missing --cov-report=json
- name: Run js-instrumentation-watcher tests
run: |
cd ecosystem-automation/js-instrumentation-watcher
uv run pytest tests/ --cov=js_instrumentation_watcher --cov-report=term-missing --cov-report=json
test-automation-explorer-db-builder:
name: Test explorer-db-builder
Expand Down
42 changes: 42 additions & 0 deletions ecosystem-automation/js-instrumentation-watcher/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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.

[project]
name = "js-instrumentation-watcher"
version = "0.1.0"
description = "Automation tool for watching and collecting OpenTelemetry JavaScript instrumentation metadata"
requires-python = ">=3.11"
dependencies = [
"PyYAML>=6.0.1",
"watcher-common",
]

[project.scripts]
js-instrumentation-watcher = "js_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/js_instrumentation_watcher"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 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.
#
"""OpenTelemetry JavaScript instrumentation metadata automation."""

import importlib.metadata

try:
__version__ = importlib.metadata.version("js-instrumentation-watcher")
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0-dev"
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 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.
#
"""Allow running js_instrumentation_watcher as a module with python -m js_instrumentation_watcher."""

from .main import main

if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# 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 JS instrumentation metadata."""

import logging
from pathlib import Path
from typing import Any

from .inventory_manager import InventoryManager
from .package_parser import PackageParser
from .package_scanner import PackageScanner

logger = logging.getLogger(__name__)


class InstrumentationSync:
"""
Orchestrates synchronization of JS instrumentation metadata.

Walks the js-contrib repository, parses each instrumentation package,
and writes per-package versioned YAML to the registry.

Unlike the Java watcher which has a single release version, JS packages
version independently. Each package is stored at its own version:
ecosystem-registry/javascript/{package}/v{version}.yaml
"""

def __init__(
self,
repo_path: Path,
inventory_manager: InventoryManager,
):
"""
Args:
repo_path: Path to the cloned opentelemetry-js-contrib repository
inventory_manager: Inventory manager for writing registry files
"""
self.repo_path = repo_path
self.inventory_manager = inventory_manager
self.scanner = PackageScanner(repo_path)

def sync(self) -> dict[str, Any]:
"""
Synchronize all JS instrumentation packages to the registry.

For each package:
- If the current version already exists in the registry, skip it
- Otherwise parse and write the metadata

Returns:
Summary dict with counts of new, skipped, and failed packages
"""
summary: dict[str, Any] = {
"new": [],
"skipped": [],
"failed": [],
}

bundle_membership = self.scanner.load_bundle_membership()
logger.info("Loaded %d packages from auto-instrumentations-node", len(bundle_membership))

component_owners = self.scanner.load_component_owners()
logger.info("Loaded owners for %d components", len(component_owners))

packages = self.scanner.discover_packages()

for package_path in packages:
name = package_path.name
parser = PackageParser(
package_path=package_path,
bundle_membership=bundle_membership,
component_owners=component_owners,
)

try:
data = parser.parse()
except Exception:
logger.exception("Failed to parse %s", name)
summary["failed"].append(name)
continue

if data is None:
logger.warning("No data parsed for %s — skipping", name)
summary["failed"].append(name)
continue

version = data.get("version", "")
if not version:
logger.warning("No version found for %s — skipping", name)
summary["failed"].append(name)
continue

if self.inventory_manager.version_exists(name, version):
logger.debug("Already tracked: %s v%s", name, version)
summary["skipped"].append(f"{name}@{version}")
continue

self.inventory_manager.save(name, version, data)
logger.info("Saved: %s v%s", name, version)
summary["new"].append(f"{name}@{version}")

logger.info(
"Sync complete — new: %d, skipped: %d, failed: %d",
len(summary["new"]),
len(summary["skipped"]),
len(summary["failed"]),
)

return summary
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# 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 manager for JS instrumentation registry storage."""

import logging
from pathlib import Path

import yaml

logger = logging.getLogger(__name__)


class InventoryManager:
"""
Manages storage of JS instrumentation metadata in the registry.

Registry layout:
ecosystem-registry/javascript/{package-name}/v{version}.yaml
"""

def __init__(self, registry_dir: str):
"""
Args:
registry_dir: Base registry directory, e.g. 'ecosystem-registry/javascript'
"""
self.registry_dir = Path(registry_dir)

def version_exists(self, package_name: str, version: str) -> bool:
"""
Check if a specific package version already exists in the registry.

Args:
package_name: Package directory name, e.g. 'instrumentation-express'
version: Version string, e.g. '0.66.0'

Returns:
True if the version file exists
"""
return self._version_path(package_name, version).exists()

def save(self, package_name: str, version: str, data: dict) -> None:
"""
Save a package version to the registry.

Args:
package_name: Package directory name
version: Version string
data: Metadata dict to serialize as YAML
"""
path = self._version_path(package_name, version)
path.parent.mkdir(parents=True, exist_ok=True)

with path.open("w") as f:
yaml.dump(
data,
f,
default_flow_style=False,
sort_keys=True,
allow_unicode=True,
)

logger.debug("Saved %s v%s to %s", package_name, version, path)

def _version_path(self, package_name: str, version: str) -> Path:
"""
Build the path for a package version file.

Args:
package_name: Package directory name
version: Version string

Returns:
Path to the version YAML file
"""
return self.registry_dir / package_name / f"v{version}.yaml"
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# 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.
#

"""Entry point for the JS instrumentation watcher."""

import logging
import os

from .instrumentation_sync import InstrumentationSync
from .inventory_manager import InventoryManager
from .repository_manager import JsContribRepositoryManager

logger = logging.getLogger(__name__)

REGISTRY_DIR = "ecosystem-registry/javascript"


def configure_logging() -> None:
"""Configure root logging for the watcher."""
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)


def main() -> None:
"""Main entry point for the JS instrumentation watcher."""
configure_logging()

base_dir = os.environ.get("JS_CONTRIB_REPOS_DIR", "tmp_repos")

logger.info("Starting JS instrumentation watcher...")

repo_manager = JsContribRepositoryManager(base_dir=base_dir)
repo_path = repo_manager.setup()

inventory_manager = InventoryManager(registry_dir=REGISTRY_DIR)

sync = InstrumentationSync(
repo_path=repo_path,
inventory_manager=inventory_manager,
)

summary = sync.sync()

logger.info("Sync complete: %s", summary)
Loading
Loading