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.
- phlo_clickstack-0.1.0/PKG-INFO +15 -0
- phlo_clickstack-0.1.0/README.md +37 -0
- phlo_clickstack-0.1.0/pyproject.toml +44 -0
- phlo_clickstack-0.1.0/setup.cfg +4 -0
- phlo_clickstack-0.1.0/src/phlo_clickstack/__init__.py +1 -0
- phlo_clickstack-0.1.0/src/phlo_clickstack/cli.py +113 -0
- phlo_clickstack-0.1.0/src/phlo_clickstack/cli_plugin.py +26 -0
- phlo_clickstack-0.1.0/src/phlo_clickstack/plugin.py +31 -0
- phlo_clickstack-0.1.0/src/phlo_clickstack/service.yaml +60 -0
- phlo_clickstack-0.1.0/src/phlo_clickstack.egg-info/PKG-INFO +15 -0
- phlo_clickstack-0.1.0/src/phlo_clickstack.egg-info/SOURCES.txt +15 -0
- phlo_clickstack-0.1.0/src/phlo_clickstack.egg-info/dependency_links.txt +1 -0
- phlo_clickstack-0.1.0/src/phlo_clickstack.egg-info/entry_points.txt +5 -0
- phlo_clickstack-0.1.0/src/phlo_clickstack.egg-info/requires.txt +6 -0
- phlo_clickstack-0.1.0/src/phlo_clickstack.egg-info/top_level.txt +1 -0
- phlo_clickstack-0.1.0/tests/test_clickstack_cli.py +120 -0
- phlo_clickstack-0.1.0/tests/test_clickstack_plugin.py +21 -0
|
@@ -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 @@
|
|
|
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 @@
|
|
|
1
|
+
|
|
@@ -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
|