Skip to content

Commit b8e13c6

Browse files
committed
refactor(logging): implement structured logging across modules
1 parent ab08b72 commit b8e13c6

File tree

8 files changed

+107
-60
lines changed

8 files changed

+107
-60
lines changed

.vscode/settings.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,16 @@
1313
"source.organizeImports.ruff": "explicit"
1414
}
1515
},
16+
"python.defaultInterpreterPath": ".venv/bin/python",
17+
"python.terminal.activateEnvironment": true,
1618
"yaml.schemas": {
1719
"https://www.schemastore.org/github-issue-config.json": "file:///Users/daniel/Documents/development/python/linkedin-mcp-server/.github/ISSUE_TEMPLATE/config.yml"
1820
},
21+
"cursorpyright.analysis.autoImportCompletions": true,
22+
"cursorpyright.analysis.diagnosticMode": "workspace",
23+
"cursorpyright.analysis.extraPaths": [
24+
"./linkedin_mcp_server"
25+
],
26+
"cursorpyright.analysis.stubPath": "./linkedin_mcp_server",
27+
"cursorpyright.analysis.typeCheckingMode": "off"
1928
}

linkedin_mcp_server/config/secrets.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ def get_credentials() -> Dict[str, str]:
2222

2323
# First, try configuration (includes environment variables)
2424
if config.linkedin.email and config.linkedin.password:
25-
print("Using LinkedIn credentials from configuration")
25+
logger.info("Using LinkedIn credentials from configuration")
2626
return {"email": config.linkedin.email, "password": config.linkedin.password}
2727

2828
# Second, try keyring if enabled
2929
if config.linkedin.use_keyring:
3030
credentials = get_credentials_from_keyring()
3131
if credentials["email"] and credentials["password"]:
32-
print(f"Using LinkedIn credentials from {get_keyring_name()}")
32+
logger.info(f"Using LinkedIn credentials from {get_keyring_name()}")
3333
return {"email": credentials["email"], "password": credentials["password"]}
3434

3535
# If in non-interactive mode and no credentials found, raise error
@@ -57,9 +57,9 @@ def prompt_for_credentials() -> Dict[str, str]:
5757

5858
# Store credentials securely in keyring
5959
if save_credentials_to_keyring(credentials["email"], credentials["password"]):
60-
print(f"Credentials stored securely in {get_keyring_name()}")
60+
logger.info(f"Credentials stored securely in {get_keyring_name()}")
6161
else:
62-
print("⚠️ Warning: Could not store credentials in system keyring.")
63-
print(" Your credentials will only be used for this session.")
62+
logger.warning("Could not store credentials in system keyring.")
63+
logger.info("Your credentials will only be used for this session.")
6464

6565
return credentials

linkedin_mcp_server/drivers/chrome.py

Lines changed: 53 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
This module handles the creation and management of Chrome WebDriver instances.
66
"""
77

8+
import logging
89
import os
910
import sys
1011
from typing import Dict, Optional
@@ -34,6 +35,8 @@
3435
# Global driver storage to reuse sessions
3536
active_drivers: Dict[str, webdriver.Chrome] = {}
3637

38+
logger = logging.getLogger(__name__)
39+
3740

3841
def get_or_create_driver() -> Optional[webdriver.Chrome]:
3942
"""
@@ -55,8 +58,8 @@ def get_or_create_driver() -> Optional[webdriver.Chrome]:
5558

