|
1 | 1 | """ |
2 | | -The Telemetry service. |
| 2 | +The Telemetry service. Sends anonymous data to Prefect to help us improve. |
3 | 3 | """ |
4 | 4 |
|
5 | | -import asyncio |
| 5 | +import logging |
6 | 6 | import os |
7 | 7 | import platform |
8 | | -from typing import Any, Optional |
| 8 | +from datetime import timedelta |
9 | 9 | from uuid import uuid4 |
10 | 10 |
|
11 | 11 | import httpx |
| 12 | +from docket import Perpetual |
12 | 13 |
|
13 | 14 | import prefect |
14 | | -from prefect.server.database import PrefectDBInterface |
15 | | -from prefect.server.database.dependencies import db_injector |
| 15 | +from prefect.logging import get_logger |
| 16 | +from prefect.server.database import PrefectDBInterface, provide_database_interface |
16 | 17 | from prefect.server.models import configuration |
17 | 18 | from prefect.server.schemas.core import Configuration |
18 | | -from prefect.server.services.base import ( |
19 | | - LoopService, |
20 | | - RunInEphemeralServers, |
21 | | - RunInWebservers, |
22 | | -) |
| 19 | +from prefect.server.services.perpetual_services import perpetual_service |
23 | 20 | from prefect.settings import PREFECT_DEBUG_MODE |
24 | 21 | from prefect.settings.context import get_current_settings |
25 | | -from prefect.settings.models.server.services import ServicesBaseSetting |
26 | 22 | from prefect.types._datetime import now |
27 | 23 |
|
| 24 | +logger: logging.Logger = get_logger(__name__) |
| 25 | + |
28 | 26 |
|
29 | | -class Telemetry(RunInEphemeralServers, RunInWebservers, LoopService): |
| 27 | +async def _fetch_or_set_telemetry_session( |
| 28 | + db: PrefectDBInterface, |
| 29 | +) -> tuple[str, str]: |
30 | 30 | """ |
31 | | - Sends anonymous data to Prefect to help us improve |
| 31 | + Fetch or create a telemetry session in the configuration table. |
32 | 32 |
|
33 | | - It can be toggled off with the PREFECT_SERVER_ANALYTICS_ENABLED setting. |
| 33 | + Returns: |
| 34 | + tuple of (session_start_timestamp, session_id) |
34 | 35 | """ |
| 36 | + async with db.session_context(begin_transaction=True) as session: |
| 37 | + telemetry_session = await configuration.read_configuration( |
| 38 | + session, "TELEMETRY_SESSION" |
| 39 | + ) |
35 | 40 |
|
36 | | - loop_seconds: float = 600 |
| 41 | + if telemetry_session is None: |
| 42 | + logger.debug("No telemetry session found, setting") |
| 43 | + session_id = str(uuid4()) |
| 44 | + session_start_timestamp = now("UTC").isoformat() |
| 45 | + |
| 46 | + telemetry_session = Configuration( |
| 47 | + key="TELEMETRY_SESSION", |
| 48 | + value={ |
| 49 | + "session_id": session_id, |
| 50 | + "session_start_timestamp": session_start_timestamp, |
| 51 | + }, |
| 52 | + ) |
37 | 53 |
|
38 | | - @classmethod |
39 | | - def service_settings(cls) -> ServicesBaseSetting: |
40 | | - raise NotImplementedError("Telemetry service does not have settings") |
| 54 | + await configuration.write_configuration(session, telemetry_session) |
| 55 | + else: |
| 56 | + logger.debug("Session information retrieved from database") |
| 57 | + session_id = telemetry_session.value["session_id"] |
| 58 | + session_start_timestamp = telemetry_session.value["session_start_timestamp"] |
41 | 59 |
|
42 | | - @classmethod |
43 | | - def environment_variable_name(cls) -> str: |
44 | | - return "PREFECT_SERVER_ANALYTICS_ENABLED" |
| 60 | + logger.debug(f"Telemetry Session: {session_id}, {session_start_timestamp}") |
| 61 | + return (session_start_timestamp, session_id) |
45 | 62 |
|
46 | | - @classmethod |
47 | | - def enabled(cls) -> bool: |
48 | | - return get_current_settings().server.analytics_enabled |
49 | 63 |
|
50 | | - def __init__(self, loop_seconds: Optional[int] = None, **kwargs: Any): |
51 | | - super().__init__(loop_seconds=loop_seconds, **kwargs) |
52 | | - self.telemetry_environment: str = os.environ.get( |
53 | | - "PREFECT_API_TELEMETRY_ENVIRONMENT", "production" |
54 | | - ) |
| 64 | +@perpetual_service( |
| 65 | + enabled_getter=lambda: get_current_settings().server.analytics_enabled, |
| 66 | + run_in_ephemeral=True, |
| 67 | + run_in_webserver=True, |
| 68 | +) |
| 69 | +async def send_telemetry_heartbeat( |
| 70 | + perpetual: Perpetual = Perpetual(automatic=True, every=timedelta(seconds=600)), |
| 71 | +) -> None: |
| 72 | + """ |
| 73 | + Sends anonymous telemetry data to Prefect to help us improve. |
55 | 74 |
|
56 | | - @db_injector |
57 | | - async def _fetch_or_set_telemetry_session(self, db: PrefectDBInterface): |
58 | | - """ |
59 | | - This method looks for a telemetry session in the configuration table. If there |
60 | | - isn't one, it sets one. It then sets `self.session_id` and |
61 | | - `self.session_start_timestamp`. |
62 | | -
|
63 | | - Telemetry sessions last until the database is reset. |
64 | | - """ |
65 | | - async with db.session_context(begin_transaction=True) as session: |
66 | | - telemetry_session = await configuration.read_configuration( |
67 | | - session, "TELEMETRY_SESSION" |
| 75 | + It can be toggled off with the PREFECT_SERVER_ANALYTICS_ENABLED setting. |
| 76 | + """ |
| 77 | + from prefect.client.constants import SERVER_API_VERSION |
| 78 | + |
| 79 | + db = provide_database_interface() |
| 80 | + session_start_timestamp, session_id = await _fetch_or_set_telemetry_session(db=db) |
| 81 | + telemetry_environment = os.environ.get( |
| 82 | + "PREFECT_API_TELEMETRY_ENVIRONMENT", "production" |
| 83 | + ) |
| 84 | + |
| 85 | + heartbeat = { |
| 86 | + "source": "prefect_server", |
| 87 | + "type": "heartbeat", |
| 88 | + "payload": { |
| 89 | + "platform": platform.system(), |
| 90 | + "architecture": platform.machine(), |
| 91 | + "python_version": platform.python_version(), |
| 92 | + "python_implementation": platform.python_implementation(), |
| 93 | + "environment": telemetry_environment, |
| 94 | + "ephemeral_server": bool(os.getenv("PREFECT__SERVER_EPHEMERAL", False)), |
| 95 | + "api_version": SERVER_API_VERSION, |
| 96 | + "prefect_version": prefect.__version__, |
| 97 | + "session_id": session_id, |
| 98 | + "session_start_timestamp": session_start_timestamp, |
| 99 | + }, |
| 100 | + } |
| 101 | + |
| 102 | + try: |
| 103 | + async with httpx.AsyncClient() as client: |
| 104 | + result = await client.post( |
| 105 | + "https://sens-o-matic.prefect.io/", |
| 106 | + json=heartbeat, |
| 107 | + headers={"x-prefect-event": "prefect_server"}, |
68 | 108 | ) |
69 | | - |
70 | | - if telemetry_session is None: |
71 | | - self.logger.debug("No telemetry session found, setting") |
72 | | - session_id = str(uuid4()) |
73 | | - session_start_timestamp = now("UTC").isoformat() |
74 | | - |
75 | | - telemetry_session = Configuration( |
76 | | - key="TELEMETRY_SESSION", |
77 | | - value={ |
78 | | - "session_id": session_id, |
79 | | - "session_start_timestamp": session_start_timestamp, |
80 | | - }, |
81 | | - ) |
82 | | - |
83 | | - await configuration.write_configuration(session, telemetry_session) |
84 | | - |
85 | | - self.session_id = session_id |
86 | | - self.session_start_timestamp = session_start_timestamp |
87 | | - else: |
88 | | - self.logger.debug("Session information retrieved from database") |
89 | | - self.session_id: str = telemetry_session.value["session_id"] |
90 | | - self.session_start_timestamp: str = telemetry_session.value[ |
91 | | - "session_start_timestamp" |
92 | | - ] |
93 | | - self.logger.debug( |
94 | | - f"Telemetry Session: {self.session_id}, {self.session_start_timestamp}" |
| 109 | + result.raise_for_status() |
| 110 | + except Exception as exc: |
| 111 | + logger.error( |
| 112 | + f"Failed to send telemetry: {exc}", |
| 113 | + exc_info=PREFECT_DEBUG_MODE.value(), |
95 | 114 | ) |
96 | | - return (self.session_start_timestamp, self.session_id) |
97 | | - |
98 | | - async def run_once(self) -> None: |
99 | | - """ |
100 | | - Sends a heartbeat to the sens-o-matic |
101 | | - """ |
102 | | - from prefect.client.constants import SERVER_API_VERSION |
103 | | - |
104 | | - if not hasattr(self, "session_id"): |
105 | | - await self._fetch_or_set_telemetry_session() |
106 | | - |
107 | | - heartbeat = { |
108 | | - "source": "prefect_server", |
109 | | - "type": "heartbeat", |
110 | | - "payload": { |
111 | | - "platform": platform.system(), |
112 | | - "architecture": platform.machine(), |
113 | | - "python_version": platform.python_version(), |
114 | | - "python_implementation": platform.python_implementation(), |
115 | | - "environment": self.telemetry_environment, |
116 | | - "ephemeral_server": bool(os.getenv("PREFECT__SERVER_EPHEMERAL", False)), |
117 | | - "api_version": SERVER_API_VERSION, |
118 | | - "prefect_version": prefect.__version__, |
119 | | - "session_id": self.session_id, |
120 | | - "session_start_timestamp": self.session_start_timestamp, |
121 | | - }, |
122 | | - } |
123 | | - |
124 | | - try: |
125 | | - async with httpx.AsyncClient() as client: |
126 | | - result = await client.post( |
127 | | - "https://sens-o-matic.prefect.io/", |
128 | | - json=heartbeat, |
129 | | - headers={"x-prefect-event": "prefect_server"}, |
130 | | - ) |
131 | | - result.raise_for_status() |
132 | | - except Exception as exc: |
133 | | - self.logger.error( |
134 | | - f"Failed to send telemetry: {exc}\nShutting down telemetry service...", |
135 | | - # The traceback is only needed if doing deeper debugging, otherwise |
136 | | - # this looks like an impactful server error |
137 | | - exc_info=PREFECT_DEBUG_MODE.value(), |
138 | | - ) |
139 | | - await self.stop(block=False) |
140 | | - |
141 | | - |
142 | | -if __name__ == "__main__": |
143 | | - asyncio.run(Telemetry(handle_signals=True).start()) |
0 commit comments