phlo-clickstack 0.3.0__tar.gz → 0.4.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.3.0 → phlo_clickstack-0.4.0}/PKG-INFO +2 -2
- {phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/README.md +4 -10
- {phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/pyproject.toml +2 -2
- phlo_clickstack-0.4.0/src/phlo_clickstack/authorization.py +27 -0
- {phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/src/phlo_clickstack/cli.py +18 -9
- phlo_clickstack-0.4.0/src/phlo_clickstack/cli_plugin.py +17 -0
- phlo_clickstack-0.4.0/src/phlo_clickstack/plugin.py +15 -0
- {phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/src/phlo_clickstack/service.yaml +7 -15
- {phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/src/phlo_clickstack.egg-info/PKG-INFO +2 -2
- {phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/src/phlo_clickstack.egg-info/requires.txt +1 -1
- {phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/tests/test_clickstack_cli.py +33 -1
- phlo_clickstack-0.4.0/tests/test_clickstack_plugin.py +44 -0
- phlo_clickstack-0.3.0/src/phlo_clickstack/authorization.py +0 -166
- phlo_clickstack-0.3.0/src/phlo_clickstack/cli_plugin.py +0 -49
- phlo_clickstack-0.3.0/src/phlo_clickstack/plugin.py +0 -44
- phlo_clickstack-0.3.0/tests/test_clickstack_plugin.py +0 -21
- {phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/setup.cfg +0 -0
- {phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/src/phlo_clickstack/__init__.py +0 -0
- {phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/src/phlo_clickstack/observability_backend.py +0 -0
- {phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/src/phlo_clickstack/resource_provider.py +0 -0
- {phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/src/phlo_clickstack.egg-info/SOURCES.txt +0 -0
- {phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/src/phlo_clickstack.egg-info/dependency_links.txt +0 -0
- {phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/src/phlo_clickstack.egg-info/entry_points.txt +0 -0
- {phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/src/phlo_clickstack.egg-info/top_level.txt +0 -0
- {phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/tests/test_authorization.py +0 -0
- {phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/tests/test_observability_backend.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: phlo-clickstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: clickstack service plugin for Phlo
|
|
5
5
|
Author-email: Phlo Team <team@phlo.dev>
|
|
6
6
|
License: MIT
|
|
7
7
|
Requires-Python: >=3.11
|
|
8
8
|
Description-Content-Type: text/plain
|
|
9
9
|
Requires-Dist: phlo>=0.1.0
|
|
10
|
-
Requires-Dist:
|
|
10
|
+
Requires-Dist: requests>=2.32.5
|
|
11
11
|
Provides-Extra: dev
|
|
12
12
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
13
13
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
@@ -5,8 +5,7 @@ ClickStack observability service for Phlo.
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
7
|
`phlo-clickstack` packages the official ClickStack all-in-one image so Phlo can
|
|
8
|
-
|
|
9
|
-
traces in one UI.
|
|
8
|
+
query ClickHouse-backed observability data and link to the HyperDX UI.
|
|
10
9
|
|
|
11
10
|
## Installation
|
|
12
11
|
|
|
@@ -27,11 +26,6 @@ phlo services start --service clickstack
|
|
|
27
26
|
phlo clickstack query "SELECT count() FROM default.otel_logs"
|
|
28
27
|
```
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
```
|
|
29
|
+
The bundled all-in-one image exposes the HyperDX UI and ClickHouse query ports.
|
|
30
|
+
Use Alloy or another collector for OTLP ingest; this service does not publish
|
|
31
|
+
OTLP ports by default because the image does not listen on those ports.
|
|
@@ -8,12 +8,12 @@ requires = [
|
|
|
8
8
|
[project]
|
|
9
9
|
dependencies = [
|
|
10
10
|
"phlo>=0.1.0",
|
|
11
|
-
"
|
|
11
|
+
"requests>=2.32.5",
|
|
12
12
|
]
|
|
13
13
|
description = "clickstack service plugin for Phlo"
|
|
14
14
|
name = "phlo-clickstack"
|
|
15
15
|
requires-python = ">=3.11"
|
|
16
|
-
version = "0.
|
|
16
|
+
version = "0.4.0"
|
|
17
17
|
|
|
18
18
|
[[project.authors]]
|
|
19
19
|
email = "team@phlo.dev"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""phlo_clickstack CLI authorization table."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from phlo.cli.authorization import cli_surface_adapter_class
|
|
6
|
+
|
|
7
|
+
SURFACE_NAME = "phlo-clickstack"
|
|
8
|
+
FRAMEWORK_TYPE = "cli"
|
|
9
|
+
MUTATION_COMMANDS: frozenset[str] = frozenset(["clickstack.query"])
|
|
10
|
+
READ_COMMANDS: frozenset[str] = frozenset([])
|
|
11
|
+
COMMAND_RESOURCE_MAP: dict[str, str] = {"clickstack.query": "dataset"}
|
|
12
|
+
COMMAND_ACTION_MAP: dict[str, str] = {"clickstack.query": "dataset.write"}
|
|
13
|
+
|
|
14
|
+
ClickStackSurfaceAdapter = cli_surface_adapter_class(
|
|
15
|
+
"ClickStackSurfaceAdapter",
|
|
16
|
+
surface_name=SURFACE_NAME,
|
|
17
|
+
mutation_commands=MUTATION_COMMANDS,
|
|
18
|
+
read_commands=READ_COMMANDS,
|
|
19
|
+
command_resource_map=COMMAND_RESOURCE_MAP,
|
|
20
|
+
command_action_map=COMMAND_ACTION_MAP,
|
|
21
|
+
default_action_prefix="clickstack",
|
|
22
|
+
default_resource_prefix="clickstack",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_adapter() -> ClickStackSurfaceAdapter:
|
|
27
|
+
return ClickStackSurfaceAdapter.get_instance()
|
|
@@ -13,11 +13,21 @@ from subprocess import TimeoutExpired
|
|
|
13
13
|
|
|
14
14
|
import click
|
|
15
15
|
|
|
16
|
+
from phlo.cli.authorization_wrappers import enforce_surface_mutation_authorization
|
|
16
17
|
from phlo.cli.commands.services import utils as services_utils
|
|
18
|
+
from phlo.cli.commands.services.utils import ensure_compose_project
|
|
17
19
|
from phlo.cli.infrastructure.command import CommandError, run_command
|
|
18
20
|
from phlo.cli.infrastructure.compose import compose_base_cmd
|
|
19
21
|
from phlo.cli.infrastructure.utils import get_project_name
|
|
22
|
+
from phlo.cli.output import (
|
|
23
|
+
empty_file_error,
|
|
24
|
+
exclusive_options_error,
|
|
25
|
+
file_read_error,
|
|
26
|
+
missing_query_error,
|
|
27
|
+
)
|
|
28
|
+
from phlo.cli.sql import is_mutating_sql
|
|
20
29
|
from phlo.logging import get_logger
|
|
30
|
+
from phlo_clickstack.authorization import get_adapter as get_clickstack_adapter
|
|
21
31
|
|
|
22
32
|
logger = get_logger(__name__)
|
|
23
33
|
|
|
@@ -44,18 +54,18 @@ def _read_query(*, query: str | None, file: Path | None) -> str:
|
|
|
44
54
|
|
|
45
55
|
"""
|
|
46
56
|
if query and file:
|
|
47
|
-
raise
|
|
57
|
+
raise exclusive_options_error("an inline query", "--file")
|
|
48
58
|
if file is not None:
|
|
49
59
|
try:
|
|
50
60
|
sql = file.read_text(encoding="utf-8")
|
|
51
61
|
except OSError as exc:
|
|
52
|
-
raise
|
|
62
|
+
raise file_read_error(file) from exc
|
|
53
63
|
if sql.strip():
|
|
54
64
|
return sql
|
|
55
|
-
raise
|
|
65
|
+
raise empty_file_error(file)
|
|
56
66
|
if query and query.strip():
|
|
57
67
|
return query
|
|
58
|
-
raise
|
|
68
|
+
raise missing_query_error(command_hint='phlo clickstack query "SELECT 1"')
|
|
59
69
|
|
|
60
70
|
|
|
61
71
|
def _ensure_phlo_dir() -> Path:
|
|
@@ -71,10 +81,7 @@ def _ensure_phlo_dir() -> Path:
|
|
|
71
81
|
click.ClickException: If .phlo directory does not exist.
|
|
72
82
|
|
|
73
83
|
"""
|
|
74
|
-
|
|
75
|
-
if phlo_dir.exists():
|
|
76
|
-
return phlo_dir
|
|
77
|
-
raise click.ClickException(".phlo directory not found. Run 'phlo services init' first.")
|
|
84
|
+
return ensure_compose_project()
|
|
78
85
|
|
|
79
86
|
|
|
80
87
|
def _require_container_backend() -> None:
|
|
@@ -123,10 +130,12 @@ def clickstack_query(
|
|
|
123
130
|
click.ClickException: If query times out.
|
|
124
131
|
|
|
125
132
|
"""
|
|
133
|
+
sql = _read_query(query=query, file=query_file)
|
|
134
|
+
if is_mutating_sql(sql):
|
|
135
|
+
enforce_surface_mutation_authorization("clickstack.query", get_clickstack_adapter)
|
|
126
136
|
_require_container_backend()
|
|
127
137
|
phlo_dir = _ensure_phlo_dir()
|
|
128
138
|
project_name = get_project_name()
|
|
129
|
-
sql = _read_query(query=query, file=query_file)
|
|
130
139
|
|
|
131
140
|
cmd = compose_base_cmd(phlo_dir=phlo_dir, project_name=project_name)
|
|
132
141
|
cmd.extend(
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Clickstack CLI plugin registration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from phlo.plugins.base import cli_command_plugin_class
|
|
7
|
+
|
|
8
|
+
from phlo_clickstack.cli import clickstack_group
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ClickStackCliPlugin = cli_command_plugin_class(
|
|
12
|
+
"ClickStackCliPlugin",
|
|
13
|
+
name="clickstack",
|
|
14
|
+
version="0.1.0",
|
|
15
|
+
description="CLI commands for ClickStack query access",
|
|
16
|
+
commands=[clickstack_group],
|
|
17
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Clickstack service plugin registration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from phlo.plugins import service_plugin_class
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
ClickStackServicePlugin = service_plugin_class(
|
|
9
|
+
"ClickStackServicePlugin",
|
|
10
|
+
name="clickstack",
|
|
11
|
+
version="0.1.0",
|
|
12
|
+
description="ClickStack all-in-one observability backend",
|
|
13
|
+
author="Phlo Team",
|
|
14
|
+
tags=["observability", "logs", "metrics", "traces"],
|
|
15
|
+
)
|
|
@@ -12,13 +12,11 @@ compose:
|
|
|
12
12
|
environment:
|
|
13
13
|
FRONTEND_URL: ${CLICKSTACK_PUBLIC_URL:-}
|
|
14
14
|
ports:
|
|
15
|
-
- "${CLICKSTACK_PORT:-
|
|
16
|
-
- "${CLICKSTACK_HTTP_PORT:-
|
|
17
|
-
- "${
|
|
18
|
-
- "${CLICKSTACK_OTLP_HTTP_PORT:-4318}:4318"
|
|
19
|
-
- "${CLICKSTACK_NATIVE_PORT:-9000}:9000"
|
|
15
|
+
- "${CLICKSTACK_PORT:-18080}:8080"
|
|
16
|
+
- "${CLICKSTACK_HTTP_PORT:-18123}:8123"
|
|
17
|
+
- "${CLICKSTACK_NATIVE_PORT:-19002}:9000"
|
|
20
18
|
volumes:
|
|
21
|
-
-
|
|
19
|
+
- clickstack-data:/var/lib/clickhouse
|
|
22
20
|
healthcheck:
|
|
23
21
|
test:
|
|
24
22
|
[
|
|
@@ -38,19 +36,13 @@ env_vars:
|
|
|
38
36
|
default: "docker.hyperdx.io/hyperdx/hyperdx-all-in-one"
|
|
39
37
|
description: Official ClickStack all-in-one image reference
|
|
40
38
|
CLICKSTACK_PORT:
|
|
41
|
-
default:
|
|
39
|
+
default: 18080
|
|
42
40
|
description: ClickStack UI port
|
|
43
41
|
CLICKSTACK_HTTP_PORT:
|
|
44
|
-
default:
|
|
42
|
+
default: 18123
|
|
45
43
|
description: ClickHouse HTTP query port used by phlo-api trace endpoints
|
|
46
|
-
CLICKSTACK_OTLP_GRPC_PORT:
|
|
47
|
-
default: 4317
|
|
48
|
-
description: ClickStack OTLP gRPC ingest port
|
|
49
|
-
CLICKSTACK_OTLP_HTTP_PORT:
|
|
50
|
-
default: 4318
|
|
51
|
-
description: ClickStack OTLP HTTP ingest port
|
|
52
44
|
CLICKSTACK_NATIVE_PORT:
|
|
53
|
-
default:
|
|
45
|
+
default: 19002
|
|
54
46
|
description: ClickHouse native port exposed by the all-in-one image
|
|
55
47
|
CLICKSTACK_PUBLIC_URL:
|
|
56
48
|
default: ""
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: phlo-clickstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: clickstack service plugin for Phlo
|
|
5
5
|
Author-email: Phlo Team <team@phlo.dev>
|
|
6
6
|
License: MIT
|
|
7
7
|
Requires-Python: >=3.11
|
|
8
8
|
Description-Content-Type: text/plain
|
|
9
9
|
Requires-Dist: phlo>=0.1.0
|
|
10
|
-
Requires-Dist:
|
|
10
|
+
Requires-Dist: requests>=2.32.5
|
|
11
11
|
Provides-Extra: dev
|
|
12
12
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
13
13
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
@@ -51,6 +51,36 @@ def test_clickstack_query_runs_clickhouse_client(monkeypatch) -> None:
|
|
|
51
51
|
assert result.output == '{"1":1}\n'
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def test_clickstack_query_authorizes_only_mutating_sql(monkeypatch) -> None:
|
|
55
|
+
calls: list[str] = []
|
|
56
|
+
|
|
57
|
+
monkeypatch.setattr("phlo_clickstack.cli._ensure_phlo_dir", lambda: Path("/tmp/project/.phlo"))
|
|
58
|
+
monkeypatch.setattr(
|
|
59
|
+
"phlo_clickstack.cli._require_container_backend",
|
|
60
|
+
lambda: calls.append("backend"),
|
|
61
|
+
)
|
|
62
|
+
monkeypatch.setattr("phlo_clickstack.cli.get_project_name", lambda: "demo")
|
|
63
|
+
monkeypatch.setattr(
|
|
64
|
+
"phlo_clickstack.cli.compose_base_cmd",
|
|
65
|
+
lambda **_kwargs: ["docker", "compose", "-p", "demo"],
|
|
66
|
+
)
|
|
67
|
+
monkeypatch.setattr(
|
|
68
|
+
"phlo_clickstack.cli.run_command",
|
|
69
|
+
lambda cmd, **_kwargs: CompletedProcess(cmd, 0, stdout="ok\n", stderr=""),
|
|
70
|
+
)
|
|
71
|
+
monkeypatch.setattr(
|
|
72
|
+
"phlo_clickstack.cli.enforce_surface_mutation_authorization",
|
|
73
|
+
lambda *_args, **_kwargs: calls.append("auth"),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
select_result = CliRunner().invoke(clickstack_group, ["query", "SELECT 1"])
|
|
77
|
+
insert_result = CliRunner().invoke(clickstack_group, ["query", "INSERT INTO t VALUES (1)"])
|
|
78
|
+
|
|
79
|
+
assert select_result.exit_code == 0
|
|
80
|
+
assert insert_result.exit_code == 0
|
|
81
|
+
assert calls == ["backend", "auth", "backend"]
|
|
82
|
+
|
|
83
|
+
|
|
54
84
|
def test_clickstack_query_uses_selected_container_backend(monkeypatch, tmp_path) -> None:
|
|
55
85
|
phlo_dir = tmp_path / ".phlo"
|
|
56
86
|
phlo_dir.mkdir()
|
|
@@ -148,7 +178,9 @@ def test_clickstack_query_rejects_missing_input(monkeypatch) -> None:
|
|
|
148
178
|
result = CliRunner().invoke(clickstack_group, ["query"])
|
|
149
179
|
|
|
150
180
|
assert result.exit_code != 0
|
|
151
|
-
assert "
|
|
181
|
+
assert "Error: no SQL query provided" in result.output
|
|
182
|
+
assert "Provide an inline query argument or pass --file." in result.output
|
|
183
|
+
assert 'Run: phlo clickstack query "SELECT 1"' in result.output
|
|
152
184
|
|
|
153
185
|
|
|
154
186
|
def test_clickstack_query_surfaces_timeout(monkeypatch) -> None:
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Tests for ClickStack service plugin."""
|
|
2
|
+
|
|
3
|
+
from click.testing import CliRunner
|
|
4
|
+
|
|
5
|
+
from phlo_clickstack.cli import clickstack_group
|
|
6
|
+
from phlo_clickstack.plugin import ClickStackServicePlugin
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_clickstack_service_definition() -> None:
|
|
10
|
+
"""Validate ClickStack service definition defaults."""
|
|
11
|
+
plugin = ClickStackServicePlugin()
|
|
12
|
+
defn = plugin.service_definition
|
|
13
|
+
|
|
14
|
+
assert defn["name"] == "clickstack"
|
|
15
|
+
assert defn["profile"] == "observability"
|
|
16
|
+
assert "${CLICKSTACK_PORT:-18080}:8080" in defn["compose"]["ports"]
|
|
17
|
+
assert "${CLICKSTACK_HTTP_PORT:-18123}:8123" in defn["compose"]["ports"]
|
|
18
|
+
assert "${CLICKSTACK_NATIVE_PORT:-19002}:9000" in defn["compose"]["ports"]
|
|
19
|
+
assert not any("4317" in port or "4318" in port for port in defn["compose"]["ports"])
|
|
20
|
+
assert "clickstack-data:/var/lib/clickhouse" in defn["compose"]["volumes"]
|
|
21
|
+
assert "./volumes/clickstack:/var/lib/clickhouse" not in defn["compose"]["volumes"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_clickstack_plugin_metadata() -> None:
|
|
25
|
+
"""Validate ClickStack plugin metadata."""
|
|
26
|
+
plugin = ClickStackServicePlugin()
|
|
27
|
+
meta = plugin.metadata
|
|
28
|
+
|
|
29
|
+
assert meta.name == "clickstack"
|
|
30
|
+
assert "observability" in meta.tags
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_clickstack_query_rejects_partial_phlo_directory(monkeypatch, tmp_path) -> None:
|
|
34
|
+
"""Logging-created .phlo directories are not initialized service projects."""
|
|
35
|
+
monkeypatch.chdir(tmp_path)
|
|
36
|
+
(tmp_path / ".phlo" / "logs").mkdir(parents=True)
|
|
37
|
+
monkeypatch.setattr("phlo_clickstack.cli._require_container_backend", lambda: None)
|
|
38
|
+
|
|
39
|
+
result = CliRunner().invoke(clickstack_group, ["query", "SELECT 1"])
|
|
40
|
+
|
|
41
|
+
assert result.exit_code != 0
|
|
42
|
+
assert "Phlo services have not been initialized" in result.output
|
|
43
|
+
assert "Missing: .phlo/docker-compose.yml" in result.output
|
|
44
|
+
assert "Run: phlo services init" in result.output
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
"""Regulated surface adapter for ClickStack CLI.
|
|
2
|
-
|
|
3
|
-
This module provides the regulated surface adapter for the ClickStack CLI,
|
|
4
|
-
declaring mutation commands that require authorization and enforcing
|
|
5
|
-
through core enforcement.
|
|
6
|
-
|
|
7
|
-
Mutation commands (require authorization):
|
|
8
|
-
- clickstack.query
|
|
9
|
-
|
|
10
|
-
Read commands (no authorization):
|
|
11
|
-
- (none currently)
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
|
-
import os
|
|
17
|
-
import threading
|
|
18
|
-
from typing import Any
|
|
19
|
-
|
|
20
|
-
from phlo.cli.authorization import CliPrincipalResolver
|
|
21
|
-
from phlo.logging import get_logger
|
|
22
|
-
from phlo.security.adapters import (
|
|
23
|
-
EnforcementResult,
|
|
24
|
-
SurfaceOperation,
|
|
25
|
-
)
|
|
26
|
-
from phlo.security.enforcement import enforce
|
|
27
|
-
|
|
28
|
-
logger = get_logger(__name__)
|
|
29
|
-
|
|
30
|
-
SURFACE_NAME = "phlo-clickstack"
|
|
31
|
-
FRAMEWORK_TYPE = "cli"
|
|
32
|
-
|
|
33
|
-
MUTATION_COMMANDS: frozenset[str] = frozenset(
|
|
34
|
-
{
|
|
35
|
-
"clickstack.query",
|
|
36
|
-
}
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
READ_COMMANDS: frozenset[str] = frozenset()
|
|
40
|
-
|
|
41
|
-
COMMAND_RESOURCE_MAP: dict[str, str] = {
|
|
42
|
-
"clickstack.query": "dataset",
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
COMMAND_ACTION_MAP: dict[str, str] = {
|
|
46
|
-
"clickstack.query": "dataset.write",
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class ClickStackSurfaceAdapter:
|
|
51
|
-
"""Regulated surface adapter for ClickStack CLI.
|
|
52
|
-
|
|
53
|
-
Declares mutation commands and enforces authorization through core
|
|
54
|
-
enforcement for privileged operations.
|
|
55
|
-
"""
|
|
56
|
-
|
|
57
|
-
_instance: ClickStackSurfaceAdapter | None = None
|
|
58
|
-
_lock = threading.Lock()
|
|
59
|
-
|
|
60
|
-
def __init__(self) -> None:
|
|
61
|
-
self._resolver = CliPrincipalResolver()
|
|
62
|
-
|
|
63
|
-
@classmethod
|
|
64
|
-
def get_instance(cls) -> ClickStackSurfaceAdapter:
|
|
65
|
-
if cls._instance is None:
|
|
66
|
-
with cls._lock:
|
|
67
|
-
if cls._instance is None:
|
|
68
|
-
cls._instance = cls()
|
|
69
|
-
return cls._instance
|
|
70
|
-
|
|
71
|
-
@property
|
|
72
|
-
def surface_name(self) -> str:
|
|
73
|
-
return SURFACE_NAME
|
|
74
|
-
|
|
75
|
-
@property
|
|
76
|
-
def framework_type(self) -> str:
|
|
77
|
-
return FRAMEWORK_TYPE
|
|
78
|
-
|
|
79
|
-
def list_operations(self) -> list[SurfaceOperation]:
|
|
80
|
-
operations: list[SurfaceOperation] = []
|
|
81
|
-
for command in MUTATION_COMMANDS:
|
|
82
|
-
resource_type = COMMAND_RESOURCE_MAP.get(command, "dataset")
|
|
83
|
-
action = COMMAND_ACTION_MAP.get(command, f"clickstack.{command}")
|
|
84
|
-
operations.append(
|
|
85
|
-
SurfaceOperation(
|
|
86
|
-
action=action,
|
|
87
|
-
resource_type=resource_type,
|
|
88
|
-
operation_name=command,
|
|
89
|
-
resource_id_strategy=None,
|
|
90
|
-
framework_metadata={"command": command},
|
|
91
|
-
)
|
|
92
|
-
)
|
|
93
|
-
return operations
|
|
94
|
-
|
|
95
|
-
def is_active(self, runtime: Any) -> bool:
|
|
96
|
-
return True
|
|
97
|
-
|
|
98
|
-
def install(self, runtime: Any) -> None:
|
|
99
|
-
pass
|
|
100
|
-
|
|
101
|
-
def enforce_mutation(self, command: str, resource_id: str | None = None) -> EnforcementResult:
|
|
102
|
-
"""Enforce authorization for a mutation command."""
|
|
103
|
-
if command not in MUTATION_COMMANDS:
|
|
104
|
-
return EnforcementResult.allow()
|
|
105
|
-
|
|
106
|
-
action = COMMAND_ACTION_MAP.get(command, f"clickstack.{command}")
|
|
107
|
-
resource_type = COMMAND_RESOURCE_MAP.get(command, "dataset")
|
|
108
|
-
resource_id_final = resource_id or f"clickstack:{command}"
|
|
109
|
-
|
|
110
|
-
principal = self._resolver.resolve()
|
|
111
|
-
|
|
112
|
-
from phlo.capabilities.interfaces import ResourceRef
|
|
113
|
-
|
|
114
|
-
resource = ResourceRef(
|
|
115
|
-
resource_type=resource_type,
|
|
116
|
-
resource_id=resource_id_final,
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
request_id = os.environ.get("PHLO_REQUEST_ID")
|
|
120
|
-
|
|
121
|
-
result = enforce(
|
|
122
|
-
principal=principal,
|
|
123
|
-
action=action,
|
|
124
|
-
resource=resource,
|
|
125
|
-
context=None,
|
|
126
|
-
request_id=request_id,
|
|
127
|
-
surface=SURFACE_NAME,
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
logger.debug(
|
|
131
|
-
"clickstack_mutation_enforcement_result",
|
|
132
|
-
command=command,
|
|
133
|
-
action=action,
|
|
134
|
-
result=result.variant,
|
|
135
|
-
subject=principal.subject,
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
return result
|
|
139
|
-
|
|
140
|
-
def check_command_authorization(self, command_path: str) -> EnforcementResult:
|
|
141
|
-
"""Check if a command is authorized to run."""
|
|
142
|
-
if command_path in READ_COMMANDS:
|
|
143
|
-
return EnforcementResult.allow()
|
|
144
|
-
|
|
145
|
-
if command_path in MUTATION_COMMANDS:
|
|
146
|
-
return self.enforce_mutation(command_path)
|
|
147
|
-
|
|
148
|
-
logger.warning(
|
|
149
|
-
"clickstack_unknown_command_classification",
|
|
150
|
-
command=command_path,
|
|
151
|
-
)
|
|
152
|
-
return EnforcementResult.deny(
|
|
153
|
-
reason_code="unknown_command",
|
|
154
|
-
explanation=f"Command '{command_path}' is not classified as read or mutation",
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
_adapter: ClickStackSurfaceAdapter | None = None
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def get_adapter() -> ClickStackSurfaceAdapter:
|
|
162
|
-
"""Get the singleton ClickStack surface adapter."""
|
|
163
|
-
global _adapter
|
|
164
|
-
if _adapter is None:
|
|
165
|
-
_adapter = ClickStackSurfaceAdapter()
|
|
166
|
-
return _adapter
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
"""CLI plugin for ClickStack commands.
|
|
2
|
-
|
|
3
|
-
This module defines the ClickStackCliPlugin class which registers CLI commands
|
|
4
|
-
for interacting with the ClickStack ClickHouse service.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import click
|
|
10
|
-
|
|
11
|
-
from phlo.plugins.base import CliCommandPlugin, PluginMetadata
|
|
12
|
-
|
|
13
|
-
from phlo_clickstack.cli import clickstack_group
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class ClickStackCliPlugin(CliCommandPlugin):
|
|
17
|
-
"""Register ClickStack CLI commands.
|
|
18
|
-
|
|
19
|
-
This plugin provides CLI commands for querying and managing the
|
|
20
|
-
ClickStack ClickHouse service instance.
|
|
21
|
-
|
|
22
|
-
Example:
|
|
23
|
-
Registered automatically when phlo_clickstack is installed.
|
|
24
|
-
Use `phlo clickstack --help` to see available commands.
|
|
25
|
-
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
@property
|
|
29
|
-
def metadata(self) -> PluginMetadata:
|
|
30
|
-
"""Return plugin metadata for ClickStack CLI registration.
|
|
31
|
-
|
|
32
|
-
Returns:
|
|
33
|
-
PluginMetadata: Metadata including name, version, and description.
|
|
34
|
-
|
|
35
|
-
"""
|
|
36
|
-
return PluginMetadata(
|
|
37
|
-
name="clickstack",
|
|
38
|
-
version="0.1.0",
|
|
39
|
-
description="CLI commands for ClickStack query access",
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
def get_cli_commands(self) -> list[click.Command]:
|
|
43
|
-
"""Return ClickStack CLI commands.
|
|
44
|
-
|
|
45
|
-
Returns:
|
|
46
|
-
list[click.Command]: List of Click commands provided by this plugin.
|
|
47
|
-
|
|
48
|
-
"""
|
|
49
|
-
return [clickstack_group]
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
"""ClickStack service plugin.
|
|
2
|
-
|
|
3
|
-
This module defines the ClickStackServicePlugin class which provides
|
|
4
|
-
service registration and definition for the ClickStack observability
|
|
5
|
-
backend service.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
from phlo.plugins import PackageYamlServicePlugin, PluginMetadata
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class ClickStackServicePlugin(PackageYamlServicePlugin):
|
|
14
|
-
"""Service plugin for ClickStack.
|
|
15
|
-
|
|
16
|
-
Provides ClickStack (ClickHouse-based observability backend) as a
|
|
17
|
-
managed service within the Phlo services framework. The service
|
|
18
|
-
definition is loaded from the bundled service.yaml file.
|
|
19
|
-
|
|
20
|
-
Example:
|
|
21
|
-
Plugin is automatically discovered via entry points.
|
|
22
|
-
Service is started with `phlo services start clickstack`.
|
|
23
|
-
|
|
24
|
-
Attributes:
|
|
25
|
-
service_definition: ClickStack Docker Compose configuration.
|
|
26
|
-
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
@property
|
|
30
|
-
def metadata(self) -> PluginMetadata:
|
|
31
|
-
"""Return plugin metadata for ClickStack service registration.
|
|
32
|
-
|
|
33
|
-
Returns:
|
|
34
|
-
PluginMetadata: Metadata including name, version, description,
|
|
35
|
-
author, and tags for discovery.
|
|
36
|
-
|
|
37
|
-
"""
|
|
38
|
-
return PluginMetadata(
|
|
39
|
-
name="clickstack",
|
|
40
|
-
version="0.1.0",
|
|
41
|
-
description="ClickStack all-in-one observability backend",
|
|
42
|
-
author="Phlo Team",
|
|
43
|
-
tags=["observability", "logs", "metrics", "traces"],
|
|
44
|
-
)
|
|
@@ -1,21 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
{phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/src/phlo_clickstack/observability_backend.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/src/phlo_clickstack.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{phlo_clickstack-0.3.0 → phlo_clickstack-0.4.0}/src/phlo_clickstack.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|