Skip to content

Commit 8584d4a

Browse files
Merge pull request #56 from alexander-zuev/feat/log-tools
feat(logs): introduce retrieve_logs tool
2 parents 0f6d8dd + ca81586 commit 8584d4a

32 files changed

+1265
-65
lines changed

.github/workflows/docs/release-checklist.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@ Post-release
1616
- Clean install from PyPi works
1717

1818

19+
## v0.3.12 - 2025-03-12
20+
21+
Pre-release
22+
1. Tests pass - []
23+
2. CI passes - []
24+
3. Build succeeds - []
25+
4. Documentation is up to date - []
26+
5. Changelog is up to date - []
27+
6. Tag and release on GitHub
28+
7. Release is published to PyPI
29+
8. Update dockerfile - []
30+
9. Update .env.example (if necessary) - []
31+
32+
Post-release
33+
10. Clean install from PyPi works - []
34+
35+
1936
## v0.3.8 - 2025-03-07
2037

2138
Pre-release

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ Icon
6464
# Local development
6565

6666
*.log
67-
logs/
67+
# Only ignore logs directory in the root, not in the package
68+
/logs/
6869

6970

7071
# Ignore local assets

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,10 @@ repos:
120120
pass_filenames: false
121121
args: [
122122
"--no-header",
123-
"-v",
124123
"--quiet",
125124
"--no-summary",
126-
"--show-capture=no"
125+
"--show-capture=no",
126+
"--tb=line" # Show only one line per failure
127127
]
128128
stages: [pre-commit, pre-push]
129129

CHANGELOG.MD

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

77

8+
## [0.3.12] - 2025-03-12
9+
### Added
10+
- Implemented a new `retrieve_logs` tool that allows retrieval of logs from any Supabase log collection - Postgres, PostgREST, Auth, Edge Functions and others. Provides a way to query and filter
11+
- Implemented log rotation to prevent unbounded log file growth (5MB limit with 3 backup files)
12+
13+
### Changed
14+
- Improved region configuration with clearer error messages for region mismatches
15+
- Updated smithery.yaml to reduce configuration error rate (Tenant not found)
16+
- Improved PostgreSQL client connection error handling with specific guidance for "Tenant or user not found" errors
17+
18+
819
## [0.3.11] - 2025-03-10
920
### Fixed
1021
- Fixed an error with creating a migration file when a user doesn't have `supabase_migrations` schema

README.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,16 @@ The server uses the following environment variables:
120120

121121
| Variable | Required | Default | Description |
122122
|----------|----------|---------|-------------|
123-
| `SUPABASE_PROJECT_REF` | No | `127.0.0.1:54322` | Your Supabase project reference ID (or local host:port) |
124-
| `SUPABASE_DB_PASSWORD` | No | `postgres` | Your database password |
125-
| `SUPABASE_REGION` | No | `us-east-1` | AWS region where your Supabase project is hosted |
123+
| `SUPABASE_PROJECT_REF` | Yes | `127.0.0.1:54322` | Your Supabase project reference ID (or local host:port) |
124+
| `SUPABASE_DB_PASSWORD` | Yes | `postgres` | Your database password |
125+
| `SUPABASE_REGION` | Yes* | `us-east-1` | AWS region where your Supabase project is hosted |
126126
| `SUPABASE_ACCESS_TOKEN` | No | None | Personal access token for Supabase Management API |
127127
| `SUPABASE_SERVICE_ROLE_KEY` | No | None | Service role key for Auth Admin SDK |
128128

129129
> **Note**: The default values are configured for local Supabase development. For remote Supabase projects, you must provide your own values for `SUPABASE_PROJECT_REF` and `SUPABASE_DB_PASSWORD`.
130130
131+
> 🚨 **CRITICAL CONFIGURATION NOTE**: For remote Supabase projects, you MUST specify the correct region where your project is hosted using `SUPABASE_REGION`. If you encounter a "Tenant or user not found" error, this is almost certainly because your region setting doesn't match your project's actual region. You can find your project's region in the Supabase dashboard under Project Settings.
132+
131133
#### Connection Types
132134

