e_lib.product_log
Python client for the Products REST API (application usage statistics).
Mirrors the Java ru.gentoos.ProductsLog library. Reads
PRODUCTS_HOST, PRODUCTS_USER, PRODUCTS_PASS from the
environment (or explicit constructor arguments) and provides helpers
to register a product, post status messages and log application start /
shutdown events.
If any required credential is missing the client disables itself silently (logs a warning, all public methods become no-ops) so that development environments without the service never crash the application boot.
Example::
from e_lib.product_log import ProductsLog
log = ProductsLog("MyApp", "1.0.0")
log.post_started()
...
log.post_shutdown()
1# -*- coding: utf-8 -*- 2"""e_lib.product_log 3 4Python client for the Products REST API (application usage statistics). 5 6Mirrors the Java ``ru.gentoos.ProductsLog`` library. Reads 7``PRODUCTS_HOST``, ``PRODUCTS_USER``, ``PRODUCTS_PASS`` from the 8environment (or explicit constructor arguments) and provides helpers 9to register a product, post status messages and log application start / 10shutdown events. 11 12If any required credential is missing the client disables itself 13silently (logs a warning, all public methods become no-ops) so that 14development environments without the service never crash the application 15boot. 16 17Example:: 18 19 from e_lib.product_log import ProductsLog 20 21 log = ProductsLog("MyApp", "1.0.0") 22 log.post_started() 23 ... 24 log.post_shutdown() 25""" 26 27from __future__ import annotations 28 29import logging 30import os 31from typing import Any, Dict, Optional 32from urllib.parse import quote 33 34import requests 35 36 37class ProductsLog: 38 """Client for the Products REST API. 39 40 Parameters 41 ---------- 42 product_name: 43 Logical name of the product (must be unique in Products). 44 product_version: 45 Version string attached to every stat record. 46 base_url: 47 Base URL of the Products service (``PRODUCTS_HOST`` env fallback). 48 username: 49 Basic-auth username (``PRODUCTS_USER`` env fallback). 50 password: 51 Basic-auth password (``PRODUCTS_PASS`` env fallback). 52 timeout: 53 HTTP timeout in seconds for every request. 54 logger: 55 Logger for messages. Projects may inject their own (e.g. a 56 loguru logger) so that all output goes through a single sink. 57 """ 58 59 def __init__( 60 self, 61 product_name: str, 62 product_version: str = "UNKNOWN", 63 base_url: Optional[str] = None, 64 username: Optional[str] = None, 65 password: Optional[str] = None, 66 timeout: float = 5.0, 67 logger: Optional[Any] = None, 68 ) -> None: 69 self.product_name = product_name 70 self.product_version = product_version 71 self.timeout = timeout 72 self._logger = logger or logging.getLogger(__name__) 73 74 self.base_url = (base_url or os.getenv("PRODUCTS_HOST", "")).rstrip("/") 75 self.username = username or os.getenv("PRODUCTS_USER", "") 76 self.password = password or os.getenv("PRODUCTS_PASS", "") 77 78 self._enabled = bool(self.base_url and self.username and self.password) 79 if not self._enabled: 80 self._logger.warning( 81 "ProductsLog: PRODUCTS_HOST / PRODUCTS_USER / PRODUCTS_PASS " 82 "are not all set — statistics reporting is disabled." 83 ) 84 return 85 86 self._auth = (self.username, self.password) 87 self._product_id: Optional[int] = None 88 self._product_found = False 89 90 # ------------------------------------------------------------------ 91 # Internal helpers 92 # ------------------------------------------------------------------ 93 94 def _ensure_product(self) -> Optional[int]: 95 """Return the product id, fetching or registering as needed.""" 96 if self._product_id is not None: 97 return self._product_id 98 99 self._product_id = self._find_product_id() 100 if self._product_id is None: 101 self._product_id = self._register_product_id() 102 return self._product_id 103 104 def _find_product_id(self) -> Optional[int]: 105 """GET /products/byName/{name}. Returns the id or None.""" 106 url = f"{self.base_url}/products/byName/{quote(self.product_name, safe='')}" 107 try: 108 resp = requests.get(url, auth=self._auth, timeout=self.timeout) 109 if resp.status_code == 200: 110 return resp.json().get("id") 111 return None 112 except requests.RequestException as exc: 113 self._logger.warning( 114 "ProductsLog: failed to look up product %r: %s", 115 self.product_name, 116 exc, 117 ) 118 return None 119 120 def _register_product_id(self) -> Optional[int]: 121 """POST /products — register a new product. Returns the new id.""" 122 url = f"{self.base_url}/products" 123 try: 124 resp = requests.post( 125 url, 126 json={"name": self.product_name}, 127 auth=self._auth, 128 timeout=self.timeout, 129 ) 130 if resp.status_code in (200, 201): 131 return resp.json().get("id") 132 self._logger.warning( 133 "ProductsLog: failed to register product %r (HTTP %d)", 134 self.product_name, 135 resp.status_code, 136 ) 137 return None 138 except requests.RequestException as exc: 139 self._logger.warning( 140 "ProductsLog: failed to register product %r: %s", 141 self.product_name, 142 exc, 143 ) 144 return None 145 146 # ------------------------------------------------------------------ 147 # Public API 148 # ------------------------------------------------------------------ 149 150 def post_message(self, comment: str) -> Optional[Dict[str, Any]]: 151 """Post a stat record to the Products API. 152 153 Parameters 154 ---------- 155 comment: 156 Free-text message (e.g. ``"Program started"``). 157 158 Returns 159 ------- 160 The parsed JSON response body (``{"id": ..., "product": ..., 161 "version": ..., "comment": ..., "created": ...}``) or ``None`` 162 on failure. 163 """ 164 if not self._enabled: 165 return None 166 167 product_id = self._ensure_product() 168 if product_id is None: 169 return None 170 171 payload = { 172 "product": {"id": product_id}, 173 "version": self.product_version, 174 "comment": comment, 175 } 176 url = f"{self.base_url}/stats" 177 try: 178 resp = requests.post( 179 url, 180 json=payload, 181 auth=self._auth, 182 timeout=self.timeout, 183 ) 184 if resp.status_code in (200, 201): 185 return resp.json() 186 self._logger.warning( 187 "ProductsLog: post_message failed (HTTP %d): %s", 188 resp.status_code, 189 resp.text, 190 ) 191 return None 192 except requests.RequestException as exc: 193 self._logger.warning("ProductsLog: post_message failed: %s", exc) 194 return None 195 196 def post_started(self) -> Optional[Dict[str, Any]]: 197 """Convenience — posts ``"Program started"``.""" 198 return self.post_message("Program started") 199 200 def post_shutdown(self) -> Optional[Dict[str, Any]]: 201 """Convenience — posts ``"Program shutdown"``.""" 202 return self.post_message("Program shutdown")
38class ProductsLog: 39 """Client for the Products REST API. 40 41 Parameters 42 ---------- 43 product_name: 44 Logical name of the product (must be unique in Products). 45 product_version: 46 Version string attached to every stat record. 47 base_url: 48 Base URL of the Products service (``PRODUCTS_HOST`` env fallback). 49 username: 50 Basic-auth username (``PRODUCTS_USER`` env fallback). 51 password: 52 Basic-auth password (``PRODUCTS_PASS`` env fallback). 53 timeout: 54 HTTP timeout in seconds for every request. 55 logger: 56 Logger for messages. Projects may inject their own (e.g. a 57 loguru logger) so that all output goes through a single sink. 58 """ 59 60 def __init__( 61 self, 62 product_name: str, 63 product_version: str = "UNKNOWN", 64 base_url: Optional[str] = None, 65 username: Optional[str] = None, 66 password: Optional[str] = None, 67 timeout: float = 5.0, 68 logger: Optional[Any] = None, 69 ) -> None: 70 self.product_name = product_name 71 self.product_version = product_version 72 self.timeout = timeout 73 self._logger = logger or logging.getLogger(__name__) 74 75 self.base_url = (base_url or os.getenv("PRODUCTS_HOST", "")).rstrip("/") 76 self.username = username or os.getenv("PRODUCTS_USER", "") 77 self.password = password or os.getenv("PRODUCTS_PASS", "") 78 79 self._enabled = bool(self.base_url and self.username and self.password) 80 if not self._enabled: 81 self._logger.warning( 82 "ProductsLog: PRODUCTS_HOST / PRODUCTS_USER / PRODUCTS_PASS " 83 "are not all set — statistics reporting is disabled." 84 ) 85 return 86 87 self._auth = (self.username, self.password) 88 self._product_id: Optional[int] = None 89 self._product_found = False 90 91 # ------------------------------------------------------------------ 92 # Internal helpers 93 # ------------------------------------------------------------------ 94 95 def _ensure_product(self) -> Optional[int]: 96 """Return the product id, fetching or registering as needed.""" 97 if self._product_id is not None: 98 return self._product_id 99 100 self._product_id = self._find_product_id() 101 if self._product_id is None: 102 self._product_id = self._register_product_id() 103 return self._product_id 104 105 def _find_product_id(self) -> Optional[int]: 106 """GET /products/byName/{name}. Returns the id or None.""" 107 url = f"{self.base_url}/products/byName/{quote(self.product_name, safe='')}" 108 try: 109 resp = requests.get(url, auth=self._auth, timeout=self.timeout) 110 if resp.status_code == 200: 111 return resp.json().get("id") 112 return None 113 except requests.RequestException as exc: 114 self._logger.warning( 115 "ProductsLog: failed to look up product %r: %s", 116 self.product_name, 117 exc, 118 ) 119 return None 120 121 def _register_product_id(self) -> Optional[int]: 122 """POST /products — register a new product. Returns the new id.""" 123 url = f"{self.base_url}/products" 124 try: 125 resp = requests.post( 126 url, 127 json={"name": self.product_name}, 128 auth=self._auth, 129 timeout=self.timeout, 130 ) 131 if resp.status_code in (200, 201): 132 return resp.json().get("id") 133 self._logger.warning( 134 "ProductsLog: failed to register product %r (HTTP %d)", 135 self.product_name, 136 resp.status_code, 137 ) 138 return None 139 except requests.RequestException as exc: 140 self._logger.warning( 141 "ProductsLog: failed to register product %r: %s", 142 self.product_name, 143 exc, 144 ) 145 return None 146 147 # ------------------------------------------------------------------ 148 # Public API 149 # ------------------------------------------------------------------ 150 151 def post_message(self, comment: str) -> Optional[Dict[str, Any]]: 152 """Post a stat record to the Products API. 153 154 Parameters 155 ---------- 156 comment: 157 Free-text message (e.g. ``"Program started"``). 158 159 Returns 160 ------- 161 The parsed JSON response body (``{"id": ..., "product": ..., 162 "version": ..., "comment": ..., "created": ...}``) or ``None`` 163 on failure. 164 """ 165 if not self._enabled: 166 return None 167 168 product_id = self._ensure_product() 169 if product_id is None: 170 return None 171 172 payload = { 173 "product": {"id": product_id}, 174 "version": self.product_version, 175 "comment": comment, 176 } 177 url = f"{self.base_url}/stats" 178 try: 179 resp = requests.post( 180 url, 181 json=payload, 182 auth=self._auth, 183 timeout=self.timeout, 184 ) 185 if resp.status_code in (200, 201): 186 return resp.json() 187 self._logger.warning( 188 "ProductsLog: post_message failed (HTTP %d): %s", 189 resp.status_code, 190 resp.text, 191 ) 192 return None 193 except requests.RequestException as exc: 194 self._logger.warning("ProductsLog: post_message failed: %s", exc) 195 return None 196 197 def post_started(self) -> Optional[Dict[str, Any]]: 198 """Convenience — posts ``"Program started"``.""" 199 return self.post_message("Program started") 200 201 def post_shutdown(self) -> Optional[Dict[str, Any]]: 202 """Convenience — posts ``"Program shutdown"``.""" 203 return self.post_message("Program shutdown")
Client for the Products REST API.
Parameters
product_name:
Logical name of the product (must be unique in Products).
product_version:
Version string attached to every stat record.
base_url:
Base URL of the Products service (PRODUCTS_HOST env fallback).
username:
Basic-auth username (PRODUCTS_USER env fallback).
password:
Basic-auth password (PRODUCTS_PASS env fallback).
timeout:
HTTP timeout in seconds for every request.
logger:
Logger for messages. Projects may inject their own (e.g. a
loguru logger) so that all output goes through a single sink.
60 def __init__( 61 self, 62 product_name: str, 63 product_version: str = "UNKNOWN", 64 base_url: Optional[str] = None, 65 username: Optional[str] = None, 66 password: Optional[str] = None, 67 timeout: float = 5.0, 68 logger: Optional[Any] = None, 69 ) -> None: 70 self.product_name = product_name 71 self.product_version = product_version 72 self.timeout = timeout 73 self._logger = logger or logging.getLogger(__name__) 74 75 self.base_url = (base_url or os.getenv("PRODUCTS_HOST", "")).rstrip("/") 76 self.username = username or os.getenv("PRODUCTS_USER", "") 77 self.password = password or os.getenv("PRODUCTS_PASS", "") 78 79 self._enabled = bool(self.base_url and self.username and self.password) 80 if not self._enabled: 81 self._logger.warning( 82 "ProductsLog: PRODUCTS_HOST / PRODUCTS_USER / PRODUCTS_PASS " 83 "are not all set — statistics reporting is disabled." 84 ) 85 return 86 87 self._auth = (self.username, self.password) 88 self._product_id: Optional[int] = None 89 self._product_found = False
151 def post_message(self, comment: str) -> Optional[Dict[str, Any]]: 152 """Post a stat record to the Products API. 153 154 Parameters 155 ---------- 156 comment: 157 Free-text message (e.g. ``"Program started"``). 158 159 Returns 160 ------- 161 The parsed JSON response body (``{"id": ..., "product": ..., 162 "version": ..., "comment": ..., "created": ...}``) or ``None`` 163 on failure. 164 """ 165 if not self._enabled: 166 return None 167 168 product_id = self._ensure_product() 169 if product_id is None: 170 return None 171 172 payload = { 173 "product": {"id": product_id}, 174 "version": self.product_version, 175 "comment": comment, 176 } 177 url = f"{self.base_url}/stats" 178 try: 179 resp = requests.post( 180 url, 181 json=payload, 182 auth=self._auth, 183 timeout=self.timeout, 184 ) 185 if resp.status_code in (200, 201): 186 return resp.json() 187 self._logger.warning( 188 "ProductsLog: post_message failed (HTTP %d): %s", 189 resp.status_code, 190 resp.text, 191 ) 192 return None 193 except requests.RequestException as exc: 194 self._logger.warning("ProductsLog: post_message failed: %s", exc) 195 return None
Post a stat record to the Products API.
Parameters
comment:
Free-text message (e.g. "Program started").
Returns
The parsed JSON response body ({"id": ..., "product": ...,
"version": ..., "comment": ..., "created": ...}) or None
on failure.