Skip to content

Commit bf54263

Browse files
authored
Merge pull request #54 from waldronlab/issue/46-api-key-masking
Implement API key masking in logs and error messages
2 parents 22233dc + 5f2ff5c commit bf54263

File tree

15 files changed

+546
-49
lines changed

15 files changed

+546
-49
lines changed

app/api/app.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,16 +110,22 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
110110
@app.exception_handler(Exception)
111111
async def global_exception_handler(request: Request, exc: Exception):
112112
"""Handle unexpected exceptions."""
113+
from app.utils.credential_masking import mask_exception_message, mask_string
114+
115+
# Mask any credentials in exception message and traceback
116+
safe_exc_msg = mask_exception_message(exc)
117+
safe_traceback = mask_string(traceback.format_exc())
118+
113119
logger.error(
114-
f"Unhandled exception: {str(exc)}\n"
115-
f"Traceback: {traceback.format_exc()}\n"
120+
f"Unhandled exception: {safe_exc_msg}\n"
121+
f"Traceback: {safe_traceback}\n"
116122
f"Request ID: {getattr(request.state, 'request_id', None)}"
117123
)
118124

119125
if ENVIRONMENT == "production":
120126
detail = "An internal error occurred. Please try again later."
121127
else:
122-
detail = str(exc)
128+
detail = safe_exc_msg
123129