133135
##### Database Connection
@@ -492,6 +494,28 @@ The Auth Admin SDK provides several key advantages over direct SQL manipulation:
492494
- Error handling: The server provides detailed error messages from the Supabase API, which may differ from the dashboard interface
493495
- Method availability: Some methods like `delete_factor` are exposed in the API but not fully implemented in the SDK
494496
497+
### Logs & Analytics
498+
499+
The server provides access to Supabase logs and analytics data, making it easier to monitor and troubleshoot your applications:
500+
501+
- **Available Tool**: `retrieve_logs` - Access logs from any Supabase service
502+
503+
- **Log Collections**:
504+
- `postgres`: Database server logs
505+
- `api_gateway`: API gateway requests
506+
- `auth`: Authentication events
507+
- `postgrest`: RESTful API service logs
508+
- `pooler`: Connection pooling logs
509+
- `storage`: Object storage operations
510+
- `realtime`: WebSocket subscription logs
511+
- `edge_functions`: Serverless function executions
512+
- `cron`: Scheduled job logs
513+
- `pgbouncer`: Connection pooler logs
514+
515+
- **Features**: Filter by time, search text, apply field filters, or use custom SQL queries
516+
517+
Simplifies debugging across your Supabase stack without switching between interfaces or writing complex queries.
518+
495519
### Automatic Versioning of Database Changes
496520
497521
"With great power comes great responsibility." While `execute_postgresql` tool coupled with aptly named `live_dangerously` tool provide a powerful and simple way to manage your Supabase database, it also means that dropping a table or modifying one is one chat message away. In order to reduce the risk of irreversible changes, since v0.3.8 the server supports:

smithery.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ startCommand:
1010
required:
1111
- supabaseProjectRef
1212
- supabaseDbPassword
13+
- supabaseRegion
1314
properties:
1415
supabaseProjectRef:
1516
type: string
@@ -21,8 +22,7 @@ startCommand:
2122
default: "postgres"
2223
supabaseRegion:
2324
type: string
24-
description: "(optional) - AWS region where your Supabase project is hosted - Default: us-east-1"
25-
default: "us-east-1"
25+
description: "(required) - AWS region where your Supabase project is hosted - Default: us-east-1"
2626
supabaseAccessToken:
2727
type: string
2828
description: "(optional) - Personal access token for Supabase Management API - Default: none"