5659
# Set up Chrome options
5760
chrome_options = Options()
58-
print(
59-
f"🌐 Running browser in {'headless' if config.chrome.headless else 'visible'} mode"
61+
logger.info(
62+
f"Running browser in {'headless' if config.chrome.headless else 'visible'} mode"
6063
)
6164
if config.chrome.headless:
6265
chrome_options.add_argument("--headless=new")
@@ -78,22 +81,22 @@ def get_or_create_driver() -> Optional[webdriver.Chrome]:
7881

7982
# Initialize Chrome driver
8083
try:
81-
print("🌐 Initializing Chrome WebDriver...")
84+
logger.info("Initializing Chrome WebDriver...")
8285

8386
# Use ChromeDriver path from environment or config
8487
chromedriver_path = (
8588
os.environ.get("CHROMEDRIVER_PATH") or config.chrome.chromedriver_path
8689
)
8790

8891
if chromedriver_path:
89-
print(f"🌐 Using ChromeDriver at path: {chromedriver_path}")
92+
logger.info(f"Using ChromeDriver at path: {chromedriver_path}")
9093
service = Service(executable_path=chromedriver_path)
9194
driver = webdriver.Chrome(service=service, options=chrome_options)
9295
else:
93-
print("🌐 Using auto-detected ChromeDriver")
96+
logger.info("Using auto-detected ChromeDriver")
9497
driver = webdriver.Chrome(options=chrome_options)
9598

96-
print("✅ Chrome WebDriver initialized successfully")
99+
logger.info("Chrome WebDriver initialized successfully")
97100

98101
# Add a page load timeout for safety
99102
driver.set_page_load_timeout(60)
@@ -103,7 +106,7 @@ def get_or_create_driver() -> Optional[webdriver.Chrome]:
103106
for attempt in range(max_retries):
104107
try:
105108
if login_to_linkedin(driver):
106-
print("Successfully logged in to LinkedIn")
109+
logger.info("Successfully logged in to LinkedIn")
107110
active_drivers[session_id] = driver
108111
return driver
109112
except (
@@ -123,15 +126,18 @@ def get_or_create_driver() -> Optional[webdriver.Chrome]:
123126
# In interactive mode, handle the error and potentially retry
124127
should_retry = handle_login_error(e)
125128
if should_retry and attempt < max_retries - 1:
126-
print(f"🔄 Retry attempt {attempt + 2}/{max_retries}")
129+
logger.info(f"Retry attempt {attempt + 2}/{max_retries}")
127130
continue
128131
else:
129132
# Clean up driver on final failure
130133
driver.quit()
131134
return None
132135
except Exception as e:
133-
error_msg = f"🛑 Error creating web driver: {e}"
134-
print(error_msg)
136+
error_msg = f"Error creating web driver: {e}"
137+
logger.error(
138+
error_msg,
139+
extra={"exception_type": type(e).__name__, "exception_message": str(e)},
140+
)
135141

136142
if config.chrome.non_interactive:
137143
raise DriverInitializationError(error_msg)
@@ -169,7 +175,7 @@ def login_to_linkedin(driver: webdriver.Chrome) -> bool:
169175
raise CredentialsNotFoundError("No credentials available")
170176

171177
# Login to LinkedIn using enhanced linkedin-scraper
172-
print("🔑 Logging in to LinkedIn...")
178+
logger.info("Logging in to LinkedIn...")
173179

174180
from linkedin_scraper import actions # type: ignore
175181

@@ -182,7 +188,7 @@ def login_to_linkedin(driver: webdriver.Chrome) -> bool:
182188
interactive=not config.chrome.non_interactive,
183189
)
184190

185-
print("✅ Successfully logged in to LinkedIn")
191+
logger.info("Successfully logged in to LinkedIn")
186192
return True
187193

188194
except Exception:
@@ -203,7 +209,7 @@ def login_to_linkedin(driver: webdriver.Chrome) -> bool:
203209

204210
elif "feed" in current_url or "mynetwork" in current_url:
205211
# Actually logged in successfully despite the exception
206-
print("✅ Successfully logged in to LinkedIn")
212+
logger.info("Successfully logged in to LinkedIn")
207213
return True
208214

209215
else:
@@ -232,10 +238,10 @@ def handle_login_error(error: Exception) -> bool:
232238
"""
233239
config = get_config()
234240

235-
print(f"\n{str(error)}")
241+
logger.error(f"\n{str(error)}")
236242

237243
if config.chrome.headless:
238-
print(
244+
logger.info(
239245
"🔍 Try running with visible browser window: uv run main.py --no-headless"
240246
)
241247

@@ -252,8 +258,8 @@ def handle_login_error(error: Exception) -> bool:
252258
)
253259
if retry and retry.get("retry", False):
254260
clear_credentials_from_keyring()
255-
print("✅ Credentials cleared from keyring.")
256-
print("🔄 Retrying with new credentials...")
261+
logger.info("✅ Credentials cleared from keyring.")
262+
logger.info("🔄 Retrying with new credentials...")
257263
return True
258264

259265
return False
@@ -266,31 +272,33 @@ def initialize_driver() -> None:
266272
config = get_config()
267273

268274
if config.server.lazy_init:
269-
print("Using lazy initialization - driver will be created on first tool call")
275+
logger.info(
276+
"Using lazy initialization - driver will be created on first tool call"
277+
)
270278
if config.linkedin.email and config.linkedin.password:
271-
print("LinkedIn credentials found in configuration")
279+
logger.info("LinkedIn credentials found in configuration")
272280
else:
273-
print(
281+
logger.info(
274282
"No LinkedIn credentials found - will look for stored credentials on first use"
275283
)
276284
return
277285

278286
# Validate chromedriver can be found
279287
if config.chrome.chromedriver_path:
280-
print(f"✅ ChromeDriver found at: {config.chrome.chromedriver_path}")
288+
logger.info(f"✅ ChromeDriver found at: {config.chrome.chromedriver_path}")
281289
os.environ["CHROMEDRIVER"] = config.chrome.chromedriver_path
282290
else:
283-
print("⚠️ ChromeDriver not found in common locations.")
284-
print("⚡ Continuing with automatic detection...")
285-
print(
291+
logger.info("⚠️ ChromeDriver not found in common locations.")
292+
logger.info("⚡ Continuing with automatic detection...")
293+
logger.info(
286294
"💡 Tip: install ChromeDriver and set the CHROMEDRIVER environment variable"
287295
)
288296

289297
# Create driver and log in
290298
try:
291299
driver = get_or_create_driver()
292300
if driver:
293-
print("✅ Web driver initialized successfully")
301+
logger.info("✅ Web driver initialized successfully")
294302
else:
295303
# Driver creation failed - always raise an error
296304
raise DriverInitializationError("Failed to initialize web driver")
@@ -310,7 +318,7 @@ def initialize_driver() -> None:
310318
raise DriverInitializationError(
311319
f"Failed to initialize web driver: {str(e)}"
312320
)
313-
print(f"❌ Failed to initialize web driver: {str(e)}")
321+
logger.error(f"❌ Failed to initialize web driver: {str(e)}")
314322
handle_driver_error()
315323

316324

@@ -322,7 +330,9 @@ def handle_driver_error() -> None:
322330

323331
# Skip interactive handling in non-interactive mode
324332
if config.chrome.non_interactive:
325-
print("❌ ChromeDriver is required for this application to work properly.")
333+
logger.error(
334+
"❌ ChromeDriver is required for this application to work properly."
335+
)
326336
sys.exit(1)
327337

328338
questions = [
@@ -347,24 +357,28 @@ def handle_driver_error() -> None:
347357
# Update config with the new path
348358
config.chrome.chromedriver_path = path
349359
os.environ["CHROMEDRIVER"] = path
350-
print(f"✅ ChromeDriver path set to: {path}")
351-
print("💡 Please restart the application to use the new ChromeDriver path.")
352-
print(" Example: uv run main.py")
360+
logger.info(f"✅ ChromeDriver path set to: {path}")
361+
logger.info(
362+
"💡 Please restart the application to use the new ChromeDriver path."
363+
)
364+
logger.info(" Example: uv run main.py")
353365
sys.exit(0)
354366
else:
355-
print(f"⚠️ Warning: The specified path does not exist: {path}")
356-
print("💡 Please check the path and restart the application.")
367+
logger.warning(f"⚠️ Warning: The specified path does not exist: {path}")
368+
logger.info("💡 Please check the path and restart the application.")
357369
sys.exit(1)
358370

359371
elif answers["chromedriver_action"] == "help":
360-
print("\n📋 ChromeDriver Installation Guide:")
361-
print("1. Find your Chrome version: Chrome menu > Help > About Google Chrome")
362-
print(
372+
logger.info("\n📋 ChromeDriver Installation Guide:")
373+
logger.info(
374+
"1. Find your Chrome version: Chrome menu > Help > About Google Chrome"
375+
)
376+
logger.info(
363377
"2. Download matching ChromeDriver: https://chromedriver.chromium.org/downloads"
364378
)
365-
print("3. Place ChromeDriver in a location on your PATH")
366-
print(" - macOS/Linux: /usr/local/bin/ is recommended")
367-
print(
379+
logger.info("3. Place ChromeDriver in a location on your PATH")
380+
logger.info(" - macOS/Linux: /usr/local/bin/ is recommended")
381+
logger.info(
368382
" - Windows: Add to a directory in your PATH or specify the full path\n"
369383
)
370384

@@ -373,5 +387,5 @@ def handle_driver_error() -> None:
373387
)["try_again"]:
374388
initialize_driver()
375389

376-
print("❌ ChromeDriver is required for this application to work properly.")
390+
logger.error("❌ ChromeDriver is required for this application to work properly.")
377391
sys.exit(1)

linkedin_mcp_server/server.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
This module creates the MCP server and registers all the LinkedIn tools.
66
"""
77

8+
import logging
89
from typing import Any, Dict
910

1011
from fastmcp import FastMCP
@@ -14,6 +15,8 @@
1415
from linkedin_mcp_server.tools.job import register_job_tools
1516
from linkedin_mcp_server.tools.person import register_person_tools
1617

18+
logger = logging.getLogger(__name__)
19+
1720

1821
def create_mcp_server() -> FastMCP:
1922
"""Create and configure the MCP server with all LinkedIn tools."""
@@ -59,4 +62,11 @@ def shutdown_handler() -> None:
5962
driver.quit()
6063
del active_drivers[session_id]
6164
except Exception as e:
62-
print(f"❌ Error closing driver during shutdown: {e}")
65+
logger.error(
66+
f"Error closing driver during shutdown: {e}",
67+
extra={
68+
"session_id": session_id,
69+
"exception_type": type(e).__name__,
70+
"exception_message": str(e),
71+
},
72+
)

linkedin_mcp_server/tools/company.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
This module provides tools for scraping LinkedIn company profiles.
66
"""
77

8+
import logging
89
from typing import Any, Dict, List
910

1011
from fastmcp import FastMCP
1112
from linkedin_scraper import Company
1213

1314
from linkedin_mcp_server.error_handler import handle_tool_error, safe_get_driver
1415

16+
logger = logging.getLogger(__name__)
17+
1518

1619
def register_company_tools(mcp: FastMCP) -> None:
1720
"""
@@ -38,9 +41,9 @@ async def get_company_profile(
3841
try:
3942
driver = safe_get_driver()
4043

41-
print(f"🏢 Scraping company: {linkedin_url}")
44+
logger.info(f"Scraping company: {linkedin_url}")
4245
if get_employees:
43-
print("⚠️ Fetching employees may take a while...")
46+
logger.info("Fetching employees may take a while...")
4447

4548
company = Company(
4649
linkedin_url,

linkedin_mcp_server/tools/job.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
This module provides tools for scraping LinkedIn job postings and searches.
66
"""
77

8+
import logging
89
from typing import Any, Dict, List
910

1011
from fastmcp import FastMCP
@@ -16,6 +17,8 @@
1617
safe_get_driver,
1718
)
1819

20+
logger = logging.getLogger(__name__)
21+
1922

2023
def register_job_tools(mcp: FastMCP) -> None:
2124
"""
@@ -46,7 +49,7 @@ async def get_job_details(job_url: str) -> Dict[str, Any]:
4649
try:
4750
driver = safe_get_driver()
4851

49-
print(f"💼 Scraping job: {job_url}")
52+
logger.info(f"Scraping job: {job_url}")
5053
job = Job(job_url, driver=driver, close_on_complete=False)
5154

5255
# Convert job object to a dictionary
@@ -68,7 +71,7 @@ async def search_jobs(search_term: str) -> List[Dict[str, Any]]:
6871
try:
6972
driver = safe_get_driver()
7073

71-
print(f"🔍 Searching jobs: {search_term}")
74+
logger.info(f"Searching jobs: {search_term}")
7275
job_search = JobSearch(driver=driver, close_on_complete=False, scrape=False)
7376
jobs = job_search.search(search_term)
7477

@@ -88,7 +91,7 @@ async def get_recommended_jobs() -> List[Dict[str, Any]]:
8891
try:
8992
driver = safe_get_driver()
9093

91-
print("📋 Getting recommended jobs")
94+
logger.info("Getting recommended jobs")
9295
job_search = JobSearch(
9396
driver=driver,
9497
close_on_complete=False,

0 commit comments

Comments
 (0)