124130
return JSONResponse(
125131
status_code=500,

app/api/routers/bugsigdb_analysis.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""API router for BugSigDB field analysis."""
22
from fastapi import APIRouter, HTTPException
33
import logging
4+
from app.utils.credential_masking import mask_exception_message
45

56
from app.services.bugsigdb_analyzer import analyze_paper_simple
67

@@ -92,7 +93,8 @@ async def analyze_paper(pmid: str):
9293
except HTTPException:
9394
raise
9495
except Exception as e:
95-
logger.error(f"Error in analysis for PMID {pmid}: {e}")
96+
safe_error = mask_exception_message(e)
97+
logger.error(f"Error in analysis for PMID {pmid}: {safe_error}")
9698
raise HTTPException(status_code=500, detail=f"Analysis error: {str(e)}")
9799

98100

@@ -104,7 +106,8 @@ async def analyze_paper_post(pmid: str):
104106
except HTTPException:
105107
raise
106108
except Exception as e:
107-
logger.error(f"Error in analysis for PMID {pmid}: {e}")
109+
safe_error = mask_exception_message(e)
110+
logger.error(f"Error in analysis for PMID {pmid}: {safe_error}")
108111
raise HTTPException(status_code=500, detail=f"Analysis error: {str(e)}")
109112

110113

app/api/routers/bugsigdb_analysis_v2.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Optional, List
44
import logging
55
from datetime import datetime
6+
from app.utils.credential_masking import mask_exception_message
67

78
from app.services.bugsigdb_analyzer import analyze_paper_simple, analyze_paper_with_rag
89
from app.api.models.api_models import (
@@ -140,7 +141,8 @@ async def analyze_paper(
140141
except HTTPException:
141142
raise
142143
except Exception as e:
143-
logger.error(f"Error in v2 analysis for PMID {pmid}: {e}")
144+
safe_error = mask_exception_message(e)
145+
logger.error(f"Error in v2 analysis for PMID {pmid}: {safe_error}")
144146
raise HTTPException(status_code=500, detail=f"Analysis error: {str(e)}")
145147

146148

@@ -183,7 +185,8 @@ async def analyze_paper_post(request: AnalysisRequestV2) -> PaperAnalysisResultV
183185
except HTTPException:
184186
raise
185187
except Exception as e:
186-
logger.error(f"Error in v2 POST analysis for PMID {request.pmid}: {e}")
188+
safe_error = mask_exception_message(e)
189+
logger.error(f"Error in v2 POST analysis for PMID {request.pmid}: {safe_error}")
187190
raise HTTPException(status_code=500, detail=f"Analysis error: {str(e)}")
188191

189192

@@ -222,7 +225,8 @@ async def analyze_with_semaphore(pmid: str):
222225
use_rag=request.use_rag
223226
)
224227
except Exception as e:
225-
logger.error(f"Error analyzing PMID {pmid} in batch: {e}")
228+
safe_error = mask_exception_message(e)
229+
logger.error(f"Error analyzing PMID {pmid} in batch: {safe_error}")
226230
return None
227231

228232
tasks = [analyze_with_semaphore(pmid) for pmid in request.pmids]
@@ -237,7 +241,8 @@ async def analyze_with_semaphore(pmid: str):
237241
return valid_results
238242

239243
except Exception as e:
240-
logger.error(f"Error in batch analysis: {e}")
244+
safe_error = mask_exception_message(e)
245+
logger.error(f"Error in batch analysis: {safe_error}")
241246
raise HTTPException(status_code=500, detail=f"Batch analysis error: {str(e)}")
242247

243248

@@ -262,7 +267,8 @@ async def get_rag_config() -> RAGConfigResponse:
262267
available_providers=available_providers or ["gemini", "openai", "anthropic"]
263268
)
264269
except Exception as e:
265-
logger.error(f"Error getting RAG config: {e}")
270+
safe_error = mask_exception_message(e)
271+
logger.error(f"Error getting RAG config: {safe_error}")
266272
raise HTTPException(status_code=500, detail=f"Error getting RAG config: {str(e)}")
267273

268274

app/api/routers/study_analysis.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Optional
66
from fastapi import APIRouter, HTTPException, BackgroundTasks
77
from pydantic import BaseModel, HttpUrl
8+
from app.utils.credential_masking import mask_exception_message
89

910
from app.services.web_scraper import WebScraperService
1011
from app.services.image_processor import ImageProcessorService
@@ -142,7 +143,8 @@ async def process_url_analysis(
142143
gemini_api_key=GEMINI_API_KEY
143144
)
144145
except Exception as e:
145-
logger.warning(f"Job {job_id}: Unable to initialize visual LLM: {e}")
146+
safe_error = mask_exception_message(e)
147+
logger.warning(f"Job {job_id}: Unable to initialize visual LLM: {safe_error}")
146148

147149
image_descriptions = []
148150

@@ -228,6 +230,7 @@ async def process_url_analysis(
228230
logger.info(f"Job {job_id}: Completed successfully")
229231

230232
except Exception as e:
231-
logger.error(f"Job {job_id}: Failed with error: {e}")
233+
safe_error = mask_exception_message(e)
234+
logger.error(f"Job {job_id}: Failed with error: {safe_error}")
232235
job_store[job_id].status = "failed"
233236
job_store[job_id].error = str(e)

app/api/routers/system.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import asyncio
77
from datetime import datetime
88
import pytz
9+
from app.utils.credential_masking import mask_exception_message
910

1011
from app.utils.config import (
1112
AVAILABLE_MODELS,
@@ -36,7 +37,8 @@ def get_unified_qa():
3637
try:
3738
_unified_qa = UnifiedQA(use_gemini=True, gemini_api_key=GEMINI_API_KEY)
3839
except Exception as e:
39-
logger.warning(f"Failed to initialize UnifiedQA: {e}")
40+
safe_error = mask_exception_message(e)
41+
logger.warning(f"Failed to initialize UnifiedQA: {safe_error}")
4042
_unified_qa = None
4143
return _unified_qa
4244

@@ -47,7 +49,8 @@ def get_pubmed_retriever():
4749
try:
4850
_pubmed_retriever = PubMedRetriever(api_key=NCBI_API_KEY)
4951
except Exception as e:
50-
logger.warning(f"Failed to initialize PubMedRetriever: {e}")
52+
safe_error = mask_exception_message(e)
53+
logger.warning(f"Failed to initialize PubMedRetriever: {safe_error}")
5154
_pubmed_retriever = None
5255
return _pubmed_retriever
5356

@@ -71,7 +74,8 @@ async def health_check():
7174
)
7275

7376
except Exception as e:
74-
logger.error(f"Error in health check: {e}")
77+
safe_error = mask_exception_message(e)
78+
logger.error(f"Error in health check: {safe_error}")
7579
return HealthResponse(
7680
status="unhealthy",
7781
timestamp=get_current_timestamp(),
@@ -102,7 +106,8 @@ async def get_config():
102106
)
103107

104108
except Exception as e:
105-
logger.error(f"Error getting config: {e}")
109+
safe_error = mask_exception_message(e)
110+
logger.error(f"Error getting config: {safe_error}")
106111
raise HTTPException(status_code=500, detail=f"Error getting configuration: {str(e)}")
107112

108113

@@ -152,7 +157,8 @@ async def gemini_health_check():
152157
}
153158

154159
except Exception as e:
155-
logger.error(f"Error in Gemini health check: {e}")
160+
safe_error = mask_exception_message(e)
161+
logger.error(f"Error in Gemini health check: {safe_error}")
156162
return {
157163
"status": "unhealthy",
158164
"api_key_configured": bool(GEMINI_API_KEY),
@@ -183,7 +189,8 @@ async def ncbi_health_check(pmid: str = "31452104"):
183189
"pmid": pmid
184190
}
185191
except Exception as e:
186-
logger.error(f"NCBI health check error: {e}", exc_info=True)
192+
safe_error = mask_exception_message(e)
193+
logger.error(f"NCBI health check error: {safe_error}", exc_info=False) # Don't log full traceback to avoid credential exposure
187194
return {
188195
"status": "unhealthy",
189196
"error": "An internal error occurred. Please try again later.",
@@ -234,7 +241,8 @@ async def get_metrics():
234241
)
235242

236243
except Exception as e:
237-
logger.error(f"Error getting metrics: {e}")
244+
safe_error = mask_exception_message(e)
245+
logger.error(f"Error getting metrics: {safe_error}")
238246
raise HTTPException(status_code=500, detail=f"Error getting metrics: {str(e)}")
239247

240248

@@ -274,7 +282,8 @@ async def get_system_status():
274282
}
275283

276284
except Exception as e:
277-
logger.error(f"Error getting system status: {e}")
285+
safe_error = mask_exception_message(e)
286+
logger.error(f"Error getting system status: {safe_error}")
278287
raise HTTPException(status_code=500, detail=f"Error getting system status: {str(e)}")
279288

280289

@@ -303,7 +312,8 @@ async def get_version():
303312
}
304313

305314
except Exception as e:
306-
logger.error(f"Error getting version: {e}")
315+
safe_error = mask_exception_message(e)
316+
logger.error(f"Error getting version: {safe_error}")
307317
raise HTTPException(status_code=500, detail=f"Error getting version: {str(e)}")
308318

309319

@@ -380,5 +390,6 @@ async def ask_question(request: Dict[str, Any]):
380390
except HTTPException:
381391
raise
382392
except Exception as e:
383-
logger.error(f"Error in Q&A endpoint: {e}")
393+
safe_error = mask_exception_message(e)
394+
logger.error(f"Error in Q&A endpoint: {safe_error}")
384395
raise HTTPException(status_code=500, detail=f"Error processing question: {str(e)}")

app/api/utils/api_utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ def get_paper_metadata_from_csv(pmid: str, csv_path: str = 'data/full_dump.csv')
135135
'publication_date': row.get('publication_date', '')
136136
}
137137
except Exception as e:
138-
logger.error(f"Error reading CSV metadata for PMID {pmid}: {e}")
138+
from app.utils.credential_masking import mask_exception_message
139+
safe_error = mask_exception_message(e)
140+
logger.error(f"Error reading CSV metadata for PMID {pmid}: {safe_error}")
139141

140142
return None
141143

app/models/gemini_qa.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import os
88
import json
99
from app.utils.config import GEMINI_TIMEOUT
10+
from app.utils.credential_masking import mask_string, mask_exception_message
1011
import asyncio
1112
import time
1213

@@ -592,7 +593,8 @@ async def analyze_paper_enhanced(self, prompt: str) -> Dict[str, Union[str, floa
592593
}
593594
}
594595
except Exception as e:
595-
error_msg = str(e)
596+
# Mask any credentials in error message before logging
597+
error_msg = mask_string(str(e))
596598
error_type = type(e).__name__
597599

598600
if "quota" in error_msg.lower() or "quota exceeded" in error_msg.lower():

app/models/llm_provider.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
from typing import Optional, Dict, List, Any
55
from enum import Enum
6+
from app.utils.credential_masking import mask_exception_message, mask_string
67

78
logger = logging.getLogger(__name__)
89

@@ -167,11 +168,13 @@ async def chat(
167168
"error": "timeout",
168169
}
169170
except Exception as e:
170-
logger.error(f"LLM request failed: {e}")
171+
# Mask any credentials in error message
172+
safe_error = mask_exception_message(e)
173+
logger.error(f"LLM request failed: {safe_error}")
171174
return {
172-
"text": f"Error: {str(e)}",
175+
"text": f"Error: {safe_error}",
173176
"confidence": 0.0,
174-
"error": str(e),
177+
"error": safe_error,
175178
}
176179

177180
async def analyze_image(
@@ -229,8 +232,10 @@ async def analyze_image(
229232
logger.error(f"Image analysis timed out after {timeout}s")
230233
return "Image analysis timed out."
231234
except Exception as e:
232-
logger.error(f"Image analysis failed: {e}")
233-
return f"Error analyzing image: {str(e)}"
235+
# Mask any credentials in error message
236+
safe_error = mask_exception_message(e)
237+
logger.error(f"Image analysis failed: {safe_error}")
238+
return f"Error analyzing image: {safe_error}"
234239

235240
@staticmethod
236241
def get_available_providers() -> List[str]:

app/models/unified_qa.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from .gemini_qa import GeminiQA
2323

2424
from app.utils.config import GEMINI_TIMEOUT, GEMINI_API_KEY, LLM_PROVIDER, LLM_MODEL
25+
from app.utils.credential_masking import mask_exception_message, mask_string
2526

2627
logger = logging.getLogger(__name__)
2728

@@ -113,7 +114,8 @@ async def chat(self, prompt: str) -> dict:
113114
)
114115
return response
115116
except Exception as e:
116-
logger.error(f"UnifiedQA.chat (LiteLLM) error: {e}")
117+
safe_error = mask_exception_message(e)
118+
logger.error(f"UnifiedQA.chat (LiteLLM) error: {safe_error}")
117119

118120
if not self.qa_system:
119121
return {"text": "Model not available. Check API keys or LLM_PROVIDER config.", "confidence": 0.0}
@@ -124,7 +126,8 @@ async def chat(self, prompt: str) -> dict:
124126
return await self._fallback_to_gemini(prompt)
125127
return response
126128
except Exception as e:
127-
logger.error(f"UnifiedQA.chat error: {e}")
129+
safe_error = mask_exception_message(e)
130+
logger.error(f"UnifiedQA.chat error: {safe_error}")
128131
logger.info("Attempting fallback to GeminiQA after error")
129132
return await self._fallback_to_gemini(prompt)
130133

@@ -141,8 +144,9 @@ async def _fallback_to_gemini(self, prompt: str) -> dict:
141144
logger.info("Fallback to GeminiQA successful")
142145
return response
143146
except Exception as e:
144-
logger.error(f"Fallback to GeminiQA also failed: {e}")
145-
return {"text": f"Error: All QA systems failed. Last error: {e}", "confidence": 0.0}
147+
safe_error = mask_exception_message(e)
148+
logger.error(f"Fallback to GeminiQA also failed: {safe_error}")
149+
return {"text": f"Error: All QA systems failed. Last error: {safe_error}", "confidence": 0.0}
146150

147151
async def ask_question(self, question: str, context: Optional[str] = None, pmid: Optional[str] = None) -> Dict:
148152
"""Ask a question with optional context. Returns {'answer': str, 'confidence': float}."""

0 commit comments

Comments
 (0)