supabase_mcp/core/container.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from supabase_mcp.services.api.api_manager import SupabaseApiManager
66
from supabase_mcp.services.database.postgres_client import PostgresClient
77
from supabase_mcp.services.database.query_manager import QueryManager
8+
from supabase_mcp.services.logs.log_manager import LogManager
89
from supabase_mcp.services.safety.safety_manager import SafetyManager
910
from supabase_mcp.services.sdk.sdk_client import SupabaseSDKClient
1011
from supabase_mcp.settings import Settings
@@ -22,6 +23,7 @@ def __init__(
2223
safety_manager: SafetyManager | None = None,
2324
query_manager: QueryManager | None = None,
2425
tool_manager: ToolManager | None = None,
26+
log_manager: LogManager | None = None,
2527
) -> None:
2628
"""Create a new container container reference"""
2729
self.mcp_server = mcp_server
@@ -32,6 +34,7 @@ def __init__(
3234
self.safety_manager = safety_manager
3335
self.query_manager = query_manager
3436
self.tool_manager = tool_manager
37+
self.log_manager = log_manager
3538

3639
def initialize(self, settings: Settings) -> "Container":
3740
"""Initializes all services in a synchronous manner to satisfy MCP runtime requirements"""

supabase_mcp/logger.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import logging
2+
import logging.handlers
23
from pathlib import Path
34

45

56
def setup_logger() -> logging.Logger:
6-
"""Configure logging for the MCP server."""
7+
"""Configure logging for the MCP server with log rotation."""
78
logger = logging.getLogger("supabase-mcp")
89

910
# Remove existing handlers to avoid duplicate logs
@@ -17,8 +18,15 @@ def setup_logger() -> logging.Logger:
1718
# Define the log file path
1819
log_file = log_dir / "mcp_server.log"
1920

20-
# Create a file handler (only logs to file, no stdout)
21-
file_handler = logging.FileHandler(log_file)
21+
# Create a rotating file handler
22+
# - Rotate when log reaches 5MB
23+
# - Keep 3 backup files
24+
file_handler = logging.handlers.RotatingFileHandler(
25+
log_file,
26+
maxBytes=5 * 1024 * 1024, # 5MB
27+
backupCount=3,
28+
encoding="utf-8",
29+
)
2230

2331
# Create formatter
2432
formatter = logging.Formatter("[%(asctime)s] %(levelname)-8s %(message)s", datefmt="%y/%m/%d %H:%M:%S")

supabase_mcp/services/api/api_manager.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from supabase_mcp.logger import logger
77
from supabase_mcp.services.api.api_client import ManagementAPIClient
88
from supabase_mcp.services.api.spec_manager import ApiSpecManager
9+
from supabase_mcp.services.logs.log_manager import LogManager
910
from supabase_mcp.services.safety.models import ClientType
1011
from supabase_mcp.services.safety.safety_manager import SafetyManager
1112
from supabase_mcp.settings import settings
@@ -35,11 +36,13 @@ def __init__(
3536
api_client: ManagementAPIClient,
3637
safety_manager: SafetyManager,
3738
spec_manager: ApiSpecManager | None = None,
39+
log_manager: LogManager | None = None,
3840
) -> None:
3941
"""Initialize the API manager."""
4042
self.spec_manager = spec_manager or ApiSpecManager() # this is so that I don't have to pass it
4143
self.client = api_client
4244
self.safety_manager = safety_manager
45+
self.log_manager = log_manager or LogManager()
4346

4447
@classmethod
4548
def get_instance(
@@ -288,3 +291,57 @@ async def handle_spec_request(
288291
# Option 3: Get all domains (default)
289292
else:
290293
return {"domains": spec_manager.get_all_domains()}
294+
295+
async def retrieve_logs(
296+
self,
297+
collection: str,
298+
limit: int = 20,
299+
hours_ago: int | None = 1,
300+
filters: list[dict[str, Any]] | None = None,
301+
search: str | None = None,
302+
custom_query: str | None = None,
303+
) -> dict[str, Any]:
304+
"""Retrieve logs from a Supabase service.
305+
306+
Args:
307+
collection: The log collection to query
308+
limit: Maximum number of log entries to return
309+
hours_ago: Retrieve logs from the last N hours
310+
filters: List of filter objects with field, operator, and value
311+
search: Text to search for in event messages
312+
custom_query: Complete custom SQL query to execute
313+
314+
Returns:
315+
The query result
316+
317+
Raises:
318+
ValueError: If the collection is unknown
319+
"""
320+
log_manager = self.log_manager
321+
322+
# Build the SQL query using LogManager
323+
sql = log_manager.build_logs_query(
324+
collection=collection,
325+
limit=limit,
326+
hours_ago=hours_ago,
327+
filters=filters,
328+
search=search,
329+
custom_query=custom_query,
330+
)
331+
332+
logger.debug(f"Executing log query: {sql}")
333+
334+
# Make the API request
335+
try:
336+
response = await self.execute_request(
337+
method="GET",
338+
path="/v1/projects/{ref}/analytics/endpoints/logs.all",
339+
path_params={},
340+
request_params={"sql": sql},
341+
request_body={},
342+
)
343+
344+
return response
345+
except Exception as e:
346+
logger.error(f"Error retrieving logs: {e}")
347+
raise

supabase_mcp/services/database/postgres_client.py

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,12 @@ def __init__(
7777
self.db_url = self._build_connection_string()
7878
self.sql_validator: SQLValidator = SQLValidator()
7979

80-
logger.info(f"Initialized PostgresClient with project ref: {self.project_ref}")
80+
# Only log once during initialization with clear project info
81+
is_local = self.project_ref.startswith("127.0.0.1")
82+
logger.info(
83+
f"PostgreSQL client initialized for {'local' if is_local else 'remote'} "
84+
f"project: {self.project_ref} (region: {self.db_region})"
85+
)
8186

8287
@classmethod
8388
def get_instance(
@@ -118,15 +123,13 @@ def _build_connection_string(self) -> str:
118123
if self.project_ref.startswith("127.0.0.1"):
119124
# Local development
120125
connection_string = f"postgresql://postgres:{encoded_password}@{self.project_ref}/postgres"
121-
logger.debug("Using local development connection string")
122126
return connection_string
123127

124128
# Production Supabase - via transaction pooler
125129
connection_string = (
126130
f"postgresql://postgres.{self.project_ref}:{encoded_password}"
127131
f"@aws-0-{self._settings.supabase_region}.pooler.supabase.com:6543/postgres"
128132
)
129-
logger.debug(f"Using production connection string for region: {self._settings.supabase_region}")
130133
return connection_string
131134

132135
@retry(
@@ -152,7 +155,7 @@ async def create_pool(self) -> asyncpg.Pool[asyncpg.Record]:
152155
ConnectionError: If unable to establish a connection to the database
153156
"""
154157
try:
155-
logger.debug(f"Creating asyncpg connection pool for: {self.db_url.split('@')[1]}")
158+
logger.debug(f"Creating connection pool for project: {self.project_ref}")
156159

157160
# Create the pool with optimal settings
158161
pool = await asyncpg.create_pool(
@@ -167,14 +170,61 @@ async def create_pool(self) -> asyncpg.Pool[asyncpg.Record]:
167170
# Test the connection with a simple query
168171
async with pool.acquire() as conn:
169172
await conn.execute("SELECT 1")
170-
logger.debug("Connection test successful")
171173

172-
logger.info("✓ Created PostgreSQL connection pool with asyncpg")
174+
logger.info("✓ Database connection established successfully")
173175
return pool
174176

175-
except (asyncpg.PostgresError, OSError) as e:
177+
except asyncpg.PostgresError as e:
178+
# Extract connection details for better error reporting
179+
host_part = self.db_url.split("@")[1].split("/")[0] if "@" in self.db_url else "unknown"
180+
181+
# Check specifically for the "Tenant or user not found" error which is often caused by region mismatch
182+
if "Tenant or user not found" in str(e):
183+
error_message = (
184+
"CONNECTION ERROR: Region mismatch detected!\n\n"
185+
f"Could not connect to Supabase project '{self.project_ref}'.\n\n"
186+
"This error typically occurs when your SUPABASE_REGION setting doesn't match your project's actual region.\n"
187+
f"Your configuration is using region: '{self.db_region}' (default: us-east-1)\n\n"
188+
"ACTION REQUIRED: Please set the correct SUPABASE_REGION in your MCP server configuration.\n"
189+
"You can find your project's region in the Supabase dashboard under Project Settings."
190+
)
191+
else:
192+
error_message = (
193+
f"Could not connect to database: {e}\n"
194+
f"Connection attempted to: {host_part}\n via Transaction Pooler\n"
195+
f"Project ref: {self.project_ref}\n"
196+
f"Region: {self.db_region}\n\n"
197+
f"Please check:\n"
198+
f"1. Your Supabase project reference is correct\n"
199+
f"2. Your database password is correct\n"
200+
f"3. Your region setting matches your Supabase project region\n"
201+
f"4. Your Supabase project is active and the database is online\n"
202+
)
203+
176204
logger.error(f"Failed to connect to database: {e}")
177-
raise ConnectionError(f"Could not connect to database: {e}") from e
205+
logger.error(f"Connection details: {host_part}, Project: {self.project_ref}, Region: {self.db_region}")
206+
207+
raise ConnectionError(error_message) from e
208+
209+
except OSError as e:
210+
# For network-related errors, provide a different message that clearly indicates
211+
# this is a network/system issue rather than a database configuration problem
212+
host_part = self.db_url.split("@")[1].split("/")[0] if "@" in self.db_url else "unknown"
213+
214+
error_message = (
215+
f"Network error while connecting to database: {e}\n"
216+
f"Connection attempted to: {host_part}\n\n"
217+
f"This appears to be a network or system issue rather than a database configuration problem.\n"
218+
f"Please check:\n"
219+
f"1. Your internet connection is working\n"
220+
f"2. Any firewalls or network security settings allow connections to {host_part}\n"
221+
f"3. DNS resolution is working correctly\n"
222+
f"4. The Supabase service is not experiencing an outage\n"
223+
)
224+
225+
logger.error(f"Network error connecting to database: {e}")
226+
logger.error(f"Connection details: {host_part}")
227+
raise ConnectionError(error_message) from e
178228

179229
async def ensure_pool(self) -> None:
180230
"""Ensure a valid connection pool exists.

0 commit comments

Comments
 (0)