platzky 0.4.1__py3-none-any.whl → 1.0.0__py3-none-any.whl
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.
- platzky/config.py +141 -3
- platzky/db/db.py +9 -0
- platzky/db/graph_ql_db.py +15 -0
- platzky/db/json_db.py +8 -0
- platzky/db/mongodb_db.py +8 -0
- platzky/engine.py +72 -2
- platzky/platzky.py +20 -0
- platzky/telemetry.py +123 -0
- {platzky-0.4.1.dist-info → platzky-1.0.0.dist-info}/METADATA +12 -1
- {platzky-0.4.1.dist-info → platzky-1.0.0.dist-info}/RECORD +12 -10
- platzky-1.0.0.dist-info/licenses/LICENSE +21 -0
- {platzky-0.4.1.dist-info → platzky-1.0.0.dist-info}/WHEEL +0 -0
platzky/config.py
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
|
+
"""Configuration module for Platzky application.
|
|
2
|
+
|
|
3
|
+
This module defines all configuration models and parsing logic for the application.
|
|
4
|
+
"""
|
|
5
|
+
|
|
1
6
|
import sys
|
|
2
7
|
import typing as t
|
|
3
8
|
|
|
4
9
|
import yaml
|
|
5
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
6
11
|
|
|
7
12
|
from .db.db import DBConfig
|
|
8
13
|
from .db.db_loader import get_db_module
|
|
9
14
|
|
|
10
15
|
|
|
11
16
|
class StrictBaseModel(BaseModel):
|
|
17
|
+
"""Base model with immutable (frozen) configuration."""
|
|
18
|
+
|
|
12
19
|
model_config = ConfigDict(frozen=True)
|
|
13
20
|
|
|
14
21
|
|
|
15
22
|
class LanguageConfig(StrictBaseModel):
|
|
23
|
+
"""Configuration for a single language.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
name: Display name of the language
|
|
27
|
+
flag: Flag icon code (country code)
|
|
28
|
+
country: Country code
|
|
29
|
+
domain: Optional domain specific to this language
|
|
30
|
+
"""
|
|
31
|
+
|
|
16
32
|
name: str = Field(alias="name")
|
|
17
33
|
flag: str = Field(alias="flag")
|
|
18
34
|
country: str = Field(alias="country")
|
|
@@ -22,12 +38,111 @@ class LanguageConfig(StrictBaseModel):
|
|
|
22
38
|
Languages = dict[str, LanguageConfig]
|
|
23
39
|
LanguagesMapping = t.Mapping[str, t.Mapping[str, str]]
|
|
24
40
|
|
|
41
|
+
# Validation error messages
|
|
42
|
+
_INVALID_ENDPOINT_FORMAT_MSG = (
|
|
43
|
+
"Invalid endpoint: '{}'. Must be host:port or [http|https]://host[:port]"
|
|
44
|
+
)
|
|
45
|
+
_INVALID_ENDPOINT_SCHEME_MSG = "Invalid endpoint scheme: '{}'. Must be http or https"
|
|
46
|
+
_MISSING_HOSTNAME_MSG = "Invalid endpoint: '{}'. Missing hostname"
|
|
47
|
+
|
|
25
48
|
|
|
26
49
|
def languages_dict(languages: Languages) -> LanguagesMapping:
|
|
27
|
-
|
|
50
|
+
"""Convert Languages configuration to a mapping dictionary.
|
|
51
|
+
|
|
52
|
+
Excludes None values to align with type signature.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
languages: Dictionary of language configurations
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Mapping of language codes to their configuration dictionaries (excludes None values)
|
|
59
|
+
"""
|
|
60
|
+
return {
|
|
61
|
+
name: {k: v for k, v in lang.model_dump().items() if v is not None}
|
|
62
|
+
for name, lang in languages.items()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TelemetryConfig(StrictBaseModel):
|
|
67
|
+
"""OpenTelemetry configuration for application tracing.
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
enabled: Enable or disable telemetry tracing
|
|
71
|
+
endpoint: OTLP gRPC endpoint (e.g., localhost:4317 or http://localhost:4317)
|
|
72
|
+
console_export: Export traces to console for debugging
|
|
73
|
+
timeout: Timeout in seconds for exporter (default: 10)
|
|
74
|
+
deployment_environment: Deployment environment (e.g., production, staging, dev)
|
|
75
|
+
service_instance_id: Service instance ID (auto-generated if not provided)
|
|
76
|
+
flush_on_request: Flush spans after each request (default: True, may impact latency)
|
|
77
|
+
flush_timeout_ms: Timeout in milliseconds for per-request flush (default: 5000)
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
enabled: bool = Field(default=False, alias="enabled")
|
|
81
|
+
endpoint: t.Optional[str] = Field(default=None, alias="endpoint")
|
|
82
|
+
console_export: bool = Field(default=False, alias="console_export")
|
|
83
|
+
timeout: int = Field(default=10, alias="timeout", gt=0)
|
|
84
|
+
deployment_environment: t.Optional[str] = Field(default=None, alias="deployment_environment")
|
|
85
|
+
service_instance_id: t.Optional[str] = Field(default=None, alias="service_instance_id")
|
|
86
|
+
flush_on_request: bool = Field(default=True, alias="flush_on_request")
|
|
87
|
+
flush_timeout_ms: int = Field(default=5000, alias="flush_timeout_ms", gt=0)
|
|
88
|
+
|
|
89
|
+
@field_validator("endpoint")
|
|
90
|
+
@classmethod
|
|
91
|
+
def validate_endpoint(cls, v: t.Optional[str]) -> t.Optional[str]:
|
|
92
|
+
"""Validate endpoint URL format.
|
|
93
|
+
|
|
94
|
+
Accepts OTLP/gRPC spec-compliant formats:
|
|
95
|
+
- host:port (e.g., localhost:4317)
|
|
96
|
+
- http://host[:port]
|
|
97
|
+
- https://host[:port]
|
|
98
|
+
|
|
99
|
+
Note: grpc:// scheme is NOT supported per OTLP spec and will be rejected.
|
|
100
|
+
"""
|
|
101
|
+
if v is None:
|
|
102
|
+
return v
|
|
103
|
+
|
|
104
|
+
from urllib.parse import urlparse
|
|
105
|
+
|
|
106
|
+
# Check if it has a scheme (contains ://)
|
|
107
|
+
if "://" not in v:
|
|
108
|
+
# Must be host:port format - validate it has a colon
|
|
109
|
+
if ":" in v and not v.startswith("/"):
|
|
110
|
+
return v
|
|
111
|
+
raise ValueError(_INVALID_ENDPOINT_FORMAT_MSG.format(v))
|
|
112
|
+
|
|
113
|
+
# Parse URL with scheme
|
|
114
|
+
parsed = urlparse(v)
|
|
115
|
+
|
|
116
|
+
# Validate scheme (only http/https per OTLP spec, grpc is NOT supported)
|
|
117
|
+
if parsed.scheme not in ("http", "https"):
|
|
118
|
+
raise ValueError(_INVALID_ENDPOINT_SCHEME_MSG.format(parsed.scheme))
|
|
119
|
+
|
|
120
|
+
# Validate hostname exists
|
|
121
|
+
if not parsed.hostname:
|
|
122
|
+
raise ValueError(_MISSING_HOSTNAME_MSG.format(v))
|
|
123
|
+
|
|
124
|
+
return v
|
|
28
125
|
|
|
29
126
|
|
|
30
127
|
class Config(StrictBaseModel):
|
|
128
|
+
"""Main application configuration.
|
|
129
|
+
|
|
130
|
+
Attributes:
|
|
131
|
+
app_name: Application name
|
|
132
|
+
secret_key: Flask secret key for sessions
|
|
133
|
+
db: Database configuration
|
|
134
|
+
use_www: Redirect non-www to www URLs
|
|
135
|
+
seo_prefix: URL prefix for SEO routes
|
|
136
|
+
blog_prefix: URL prefix for blog routes
|
|
137
|
+
languages: Supported languages configuration
|
|
138
|
+
domain_to_lang: Domain to language mapping
|
|
139
|
+
translation_directories: Additional translation directories
|
|
140
|
+
debug: Enable debug mode
|
|
141
|
+
testing: Enable testing mode
|
|
142
|
+
feature_flags: Feature flag configuration
|
|
143
|
+
telemetry: OpenTelemetry configuration
|
|
144
|
+
"""
|
|
145
|
+
|
|
31
146
|
app_name: str = Field(alias="APP_NAME")
|
|
32
147
|
secret_key: str = Field(alias="SECRET_KEY")
|
|
33
148
|
db: DBConfig = Field(alias="DB")
|
|
@@ -42,7 +157,8 @@ class Config(StrictBaseModel):
|
|
|
42
157
|
)
|
|
43
158
|
debug: bool = Field(default=False, alias="DEBUG")
|
|
44
159
|
testing: bool = Field(default=False, alias="TESTING")
|
|
45
|
-
feature_flags: t.Optional[dict[str, bool]] = Field(
|
|
160
|
+
feature_flags: t.Optional[dict[str, bool]] = Field(default=None, alias="FEATURE_FLAGS")
|
|
161
|
+
telemetry: TelemetryConfig = Field(default_factory=TelemetryConfig, alias="TELEMETRY")
|
|
46
162
|
|
|
47
163
|
@classmethod
|
|
48
164
|
def model_validate(
|
|
@@ -53,6 +169,17 @@ class Config(StrictBaseModel):
|
|
|
53
169
|
from_attributes: bool | None = None,
|
|
54
170
|
context: dict[str, t.Any] | None = None,
|
|
55
171
|
) -> "Config":
|
|
172
|
+
"""Validate and construct Config from dictionary.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
obj: Configuration dictionary
|
|
176
|
+
strict: Enable strict validation
|
|
177
|
+
from_attributes: Populate from object attributes
|
|
178
|
+
context: Additional validation context
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Validated Config instance
|
|
182
|
+
"""
|
|
56
183
|
db_cfg_type = get_db_module(obj["DB"]["TYPE"]).db_config_type()
|
|
57
184
|
obj["DB"] = db_cfg_type.model_validate(obj["DB"])
|
|
58
185
|
return super().model_validate(
|
|
@@ -61,6 +188,17 @@ class Config(StrictBaseModel):
|
|
|
61
188
|
|
|
62
189
|
@classmethod
|
|
63
190
|
def parse_yaml(cls, path: str) -> "Config":
|
|
191
|
+
"""Parse configuration from YAML file.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
path: Path to YAML configuration file
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Validated Config instance
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
SystemExit: If config file is not found
|
|
201
|
+
"""
|
|
64
202
|
try:
|
|
65
203
|
with open(path, "r") as f:
|
|
66
204
|
return cls.model_validate(yaml.safe_load(f))
|
platzky/db/db.py
CHANGED
|
@@ -96,6 +96,15 @@ class DB(ABC):
|
|
|
96
96
|
def get_font(self) -> str:
|
|
97
97
|
pass
|
|
98
98
|
|
|
99
|
+
@abstractmethod
|
|
100
|
+
def health_check(self) -> None:
|
|
101
|
+
"""Perform a health check on the database.
|
|
102
|
+
|
|
103
|
+
Should raise an exception if the database is not healthy.
|
|
104
|
+
This should be a lightweight operation suitable for health checks.
|
|
105
|
+
"""
|
|
106
|
+
pass
|
|
107
|
+
|
|
99
108
|
|
|
100
109
|
class DBConfig(BaseModel):
|
|
101
110
|
type: str = Field(alias="TYPE")
|
platzky/db/graph_ql_db.py
CHANGED
|
@@ -307,3 +307,18 @@ class GraphQL(DB):
|
|
|
307
307
|
"""
|
|
308
308
|
)
|
|
309
309
|
return self.client.execute(plugins_data)["pluginConfigs"]
|
|
310
|
+
|
|
311
|
+
def health_check(self) -> None:
|
|
312
|
+
"""Perform a health check on the GraphQL database.
|
|
313
|
+
|
|
314
|
+
Raises an exception if the database is not accessible.
|
|
315
|
+
"""
|
|
316
|
+
# Simple query to check connectivity
|
|
317
|
+
health_query = gql(
|
|
318
|
+
"""
|
|
319
|
+
query {
|
|
320
|
+
__typename
|
|
321
|
+
}
|
|
322
|
+
"""
|
|
323
|
+
)
|
|
324
|
+
self.client.execute(health_query)
|
platzky/db/json_db.py
CHANGED
|
@@ -116,3 +116,11 @@ class Json(DB):
|
|
|
116
116
|
|
|
117
117
|
def get_plugins_data(self):
|
|
118
118
|
return self.data.get("plugins", [])
|
|
119
|
+
|
|
120
|
+
def health_check(self) -> None:
|
|
121
|
+
"""Perform a health check on the JSON database.
|
|
122
|
+
|
|
123
|
+
Raises an exception if the database is not accessible.
|
|
124
|
+
"""
|
|
125
|
+
# Try to access site_content to ensure basic structure is valid
|
|
126
|
+
self._get_site_content()
|
platzky/db/mongodb_db.py
CHANGED
|
@@ -125,6 +125,14 @@ class MongoDB(DB):
|
|
|
125
125
|
return site_content.get("font", "")
|
|
126
126
|
return ""
|
|
127
127
|
|
|
128
|
+
def health_check(self) -> None:
|
|
129
|
+
"""Perform a health check on the MongoDB database.
|
|
130
|
+
|
|
131
|
+
Raises an exception if the database is not accessible.
|
|
132
|
+
"""
|
|
133
|
+
# Simple ping to check if database is accessible
|
|
134
|
+
self.client.admin.command("ping")
|
|
135
|
+
|
|
128
136
|
def _close_connection(self) -> None:
|
|
129
137
|
"""Close the MongoDB connection"""
|
|
130
138
|
if self.client:
|
platzky/engine.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
|
3
|
+
from typing import Any, Callable, Dict, List, Tuple
|
|
3
4
|
|
|
4
|
-
from flask import Flask, request, session
|
|
5
|
+
from flask import Blueprint, Flask, jsonify, make_response, request, session
|
|
5
6
|
from flask_babel import Babel
|
|
6
7
|
|
|
7
8
|
from platzky.config import Config
|
|
@@ -17,6 +18,8 @@ class Engine(Flask):
|
|
|
17
18
|
self.login_methods = []
|
|
18
19
|
self.dynamic_body = ""
|
|
19
20
|
self.dynamic_head = ""
|
|
21
|
+
self.health_checks: List[Tuple[str, Callable[[], None]]] = []
|
|
22
|
+
self.telemetry_instrumented: bool = False
|
|
20
23
|
directory = os.path.dirname(os.path.realpath(__file__))
|
|
21
24
|
locale_dir = os.path.join(directory, "locale")
|
|
22
25
|
config.translation_directories.append(locale_dir)
|
|
@@ -26,6 +29,7 @@ class Engine(Flask):
|
|
|
26
29
|
locale_selector=self.get_locale,
|
|
27
30
|
default_translation_directories=babel_translation_directories,
|
|
28
31
|
)
|
|
32
|
+
self._register_default_health_endpoints()
|
|
29
33
|
|
|
30
34
|
self.cms_modules: List[CmsModule] = []
|
|
31
35
|
# TODO add plugins as CMS Module - all plugins should be visible from
|
|
@@ -69,3 +73,69 @@ class Engine(Flask):
|
|
|
69
73
|
|
|
70
74
|
session["language"] = lang
|
|
71
75
|
return lang
|
|
76
|
+
|
|
77
|
+
def add_health_check(self, name: str, check_function: Callable[[], None]) -> None:
|
|
78
|
+
"""Register a health check function"""
|
|
79
|
+
if not callable(check_function):
|
|
80
|
+
raise TypeError(f"check_function must be callable, got {type(check_function)}")
|
|
81
|
+
self.health_checks.append((name, check_function))
|
|
82
|
+
|
|
83
|
+
def _register_default_health_endpoints(self):
|
|
84
|
+
"""Register default health endpoints"""
|
|
85
|
+
|
|
86
|
+
health_bp = Blueprint("health", __name__)
|
|
87
|
+
HEALTH_CHECK_TIMEOUT = 10 # seconds
|
|
88
|
+
|
|
89
|
+
@health_bp.route("/health/liveness")
|
|
90
|
+
def liveness():
|
|
91
|
+
"""Simple liveness check - is the app running?"""
|
|
92
|
+
return jsonify({"status": "alive"}), 200
|
|
93
|
+
|
|
94
|
+
@health_bp.route("/health/readiness")
|
|
95
|
+
def readiness():
|
|
96
|
+
"""Readiness check - can the app serve traffic?"""
|
|
97
|
+
health_status: Dict[str, Any] = {"status": "ready", "checks": {}}
|
|
98
|
+
status_code = 200
|
|
99
|
+
|
|
100
|
+
executor = ThreadPoolExecutor(max_workers=1)
|
|
101
|
+
try:
|
|
102
|
+
# Database health check with timeout
|
|
103
|
+
future = executor.submit(self.db.health_check)
|
|
104
|
+
try:
|
|
105
|
+
future.result(timeout=HEALTH_CHECK_TIMEOUT)
|
|
106
|
+
health_status["checks"]["database"] = "ok"
|
|
107
|
+
except TimeoutError:
|
|
108
|
+
health_status["checks"]["database"] = "failed: timeout"
|
|
109
|
+
health_status["status"] = "not_ready"
|
|
110
|
+
status_code = 503
|
|
111
|
+
except Exception as e:
|
|
112
|
+
health_status["checks"]["database"] = f"failed: {e!s}"
|
|
113
|
+
health_status["status"] = "not_ready"
|
|
114
|
+
status_code = 503
|
|
115
|
+
|
|
116
|
+
# Run application-registered health checks
|
|
117
|
+
for check_name, check_func in self.health_checks:
|
|
118
|
+
future = executor.submit(check_func)
|
|
119
|
+
try:
|
|
120
|
+
future.result(timeout=HEALTH_CHECK_TIMEOUT)
|
|
121
|
+
health_status["checks"][check_name] = "ok"
|
|
122
|
+
except TimeoutError:
|
|
123
|
+
health_status["checks"][check_name] = "failed: timeout"
|
|
124
|
+
health_status["status"] = "not_ready"
|
|
125
|
+
status_code = 503
|
|
126
|
+
except Exception as e:
|
|
127
|
+
health_status["checks"][check_name] = f"failed: {e!s}"
|
|
128
|
+
health_status["status"] = "not_ready"
|
|
129
|
+
status_code = 503
|
|
130
|
+
finally:
|
|
131
|
+
# Shutdown without waiting if any futures are still running
|
|
132
|
+
executor.shutdown(wait=False)
|
|
133
|
+
|
|
134
|
+
return make_response(jsonify(health_status), status_code)
|
|
135
|
+
|
|
136
|
+
# Simple /health alias for liveness
|
|
137
|
+
@health_bp.route("/health")
|
|
138
|
+
def health():
|
|
139
|
+
return liveness()
|
|
140
|
+
|
|
141
|
+
self.register_blueprint(health_bp)
|
platzky/platzky.py
CHANGED
|
@@ -17,6 +17,12 @@ from platzky.plugin.plugin_loader import plugify
|
|
|
17
17
|
from platzky.seo import seo
|
|
18
18
|
from platzky.www_handler import redirect_nonwww_to_www, redirect_www_to_nonwww
|
|
19
19
|
|
|
20
|
+
_MISSING_OTEL_MSG = (
|
|
21
|
+
"OpenTelemetry is not installed. Install with: "
|
|
22
|
+
"poetry add opentelemetry-api opentelemetry-sdk "
|
|
23
|
+
"opentelemetry-instrumentation-flask opentelemetry-exporter-otlp-proto-grpc"
|
|
24
|
+
)
|
|
25
|
+
|
|
20
26
|
|
|
21
27
|
def create_engine(config: Config, db) -> Engine:
|
|
22
28
|
app = Engine(config, db, __name__)
|
|
@@ -85,6 +91,20 @@ def create_app_from_config(config: Config) -> Engine:
|
|
|
85
91
|
db = get_db(config.db)
|
|
86
92
|
engine = create_engine(config, db)
|
|
87
93
|
|
|
94
|
+
# Setup telemetry (optional feature)
|
|
95
|
+
if config.telemetry.enabled:
|
|
96
|
+
try:
|
|
97
|
+
from platzky.telemetry import setup_telemetry
|
|
98
|
+
|
|
99
|
+
setup_telemetry(engine, config.telemetry)
|
|
100
|
+
except ImportError as e:
|
|
101
|
+
raise ImportError(_MISSING_OTEL_MSG) from e
|
|
102
|
+
except ValueError as e:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"Telemetry configuration error: {e}. "
|
|
105
|
+
"Check your telemetry settings in the configuration file."
|
|
106
|
+
) from e
|
|
107
|
+
|
|
88
108
|
admin_blueprint = admin.create_admin_blueprint(
|
|
89
109
|
login_methods=engine.login_methods, cms_modules=engine.cms_modules
|
|
90
110
|
)
|
platzky/telemetry.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import socket
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import TYPE_CHECKING, Optional
|
|
5
|
+
|
|
6
|
+
from platzky.config import TelemetryConfig
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from opentelemetry.trace import Tracer
|
|
10
|
+
|
|
11
|
+
from platzky.engine import Engine
|
|
12
|
+
|
|
13
|
+
# Error messages
|
|
14
|
+
_MISSING_EXPORTERS_MSG = (
|
|
15
|
+
"Telemetry is enabled but no exporters are configured. "
|
|
16
|
+
"Set endpoint or console_export=True to export traces."
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def setup_telemetry(app: "Engine", telemetry_config: TelemetryConfig) -> Optional["Tracer"]:
|
|
21
|
+
"""Setup OpenTelemetry tracing for Flask application.
|
|
22
|
+
|
|
23
|
+
Configures and initializes OpenTelemetry tracing with OTLP and/or console exporters.
|
|
24
|
+
Automatically instruments Flask to capture HTTP requests and trace information.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
app: Engine instance (Flask-based application)
|
|
28
|
+
telemetry_config: Telemetry configuration specifying endpoint and export options
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
OpenTelemetry tracer instance if enabled, None otherwise
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
ImportError: If OpenTelemetry packages are not installed when telemetry is enabled
|
|
35
|
+
ValueError: If telemetry is enabled but no exporters are configured
|
|
36
|
+
"""
|
|
37
|
+
if not telemetry_config.enabled:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
# Reject telemetry enabled without exporters (creates overhead without benefit)
|
|
41
|
+
if not telemetry_config.endpoint and not telemetry_config.console_export:
|
|
42
|
+
raise ValueError(_MISSING_EXPORTERS_MSG)
|
|
43
|
+
|
|
44
|
+
# If already instrumented, return tracer without rebuilding provider/exporters
|
|
45
|
+
if app.telemetry_instrumented:
|
|
46
|
+
from opentelemetry import trace
|
|
47
|
+
|
|
48
|
+
return trace.get_tracer(__name__)
|
|
49
|
+
|
|
50
|
+
# Import OpenTelemetry modules (will raise ImportError if not installed)
|
|
51
|
+
from opentelemetry import trace
|
|
52
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
53
|
+
from opentelemetry.instrumentation.flask import FlaskInstrumentor
|
|
54
|
+
from opentelemetry.sdk.resources import Resource
|
|
55
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
56
|
+
from opentelemetry.sdk.trace.export import (
|
|
57
|
+
BatchSpanProcessor,
|
|
58
|
+
ConsoleSpanExporter,
|
|
59
|
+
SimpleSpanProcessor,
|
|
60
|
+
)
|
|
61
|
+
from opentelemetry.semconv.resource import ResourceAttributes
|
|
62
|
+
|
|
63
|
+
# Build resource attributes
|
|
64
|
+
service_name = app.config.get("APP_NAME", "platzky")
|
|
65
|
+
resource_attrs: dict[str, str] = {
|
|
66
|
+
ResourceAttributes.SERVICE_NAME: service_name,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Auto-detect service version from package metadata
|
|
70
|
+
from importlib.metadata import PackageNotFoundError
|
|
71
|
+
from importlib.metadata import version as get_version
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
resource_attrs[ResourceAttributes.SERVICE_VERSION] = get_version("platzky")
|
|
75
|
+
except PackageNotFoundError:
|
|
76
|
+
pass # Version not available
|
|
77
|
+
|
|
78
|
+
if telemetry_config.deployment_environment:
|
|
79
|
+
resource_attrs[ResourceAttributes.DEPLOYMENT_ENVIRONMENT] = (
|
|
80
|
+
telemetry_config.deployment_environment
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Add instance ID (user-provided or auto-generated)
|
|
84
|
+
if telemetry_config.service_instance_id:
|
|
85
|
+
resource_attrs[ResourceAttributes.SERVICE_INSTANCE_ID] = (
|
|
86
|
+
telemetry_config.service_instance_id
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
# Generate unique instance ID: hostname + short UUID
|
|
90
|
+
hostname = socket.gethostname()
|
|
91
|
+
instance_uuid = str(uuid.uuid4())[:8]
|
|
92
|
+
resource_attrs[ResourceAttributes.SERVICE_INSTANCE_ID] = f"{hostname}-{instance_uuid}"
|
|
93
|
+
|
|
94
|
+
resource = Resource.create(resource_attrs)
|
|
95
|
+
provider = TracerProvider(resource=resource)
|
|
96
|
+
|
|
97
|
+
# Configure exporter based on endpoint
|
|
98
|
+
if telemetry_config.endpoint:
|
|
99
|
+
exporter = OTLPSpanExporter(
|
|
100
|
+
endpoint=telemetry_config.endpoint, timeout=telemetry_config.timeout
|
|
101
|
+
)
|
|
102
|
+
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
103
|
+
|
|
104
|
+
# Optional console export
|
|
105
|
+
if telemetry_config.console_export:
|
|
106
|
+
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
|
|
107
|
+
|
|
108
|
+
trace.set_tracer_provider(provider)
|
|
109
|
+
FlaskInstrumentor().instrument_app(app)
|
|
110
|
+
app.telemetry_instrumented = True
|
|
111
|
+
|
|
112
|
+
# Optionally flush spans after each request (may impact latency)
|
|
113
|
+
if telemetry_config.flush_on_request:
|
|
114
|
+
|
|
115
|
+
@app.teardown_appcontext
|
|
116
|
+
def flush_telemetry(_exc: Optional[BaseException] = None) -> None:
|
|
117
|
+
"""Flush pending spans after request completion."""
|
|
118
|
+
provider.force_flush(timeout_millis=telemetry_config.flush_timeout_ms)
|
|
119
|
+
|
|
120
|
+
# Shutdown provider once at process exit
|
|
121
|
+
atexit.register(provider.shutdown)
|
|
122
|
+
|
|
123
|
+
return trace.get_tracer(__name__)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: platzky
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Not only blog engine
|
|
5
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Requires-Python: >=3.10,<4.0
|
|
7
8
|
Classifier: License :: OSI Approved :: MIT License
|
|
8
9
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -11,6 +12,8 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
11
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
12
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
13
14
|
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Provides-Extra: docs
|
|
16
|
+
Provides-Extra: telemetry
|
|
14
17
|
Requires-Dist: Flask (==3.0.3)
|
|
15
18
|
Requires-Dist: Flask-Babel (>=4.0.0,<5.0.0)
|
|
16
19
|
Requires-Dist: Flask-Minify (>=0.42,<0.43)
|
|
@@ -21,9 +24,17 @@ Requires-Dist: deprecation (>=2.1.0,<3.0.0)
|
|
|
21
24
|
Requires-Dist: google-cloud-storage (>=2.5.0,<3.0.0)
|
|
22
25
|
Requires-Dist: gql (>=3.4.0,<4.0.0)
|
|
23
26
|
Requires-Dist: humanize (>=4.9.0,<5.0.0)
|
|
27
|
+
Requires-Dist: myst-parser (>=4.0.0,<5.0.0) ; extra == "docs"
|
|
28
|
+
Requires-Dist: opentelemetry-api (>=1.27.0,<2.0.0) ; extra == "telemetry"
|
|
29
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc (>=1.27.0,<2.0.0) ; extra == "telemetry"
|
|
30
|
+
Requires-Dist: opentelemetry-instrumentation-flask (>=0.48b0,<0.49) ; extra == "telemetry"
|
|
31
|
+
Requires-Dist: opentelemetry-sdk (>=1.27.0,<2.0.0) ; extra == "telemetry"
|
|
24
32
|
Requires-Dist: pydantic (>=2.7.1,<3.0.0)
|
|
25
33
|
Requires-Dist: pygithub (>=2.6.1,<3.0.0)
|
|
26
34
|
Requires-Dist: pymongo (>=4.7.0,<5.0.0)
|
|
35
|
+
Requires-Dist: sphinx (>=8.0.0,<9.0.0) ; extra == "docs"
|
|
36
|
+
Requires-Dist: sphinx-rtd-theme (>=3.0.0,<4.0.0) ; extra == "docs"
|
|
37
|
+
Requires-Dist: tomli (>=2.0.0,<3.0.0) ; extra == "docs"
|
|
27
38
|
Description-Content-Type: text/markdown
|
|
28
39
|
|
|
29
40
|

|
|
@@ -7,27 +7,28 @@ platzky/admin/templates/module.html,sha256=WuQZxKQDD4INl-QF2uiKHf9Fmf2h7cEW9RLe1
|
|
|
7
7
|
platzky/blog/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
platzky/blog/blog.py,sha256=L9IYWxnLo1v1h_wLF-0HyG1Y4RSGg7maEMnYxhTgG5Y,2971
|
|
9
9
|
platzky/blog/comment_form.py,sha256=4lkNJ_S_2DZmJBbz-NPDqahvy2Zz5AGNH2spFeGIop4,513
|
|
10
|
-
platzky/config.py,sha256=
|
|
10
|
+
platzky/config.py,sha256=N5cQjV8Jh_fETw9jXE-UOcC-bSlC3l_04yMMtRwXKXw,7365
|
|
11
11
|
platzky/db/README.md,sha256=IO-LoDsd4dLBZenaz423EZjvEOQu_8m2OC0G7du170w,1753
|
|
12
12
|
platzky/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
platzky/db/db.py,sha256=
|
|
13
|
+
platzky/db/db.py,sha256=0h5rGCBO_N1wBqJRl5EoiW_bFDpNIvmNwuA0hJi89jw,3060
|
|
14
14
|
platzky/db/db_loader.py,sha256=CuEiXxhIa4bFMm0vi7ugzm7j3WycilGRKCU6smgIImE,905
|
|
15
15
|
platzky/db/github_json_db.py,sha256=G1GBIomeKOCeG05pA4qccaFntiGzkgyEMQJz_FQlvNY,2185
|
|
16
16
|
platzky/db/google_json_db.py,sha256=rS__UEK7ed71htTg066_vzpg0etTlpke6YkcrAQ3Fgk,1325
|
|
17
|
-
platzky/db/graph_ql_db.py,sha256=
|
|
18
|
-
platzky/db/json_db.py,sha256
|
|
17
|
+
platzky/db/graph_ql_db.py,sha256=af6yy1R27YO8N9zJWU7VgU7optRgpdk_1ZUtab_1eT4,8967
|
|
18
|
+
platzky/db/json_db.py,sha256=NUBPy4jt-y37TYq4SCGaSgief3MbBWL_Efw8Bxp8Jo0,4046
|
|
19
19
|
platzky/db/json_file_db.py,sha256=tPo92n5zG7vGpunn5vl66zISHBziQdxBttitvc5hPug,1030
|
|
20
|
-
platzky/db/mongodb_db.py,sha256=
|
|
21
|
-
platzky/engine.py,sha256=
|
|
20
|
+
platzky/db/mongodb_db.py,sha256=28KO8XmTEiqE7FcNBzw_pfxOy6Vo-T7qsHdUlh59QX0,5174
|
|
21
|
+
platzky/engine.py,sha256=0bxKzfK83ic-VNlKcDut_84_u5EmY2baU5JcJsleUoM,5461
|
|
22
22
|
platzky/locale/en/LC_MESSAGES/messages.po,sha256=WaZGlFAegKRq7CSz69dWKic-mKvQFhVvssvExxNmGaU,1400
|
|
23
23
|
platzky/locale/pl/LC_MESSAGES/messages.po,sha256=sUPxMKDeEOoZ5UIg94rGxZD06YVWiAMWIby2XE51Hrc,1624
|
|
24
24
|
platzky/models.py,sha256=DZZgKW2Q3fY2GMdikFUmAgpsRqT5VKAOwP6RmEsmO2M,1871
|
|
25
|
-
platzky/platzky.py,sha256=
|
|
25
|
+
platzky/platzky.py,sha256=8mTqdYqRKONv2oGvQF5Y0fuOomH7ZFYvXtxc7ZohBLE,4585
|
|
26
26
|
platzky/plugin/plugin.py,sha256=tV8aobIzMDJe1frKUAi4kLbrTAIS0FWE3oYpktSo6Ug,1633
|
|
27
27
|
platzky/plugin/plugin_loader.py,sha256=MeQ8LNbrOomwXgc1ISHuyhjZd2mzYKen70eDShWs-Co,3497
|
|
28
28
|
platzky/seo/seo.py,sha256=N_MmAA4KJZmmrDUh0hYNtD8ycOwpNKow4gVSAv8V3N4,2631
|
|
29
29
|
platzky/static/blog.css,sha256=TrppzgQbj4UtuTufDCdblyNTVAqgIbhD66Cziyv_xnY,7893
|
|
30
30
|
platzky/static/styles.css,sha256=U5ddGIK-VcGRJZ3BdOpMp0pR__k6rNEMsuQXkP4tFQ0,686
|
|
31
|
+
platzky/telemetry.py,sha256=NQuueK-uwZDdnpWaOTFSMvzRWWg6Yu4jPOMPYdMidNY,4524
|
|
31
32
|
platzky/templates/404.html,sha256=EheoLSWylOscLH8FmcMA4c6Jw14i5HkSvE_GXzGIrUo,78
|
|
32
33
|
platzky/templates/base.html,sha256=clvWlVOxNLqSQxBpPao3qnKKzkU2q48Apf1WbHJgYfE,4003
|
|
33
34
|
platzky/templates/blog.html,sha256=aPl-DzLX85bHv7tN8UjlABR086PUJ9IGlGbIBioFHGA,1281
|
|
@@ -40,6 +41,7 @@ platzky/templates/post.html,sha256=GSgjIZsOQKtNx3cEbquSjZ5L4whPnG6MzRyoq9k4B8Q,1
|
|
|
40
41
|
platzky/templates/robots.txt,sha256=2_j2tiYtYJnzZUrANiX9pvBxyw5Dp27fR_co18BPEJ0,116
|
|
41
42
|
platzky/templates/sitemap.xml,sha256=iIJZ91_B5ZuNLCHsRtsGKZlBAXojOTP8kffqKLacgvs,578
|
|
42
43
|
platzky/www_handler.py,sha256=pF6Rmvem1sdVqHD7z3RLrDuG-CwAqfGCti50_NPsB2w,725
|
|
43
|
-
platzky-0.
|
|
44
|
-
platzky-0.
|
|
45
|
-
platzky-0.
|
|
44
|
+
platzky-1.0.0.dist-info/METADATA,sha256=NSQ3OIyrJ4NQsdu3QRIo-c4jQQ9mM0HoqwmQK2EPvKc,2463
|
|
45
|
+
platzky-1.0.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
46
|
+
platzky-1.0.0.dist-info/licenses/LICENSE,sha256=wCdfk-qEosi6BDwiBulMfKMi0hxp1UXV0DdjLrRm788,1077
|
|
47
|
+
platzky-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Platzky Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
File without changes
|