phlo-clickstack 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: phlo-clickstack
3
+ Version: 0.1.0
4
+ Summary: clickstack service plugin for Phlo
5
+ Author-email: Phlo Team <team@phlo.dev>
6
+ License: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/plain
9
+ Requires-Dist: phlo>=0.1.0
10
+ Requires-Dist: pyyaml>=6.0.1
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=7.0; extra == "dev"
13
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
14
+
15
+ clickstack service plugin for Phlo.
@@ -0,0 +1,37 @@
1
+ # phlo-clickstack
2
+
3
+ ClickStack observability service for Phlo.
4
+
5
+ ## Overview
6
+
7
+ `phlo-clickstack` packages the official ClickStack all-in-one image so Phlo can
8
+ ship a single OpenTelemetry-native observability target with logs, metrics, and
9
+ traces in one UI.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pip install phlo-clickstack
15
+ # or
16
+ phlo plugin install clickstack
17
+ ```
18
+
19
+ ## Profile
20
+
21
+ Part of the `observability` profile.
22
+
23
+ ## Usage
24
+
25
+ ```bash
26
+ phlo services start --service clickstack
27
+ phlo clickstack query "SELECT count() FROM default.otel_logs"
28
+ ```
29
+
30
+ Point `phlo-otel` at ClickStack:
31
+
32
+ ```bash
33
+ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
34
+ export OTEL_TRACES_EXPORTER=otlp
35
+ export OTEL_METRICS_EXPORTER=otlp
36
+ export OTEL_LOGS_EXPORTER=otlp
37
+ ```
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=45", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "phlo-clickstack"
7
+ version = "0.1.0"
8
+ description = "clickstack service plugin for Phlo"
9
+ readme = {text = "clickstack service plugin for Phlo.", content-type = "text/plain"}
10
+ requires-python = ">=3.11"
11
+ authors = [
12
+ {name = "Phlo Team", email = "team@phlo.dev"},
13
+ ]
14
+ license = {text = "MIT"}
15
+ dependencies = [
16
+ "phlo>=0.1.0",
17
+ "pyyaml>=6.0.1",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ dev = [
22
+ "pytest>=7.0",
23
+ "ruff>=0.1.0",
24
+ ]
25
+
26
+ [project.entry-points."phlo.plugins.services"]
27
+ clickstack = "phlo_clickstack.plugin:ClickStackServicePlugin"
28
+
29
+ [project.entry-points."phlo.plugins.cli"]
30
+ clickstack = "phlo_clickstack.cli_plugin:ClickStackCliPlugin"
31
+
32
+ [tool.setuptools]
33
+ package-dir = {"" = "src"}
34
+ include-package-data = true
35
+
36
+ [tool.setuptools.packages.find]
37
+ where = ["src"]
38
+
39
+ [tool.setuptools.package-data]
40
+ phlo_clickstack = ["service.yaml"]
41
+
42
+ [tool.ruff]
43
+ line-length = 100
44
+ target-version = "py311"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ """ClickStack service package for Phlo."""
@@ -0,0 +1,113 @@
1
+ """CLI commands for querying ClickStack's bundled ClickHouse service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from shutil import which
7
+ from subprocess import TimeoutExpired
8
+
9
+ import click
10
+
11
+ from phlo.cli.infrastructure.command import CommandError, run_command
12
+ from phlo.cli.infrastructure.compose import compose_base_cmd
13
+ from phlo.cli.infrastructure.utils import get_project_name
14
+ from phlo.logging import get_logger
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ def _read_query(*, query: str | None, file: Path | None) -> str:
20
+ """Return SQL text from inline query or file input."""
21
+ if query and file:
22
+ raise click.ClickException("Use either an inline query or --file, not both.")
23
+ if file is not None:
24
+ try:
25
+ sql = file.read_text(encoding="utf-8")
26
+ except OSError as exc:
27
+ raise click.ClickException(f"Failed to read SQL file: {file}") from exc
28
+ if sql.strip():
29
+ return sql
30
+ raise click.ClickException(f"SQL file is empty: {file}")
31
+ if query and query.strip():
32
+ return query
33
+ raise click.ClickException("Provide a SQL query argument or --file.")
34
+
35
+
36
+ def _ensure_phlo_dir() -> Path:
37
+ """Return the local .phlo directory or exit with a clear error."""
38
+ phlo_dir = Path.cwd() / ".phlo"
39
+ if phlo_dir.exists():
40
+ return phlo_dir
41
+ raise click.ClickException(".phlo directory not found. Run 'phlo services init' first.")
42
+
43
+
44
+ def _require_docker() -> None:
45
+ """Validate that Docker is installed and responsive."""
46
+ if which("docker") is None:
47
+ raise click.ClickException("docker command not found.")
48
+ try:
49
+ result = run_command(
50
+ ["docker", "info"],
51
+ timeout_seconds=10,
52
+ capture_output=True,
53
+ check=False,
54
+ )
55
+ except TimeoutExpired as exc:
56
+ raise click.ClickException("docker info timed out.") from exc
57
+ if result.returncode == 0:
58
+ return
59
+ raise click.ClickException("Docker is not running.")
60
+
61
+
62
+ @click.group(name="clickstack")
63
+ def clickstack_group() -> None:
64
+ """Query and inspect the ClickStack service."""
65
+
66
+
67
+ @clickstack_group.command(name="query")
68
+ @click.argument("query", required=False)
69
+ @click.option("--file", "query_file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
70
+ @click.option("--format", "output_format", default="TabSeparatedRaw", show_default=True)
71
+ @click.option("--timeout", "timeout_seconds", default=30, show_default=True, type=int)
72
+ def clickstack_query(
73
+ query: str | None,
74
+ query_file: Path | None,
75
+ output_format: str,
76
+ timeout_seconds: int,
77
+ ) -> None:
78
+ """Execute a ClickHouse query against the running ClickStack service."""
79
+ _require_docker()
80
+ phlo_dir = _ensure_phlo_dir()
81
+ project_name = get_project_name()
82
+ sql = _read_query(query=query, file=query_file)
83
+
84
+ cmd = compose_base_cmd(phlo_dir=phlo_dir, project_name=project_name)
85
+ cmd.extend(
86
+ [
87
+ "exec",
88
+ "-T",
89
+ "clickstack",
90
+ "clickhouse-client",
91
+ "--multiquery",
92
+ "--format",
93
+ output_format,
94
+ "--query",
95
+ sql,
96
+ ]
97
+ )
98
+
99
+ try:
100
+ result = run_command(
101
+ cmd,
102
+ timeout_seconds=timeout_seconds,
103
+ capture_output=True,
104
+ check=True,
105
+ )
106
+ except CommandError as exc:
107
+ stderr = exc.stderr.strip()
108
+ raise click.ClickException(stderr or str(exc)) from exc
109
+ except TimeoutExpired as exc:
110
+ raise click.ClickException(f"Query timed out after {timeout_seconds} seconds.") from exc
111
+
112
+ if result.stdout:
113
+ click.echo(result.stdout, nl=False)
@@ -0,0 +1,26 @@
1
+ """CLI plugin for ClickStack commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from phlo.plugins.base import CliCommandPlugin, PluginMetadata
8
+
9
+ from phlo_clickstack.cli import clickstack_group
10
+
11
+
12
+ class ClickStackCliPlugin(CliCommandPlugin):
13
+ """Register ClickStack CLI commands."""
14
+
15
+ @property
16
+ def metadata(self) -> PluginMetadata:
17
+ """Return plugin metadata for ClickStack CLI registration."""
18
+ return PluginMetadata(
19
+ name="clickstack",
20
+ version="0.1.0",
21
+ description="CLI commands for ClickStack query access",
22
+ )
23
+
24
+ def get_cli_commands(self) -> list[click.Command]:
25
+ """Return ClickStack CLI commands."""
26
+ return [clickstack_group]
@@ -0,0 +1,31 @@
1
+ """ClickStack service plugin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib import resources
6
+ from typing import Any
7
+
8
+ import yaml
9
+
10
+ from phlo.plugins import PluginMetadata, ServicePlugin
11
+
12
+
13
+ class ClickStackServicePlugin(ServicePlugin):
14
+ """Service plugin for ClickStack."""
15
+
16
+ @property
17
+ def metadata(self) -> PluginMetadata:
18
+ """Return plugin metadata for ClickStack service registration."""
19
+ return PluginMetadata(
20
+ name="clickstack",
21
+ version="0.1.0",
22
+ description="ClickStack all-in-one observability backend",
23
+ author="Phlo Team",
24
+ tags=["observability", "logs", "metrics", "traces"],
25
+ )
26
+
27
+ @property
28
+ def service_definition(self) -> dict[str, Any]:
29
+ """Load and return the ClickStack service definition."""
30
+ service_path = resources.files("phlo_clickstack").joinpath("service.yaml")
31
+ return yaml.safe_load(service_path.read_text(encoding="utf-8"))
@@ -0,0 +1,60 @@
1
+ name: clickstack
2
+ description: ClickStack all-in-one observability backend
3
+ category: observability
4
+ default: false
5
+ profile: observability
6
+
7
+ image: ${CLICKSTACK_IMAGE:-docker.hyperdx.io/hyperdx/hyperdx-all-in-one}
8
+
9
+ compose:
10
+ restart: unless-stopped
11
+ user: "0"
12
+ ports:
13
+ - "${CLICKSTACK_PORT:-8080}:8080"
14
+ - "${CLICKSTACK_OTLP_GRPC_PORT:-4317}:4317"
15
+ - "${CLICKSTACK_OTLP_HTTP_PORT:-4318}:4318"
16
+ - "${CLICKSTACK_NATIVE_PORT:-9000}:9000"
17
+ volumes:
18
+ - ./volumes/clickstack:/var/lib/clickhouse
19
+ healthcheck:
20
+ test:
21
+ [
22
+ "CMD",
23
+ "wget",
24
+ "--quiet",
25
+ "--tries=1",
26
+ "--spider",
27
+ "http://127.0.0.1:8080",
28
+ ]
29
+ interval: 10s
30
+ timeout: 5s
31
+ retries: 10
32
+
33
+ env_vars:
34
+ CLICKSTACK_IMAGE:
35
+ default: "docker.hyperdx.io/hyperdx/hyperdx-all-in-one"
36
+ description: Official ClickStack all-in-one image reference
37
+ CLICKSTACK_PORT:
38
+ default: 8080
39
+ description: ClickStack UI port
40
+ CLICKSTACK_OTLP_GRPC_PORT:
41
+ default: 4317
42
+ description: ClickStack OTLP gRPC ingest port
43
+ CLICKSTACK_OTLP_HTTP_PORT:
44
+ default: 4318
45
+ description: ClickStack OTLP HTTP ingest port
46
+ CLICKSTACK_NATIVE_PORT:
47
+ default: 9000
48
+ description: ClickHouse native port exposed by the all-in-one image
49
+ CLICKSTACK_PUBLIC_URL:
50
+ default: ""
51
+ description: Public ClickStack base URL used by observability links
52
+ CLICKSTACK_LOGS_PATH:
53
+ default: "/"
54
+ description: Path used to build ClickStack log links
55
+ CLICKSTACK_METRICS_PATH:
56
+ default: "/"
57
+ description: Path used to build ClickStack metric links
58
+ CLICKSTACK_DASHBOARDS_PATH:
59
+ default: "/"
60
+ description: Path used to build ClickStack dashboard links
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: phlo-clickstack
3
+ Version: 0.1.0
4
+ Summary: clickstack service plugin for Phlo
5
+ Author-email: Phlo Team <team@phlo.dev>
6
+ License: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/plain
9
+ Requires-Dist: phlo>=0.1.0
10
+ Requires-Dist: pyyaml>=6.0.1
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=7.0; extra == "dev"
13
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
14
+
15
+ clickstack service plugin for Phlo.
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/phlo_clickstack/__init__.py
4
+ src/phlo_clickstack/cli.py
5
+ src/phlo_clickstack/cli_plugin.py
6
+ src/phlo_clickstack/plugin.py
7
+ src/phlo_clickstack/service.yaml
8
+ src/phlo_clickstack.egg-info/PKG-INFO
9
+ src/phlo_clickstack.egg-info/SOURCES.txt
10
+ src/phlo_clickstack.egg-info/dependency_links.txt
11
+ src/phlo_clickstack.egg-info/entry_points.txt
12
+ src/phlo_clickstack.egg-info/requires.txt
13
+ src/phlo_clickstack.egg-info/top_level.txt
14
+ tests/test_clickstack_cli.py
15
+ tests/test_clickstack_plugin.py
@@ -0,0 +1,5 @@
1
+ [phlo.plugins.cli]
2
+ clickstack = phlo_clickstack.cli_plugin:ClickStackCliPlugin
3
+
4
+ [phlo.plugins.services]
5
+ clickstack = phlo_clickstack.plugin:ClickStackServicePlugin
@@ -0,0 +1,6 @@
1
+ phlo>=0.1.0
2
+ pyyaml>=6.0.1
3
+
4
+ [dev]
5
+ pytest>=7.0
6
+ ruff>=0.1.0
@@ -0,0 +1 @@
1
+ phlo_clickstack
@@ -0,0 +1,120 @@
1
+ """Tests for ClickStack CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from subprocess import CompletedProcess, TimeoutExpired
7
+
8
+ from click.testing import CliRunner
9
+
10
+ from phlo_clickstack.cli import clickstack_group
11
+ from phlo_clickstack.cli_plugin import ClickStackCliPlugin
12
+
13
+
14
+ def test_clickstack_cli_plugin_metadata() -> None:
15
+ """Validate ClickStack CLI plugin metadata."""
16
+ plugin = ClickStackCliPlugin()
17
+
18
+ assert plugin.metadata.name == "clickstack"
19
+ assert plugin.get_cli_commands()[0].name == "clickstack"
20
+
21
+
22
+ def test_clickstack_query_runs_clickhouse_client(monkeypatch) -> None:
23
+ """Query command should execute clickhouse-client in the ClickStack container."""
24
+
25
+ def _run_command(cmd, **_kwargs):
26
+ if cmd[:2] == ["docker", "info"]:
27
+ return CompletedProcess(cmd, 0, stdout="", stderr="")
28
+ assert cmd[-7:] == [
29
+ "clickstack",
30
+ "clickhouse-client",
31
+ "--multiquery",
32
+ "--format",
33
+ "JSONEachRow",
34
+ "--query",
35
+ "SELECT 1",
36
+ ]
37
+ return CompletedProcess(cmd, 0, stdout='{"1":1}\n', stderr="")
38
+
39
+ monkeypatch.setattr("phlo_clickstack.cli._ensure_phlo_dir", lambda: Path("/tmp/project/.phlo"))
40
+ monkeypatch.setattr("phlo_clickstack.cli.get_project_name", lambda: "demo")
41
+ monkeypatch.setattr("phlo_clickstack.cli.which", lambda _name: "/usr/bin/docker")
42
+ monkeypatch.setattr(
43
+ "phlo_clickstack.cli.compose_base_cmd",
44
+ lambda **_kwargs: ["docker", "compose", "-p", "demo"],
45
+ )
46
+ monkeypatch.setattr("phlo_clickstack.cli.run_command", _run_command)
47
+
48
+ result = CliRunner().invoke(clickstack_group, ["query", "--format", "JSONEachRow", "SELECT 1"])
49
+
50
+ assert result.exit_code == 0
51
+ assert result.output == '{"1":1}\n'
52
+
53
+
54
+ def test_clickstack_query_supports_file(monkeypatch, tmp_path) -> None:
55
+ """Query command should read SQL from a file."""
56
+ sql_file = tmp_path / "query.sql"
57
+ sql_file.write_text("SELECT 42", encoding="utf-8")
58
+
59
+ def _run_command(cmd, **_kwargs):
60
+ if cmd[:2] == ["docker", "info"]:
61
+ return CompletedProcess(cmd, 0, stdout="", stderr="")
62
+ assert cmd[-1] == "SELECT 42"
63
+ return CompletedProcess(cmd, 0, stdout="42\n", stderr="")
64
+
65
+ monkeypatch.setattr("phlo_clickstack.cli._ensure_phlo_dir", lambda: Path("/tmp/project/.phlo"))
66
+ monkeypatch.setattr("phlo_clickstack.cli.get_project_name", lambda: "demo")
67
+ monkeypatch.setattr("phlo_clickstack.cli.which", lambda _name: "/usr/bin/docker")
68
+ monkeypatch.setattr(
69
+ "phlo_clickstack.cli.compose_base_cmd",
70
+ lambda **_kwargs: ["docker", "compose", "-p", "demo"],
71
+ )
72
+ monkeypatch.setattr("phlo_clickstack.cli.run_command", _run_command)
73
+
74
+ result = CliRunner().invoke(clickstack_group, ["query", "--file", str(sql_file)])
75
+
76
+ assert result.exit_code == 0
77
+ assert result.output == "42\n"
78
+
79
+
80
+ def test_clickstack_query_rejects_missing_input(monkeypatch) -> None:
81
+ """Query command should fail clearly when no SQL is provided."""
82
+ monkeypatch.setattr("phlo_clickstack.cli._ensure_phlo_dir", lambda: Path("/tmp/project/.phlo"))
83
+ monkeypatch.setattr("phlo_clickstack.cli.get_project_name", lambda: "demo")
84
+ monkeypatch.setattr("phlo_clickstack.cli.which", lambda _name: "/usr/bin/docker")
85
+ monkeypatch.setattr(
86
+ "phlo_clickstack.cli.run_command",
87
+ lambda cmd, **_kwargs: (
88
+ CompletedProcess(cmd, 0, stdout="", stderr="")
89
+ if cmd[:2] == ["docker", "info"]
90
+ else CompletedProcess(cmd, 0, stdout="", stderr="")
91
+ ),
92
+ )
93
+
94
+ result = CliRunner().invoke(clickstack_group, ["query"])
95
+
96
+ assert result.exit_code != 0
97
+ assert "Provide a SQL query argument or --file." in result.output
98
+
99
+
100
+ def test_clickstack_query_surfaces_timeout(monkeypatch) -> None:
101
+ """Query command should surface timeouts as click exceptions."""
102
+
103
+ def _run_command(cmd, **_kwargs):
104
+ if cmd[:2] == ["docker", "info"]:
105
+ return CompletedProcess(cmd, 0, stdout="", stderr="")
106
+ raise TimeoutExpired(cmd=cmd, timeout=30)
107
+
108
+ monkeypatch.setattr("phlo_clickstack.cli._ensure_phlo_dir", lambda: Path("/tmp/project/.phlo"))
109
+ monkeypatch.setattr("phlo_clickstack.cli.get_project_name", lambda: "demo")
110
+ monkeypatch.setattr("phlo_clickstack.cli.which", lambda _name: "/usr/bin/docker")
111
+ monkeypatch.setattr(
112
+ "phlo_clickstack.cli.compose_base_cmd",
113
+ lambda **_kwargs: ["docker", "compose", "-p", "demo"],
114
+ )
115
+ monkeypatch.setattr("phlo_clickstack.cli.run_command", _run_command)
116
+
117
+ result = CliRunner().invoke(clickstack_group, ["query", "SELECT 1"])
118
+
119
+ assert result.exit_code != 0
120
+ assert "Query timed out after 30 seconds." in result.output
@@ -0,0 +1,21 @@
1
+ """Tests for ClickStack service plugin."""
2
+
3
+ from phlo_clickstack.plugin import ClickStackServicePlugin
4
+
5
+
6
+ def test_clickstack_service_definition() -> None:
7
+ """Validate ClickStack service definition defaults."""
8
+ plugin = ClickStackServicePlugin()
9
+ defn = plugin.service_definition
10
+
11
+ assert defn["name"] == "clickstack"
12
+ assert defn["profile"] == "observability"
13
+
14
+
15
+ def test_clickstack_plugin_metadata() -> None:
16
+ """Validate ClickStack plugin metadata."""
17
+ plugin = ClickStackServicePlugin()
18
+ meta = plugin.metadata
19
+
20
+ assert meta.name == "clickstack"
21
+ assert "observability" in meta.tags