2828from app .db .models import PasteEntity
2929from app .services .cleanup_service import CleanupService
3030from app .storage import StorageClient
31+ from app .utils .active_pastes_counter import get_active_pastes_counter
32+ from app .utils .metrics import (
33+ compressed_pastes ,
34+ paste_operations ,
35+ paste_size ,
36+ storage_operations ,
37+ )
3138from app .utils .token_utils import hash_token , is_token_hashed , verify_token
3239
3340
@@ -44,6 +51,18 @@ def __init__(
4451 self ._cleanup_task : asyncio .Task [Coroutine [None , None , None ]] | None = None
4552 self ._lock_file : Path = Path (".cleanup.lock" )
4653 self ._cleanup_service : CleanupService = cleanup_service
54+ self ._storage_backend_name : str = self ._get_storage_backend_name ()
55+
56+ def _get_storage_backend_name (self ) -> str :
57+ """Get storage backend name for metrics labels."""
58+ class_name = self .storage_client .__class__ .__name__
59+ if "Local" in class_name :
60+ return "local"
61+ elif "S3" in class_name :
62+ return "s3"
63+ elif "Minio" in class_name or "MinIO" in class_name :
64+ return "minio"
65+ return "unknown"
4766
4867 async def _read_content (self , paste_path : str , is_compressed : bool = False ) -> str | None :
4968 """
@@ -60,8 +79,15 @@ async def _read_content(self, paste_path: str, is_compressed: bool = False) -> s
6079 data = await self .storage_client .get_object (paste_path )
6180 if data is None :
6281 self .logger .error ("Paste content not found: %s" , paste_path )
82+ storage_operations .labels (
83+ operation = "get" , backend = self ._storage_backend_name , status = "not_found"
84+ ).inc ()
6385 return None
6486
87+ storage_operations .labels (
88+ operation = "get" , backend = self ._storage_backend_name , status = "success"
89+ ).inc ()
90+
6591 if is_compressed :
6692 from app .utils .compression import CompressionError , decompress_content
6793
@@ -74,6 +100,9 @@ async def _read_content(self, paste_path: str, is_compressed: bool = False) -> s
74100 return data .decode ("utf-8" )
75101 except Exception as exc :
76102 self .logger .error ("Failed to read paste content: %s" , exc )
103+ storage_operations .labels (
104+ operation = "get" , backend = self ._storage_backend_name , status = "error"
105+ ).inc ()
77106 return None
78107
79108 async def _save_content (self , paste_id : str , content : str ) -> tuple [str , int , bool , int | None ] | None :
@@ -126,23 +155,38 @@ async def _save_content(self, paste_id: str, content: str) -> tuple[str, int, bo
126155 # Write content (compressed or uncompressed)
127156 if use_compression and compressed_data :
128157 await self .storage_client .put_object (storage_key , compressed_data )
158+ storage_operations .labels (
159+ operation = "put" , backend = self ._storage_backend_name , status = "success"
160+ ).inc ()
129161 content_size = len (compressed_data )
130162 return storage_key , content_size , True , original_size
131163 else :
132164 await self .storage_client .put_object (storage_key , content .encode ("utf-8" ))
165+ storage_operations .labels (
166+ operation = "put" , backend = self ._storage_backend_name , status = "success"
167+ ).inc ()
133168 content_size = original_size
134169 return storage_key , content_size , False , None
135170
136171 except Exception as exc :
137172 self .logger .error ("Failed to save paste content: %s" , exc )
173+ storage_operations .labels (
174+ operation = "put" , backend = self ._storage_backend_name , status = "error"
175+ ).inc ()
138176 return None
139177
140178 async def _remove_file (self , storage_key : str ):
141179 """Remove paste file from storage."""
142180 try :
143181 await self .storage_client .delete_object (storage_key )
182+ storage_operations .labels (
183+ operation = "delete" , backend = self ._storage_backend_name , status = "success"
184+ ).inc ()
144185 except Exception as exc :
145186 self .logger .error ("Failed to remove file %s: %s" , storage_key , exc )
187+ storage_operations .labels (
188+ operation = "delete" , backend = self ._storage_backend_name , status = "error"
189+ ).inc ()
146190
147191 def verify_storage_limit (self ):
148192 """Verify storage limit (only applicable for local storage)."""
@@ -202,11 +246,13 @@ async def get_paste_by_id(self, paste_id: UUID4) -> PasteResponse | None:
202246 )
203247 result : PasteEntity | None = (await session .execute (stmt )).scalar_one_or_none ()
204248 if result is None :
249+ paste_operations .labels (operation = "get" , status = "not_found" ).inc ()
205250 return None
206251 content = await self ._read_content (
207252 result .content_path ,
208253 is_compressed = result .is_compressed ,
209254 )
255+ paste_operations .labels (operation = "get" , status = "success" ).inc ()
210256 return PasteResponse (
211257 id = result .id ,
212258 title = result .title ,
@@ -233,6 +279,7 @@ async def edit_paste(self, paste_id: UUID4, edit_paste: EditPaste, edit_token: s
233279 result : PasteEntity | None = (await session .execute (stmt )).scalar_one_or_none ()
234280
235281 if result is None :
282+ paste_operations .labels (operation = "edit" , status = "not_found" ).inc ()
236283 return None
237284
238285 # Verify token - support both hashed (new) and plaintext (legacy)
@@ -249,6 +296,7 @@ async def edit_paste(self, paste_id: UUID4, edit_paste: EditPaste, edit_token: s
249296 self .logger .info ("Upgraded edit token to hashed format for paste %s" , paste_id )
250297
251298 if not token_valid :
299+ paste_operations .labels (operation = "edit" , status = "unauthorized" ).inc ()
252300 return None
253301
254302 # Update only the fields that are provided (not None)
@@ -290,6 +338,7 @@ async def edit_paste(self, paste_id: UUID4, edit_paste: EditPaste, edit_token: s
290338 )
291339 )
292340
341+ paste_operations .labels (operation = "edit" , status = "success" ).inc ()
293342 return PasteResponse (
294343 id = result .id ,
295344 title = result .title ,
@@ -316,6 +365,7 @@ async def delete_paste(self, paste_id: UUID4, delete_token: str) -> bool:
316365 result : PasteEntity | None = (await session .execute (stmt )).scalar_one_or_none ()
317366
318367 if result is None :
368+ paste_operations .labels (operation = "delete" , status = "not_found" ).inc ()
319369 return False
320370
321371 # Verify token - support both hashed (new) and plaintext (legacy)
@@ -329,6 +379,7 @@ async def delete_paste(self, paste_id: UUID4, delete_token: str) -> bool:
329379 # No need to upgrade here since we're deleting anyway
330380
331381 if not token_valid :
382+ paste_operations .labels (operation = "delete" , status = "unauthorized" ).inc ()
332383 return False
333384
334385 # Remove file
@@ -340,10 +391,15 @@ async def delete_paste(self, paste_id: UUID4, delete_token: str) -> bool:
340391 # Delete from database
341392 await session .delete (result )
342393 await session .commit ()
394+ paste_operations .labels (operation = "delete" , status = "success" ).inc ()
395+ counter = get_active_pastes_counter ()
396+ if counter :
397+ counter .dec ()
343398 return True
344399
345400 async def create_paste (self , paste : CreatePaste , user_data : UserMetaData ) -> PasteResponse :
346401 if not self .verify_storage_limit ():
402+ paste_operations .labels (operation = "create" , status = "storage_limit" ).inc ()
347403 raise HTTPException (
348404 status_code = 500 ,
349405 detail = "Storage limit reached, contact administration" ,
@@ -355,6 +411,7 @@ async def create_paste(self, paste: CreatePaste, user_data: UserMetaData) -> Pas
355411 paste .content ,
356412 )
357413 if not save_result :
414+ paste_operations .labels (operation = "create" , status = "error" ).inc ()
358415 raise HTTPException (
359416 status_code = 500 ,
360417 detail = "Failed to save paste content" ,
@@ -391,6 +448,15 @@ async def create_paste(self, paste: CreatePaste, user_data: UserMetaData) -> Pas
391448 await session .commit ()
392449 await session .refresh (entity )
393450
451+ # Record metrics on successful create
452+ paste_operations .labels (operation = "create" , status = "success" ).inc ()
453+ paste_size .observe (original_size if original_size else content_size )
454+ counter = get_active_pastes_counter ()
455+ if counter :
456+ counter .inc ()
457+ if is_compressed :
458+ compressed_pastes .inc ()
459+
394460 return CreatePasteResponse (
395461 id = entity .id ,
396462 title = entity .title ,
@@ -405,6 +471,7 @@ async def create_paste(self, paste: CreatePaste, user_data: UserMetaData) -> Pas
405471 except Exception as exc :
406472 self .logger .error ("Failed to create paste: %s" , exc )
407473 await self ._remove_file (paste_path )
474+ paste_operations .labels (operation = "create" , status = "error" ).inc ()
408475 raise HTTPException (
409476 status_code = 500 ,
410477 detail = "Failed to create paste" ,
0 commit comments