Skip to content

Commit 9e8767d

Browse files
committed
feat(logging): add logging support and examples
* Implement built-in logging using Python's logging module. * Configure log level via environment variable or programmatically. * Add logging_example.py to demonstrate logging configuration. * Update README.md with logging instructions. * Enhance SQLiteVecClient with logging for various operations. * Create tests for logging functionality.
1 parent aab2747 commit 9e8767d

File tree

7 files changed

+198
-10
lines changed

7 files changed

+198
-10
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,34 @@ client.close()
6767
## How it works
6868
`SQLiteVecClient` stores data in `{table}` and mirrors embeddings in `{table}_vec` (a `vec0` virtual table). SQLite triggers keep both in sync when rows are inserted, updated, or deleted. Embeddings are serialized as packed float32 bytes for compact storage.
6969

70+
## Logging
71+
72+
The library includes built-in logging support using Python's standard logging module. By default, logging is set to WARNING level.
73+
74+
**Configure log level via environment variable:**
75+
```bash
76+
export SQLITE_VEC_CLIENT_LOG_LEVEL=DEBUG # Linux/macOS
77+
set SQLITE_VEC_CLIENT_LOG_LEVEL=DEBUG # Windows
78+
```
79+
80+
**Or programmatically:**
81+
```python
82+
import logging
83+
from sqlite_vec_client import get_logger
84+
85+
logger = get_logger()
86+
logger.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, CRITICAL
87+
```
88+
89+
**Available log levels:**
90+
- `DEBUG`: Detailed information for diagnosing issues
91+
- `INFO`: General informational messages about operations
92+
- `WARNING`: Warning messages (default)
93+
- `ERROR`: Error messages
94+
- `CRITICAL`: Critical error messages
95+
96+
See [examples/logging_example.py](examples/logging_example.py) for a complete example.
97+
7098
## Testing
7199

72100
The project has comprehensive test coverage (91%+) with 75 tests covering:
@@ -148,6 +176,8 @@ ruff check . && ruff format . && mypy sqlite_vec_client/ && pytest
148176
- [CHANGELOG.md](CHANGELOG.md) - Version history
149177
- [TESTING.md](TESTING.md) - Testing documentation
150178
- [Examples](examples/) - Usage examples
179+
- [basic_usage.py](examples/basic_usage.py) - Basic CRUD operations
180+
- [logging_example.py](examples/logging_example.py) - Logging configuration
151181

152182
## Contributing
153183

TODO

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
- [x] examples/batch_operations.py
3131
- [x] examples/context_manager.py
3232
- [x] examples/real_world_scenario.py
33+
- [x] examples/logging_example.py
3334

3435
### Documentation
3536
- [x] Create CONTRIBUTING.md
@@ -54,10 +55,9 @@
5455
- [ ] Linting and type checking
5556

5657
### Logging
57-
- [ ] Python logging module integration
58-
- [ ] Log level configuration
59-
- [ ] Debug mode support
60-
- [ ] Query logging (optional)
58+
- [x] Python logging module integration
59+
- [x] Log level configuration
60+
- [x] Debug mode support
6161

6262
## 🔵 Low Priority (New Features)
6363

examples/logging_example.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Example demonstrating logging configuration in sqlite-vec-client."""
2+
3+
import logging
4+
import os
5+
6+
from sqlite_vec_client import SQLiteVecClient, get_logger
7+
8+
# Example 1: Enable DEBUG logging via environment variable
9+
os.environ["SQLITE_VEC_CLIENT_LOG_LEVEL"] = "DEBUG"
10+
11+
# Reload logger to pick up new level
12+
logger = get_logger()
13+
logger.setLevel(logging.DEBUG)
14+
15+
print("=== Example 1: DEBUG logging ===\n")
16+
17+
client = SQLiteVecClient(table="docs", db_path=":memory:")
18+
client.create_table(dim=3, distance="cosine")
19+
20+
texts = ["hello", "world"]
21+
embeddings = [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]
22+
rowids = client.add(texts=texts, embeddings=embeddings)
23+
24+
results = client.similarity_search(embedding=[0.1, 0.2, 0.3], top_k=2)
25+
client.close()
26+
27+
print("\n=== Example 2: INFO logging ===\n")
28+
29+
# Change to INFO level
30+
os.environ["SQLITE_VEC_CLIENT_LOG_LEVEL"] = "INFO"
31+
logger.setLevel(logging.INFO)
32+
33+
client2 = SQLiteVecClient(table="articles", db_path=":memory:")
34+
client2.create_table(dim=3)
35+
client2.add(texts=["test"], embeddings=[[0.1, 0.2, 0.3]])
36+
client2.close()
37+
38+
print("\n=== Example 3: WARNING logging (default) ===\n")
39+
40+
# Default level (WARNING) - minimal output
41+
os.environ["SQLITE_VEC_CLIENT_LOG_LEVEL"] = "WARNING"
42+
logger.setLevel(logging.WARNING)
43+
44+
client3 = SQLiteVecClient(table="items", db_path=":memory:")
45+
client3.create_table(dim=3)
46+
client3.add(texts=["silent"], embeddings=[[0.1, 0.2, 0.3]])
47+
client3.close()
48+
49+
print("Done! (No logs shown at WARNING level for normal operations)")

sqlite_vec_client/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
ValidationError,
1313
VecClientError,
1414
)
15+
from .logger import get_logger
1516

1617
__all__ = [
1718
"SQLiteVecClient",
@@ -21,4 +22,5 @@
2122
"TableNotFoundError",
2223
"ConnectionError",
2324
"DimensionMismatchError",
25+
"get_logger",
2426
]

sqlite_vec_client/base.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from .exceptions import ConnectionError as VecConnectionError
1818
from .exceptions import TableNotFoundError
19+
from .logger import get_logger
1920
from .types import Embeddings, Metadata, Result, Rowids, SimilaritySearchResult, Text
2021
from .utils import deserialize_f32, serialize_f32
2122
from .validation import (
@@ -27,6 +28,8 @@
2728
validate_top_k,
2829
)
2930

31+
logger = get_logger()
32+
3033

3134
class SQLiteVecClient:
3235
"""Manage a text+embedding table and its sqlite-vec index.
@@ -52,15 +55,19 @@ def create_connection(db_path: str) -> sqlite3.Connection:
5255
VecConnectionError: If connection or extension loading fails
5356
"""
5457
try:
58+
logger.debug(f"Connecting to database: {db_path}")
5559
connection = sqlite3.connect(db_path)
5660
connection.row_factory = sqlite3.Row
5761
connection.enable_load_extension(True)
5862
sqlite_vec.load(connection)
5963
connection.enable_load_extension(False)
64+
logger.info(f"Successfully connected to database: {db_path}")
6065
return connection
6166
except sqlite3.Error as e:
67+
logger.error(f"Failed to connect to database {db_path}: {e}")
6268
raise VecConnectionError(f"Failed to connect to database: {e}") from e
6369
except Exception as e:
70+
logger.error(f"Failed to load sqlite-vec extension: {e}")
6471
raise VecConnectionError(f"Failed to load sqlite-vec extension: {e}") from e
6572

6673
@staticmethod
@@ -89,6 +96,7 @@ def __init__(self, table: str, db_path: str) -> None:
8996
"""
9097
validate_table_name(table)
9198
self.table = table
99+
logger.debug(f"Initializing SQLiteVecClient for table: {table}")
92100
self.connection = self.create_connection(db_path)
93101

94102
def __enter__(self) -> SQLiteVecClient:
@@ -119,6 +127,9 @@ def create_table(
119127
ValidationError: If dimension is invalid
120128
"""
121129
validate_dimension(dim)
130+
logger.info(
131+
f"Creating table '{self.table}' with dim={dim}, distance={distance}"
132+
)
122133
self.connection.execute(
123134
f"""
124135
CREATE TABLE IF NOT EXISTS {self.table}
@@ -174,6 +185,7 @@ def create_table(
174185
"""
175186
)
176187
self.connection.commit()
188+
logger.debug(f"Table '{self.table}' and triggers created successfully")
177189

178190
def similarity_search(
179191
self,
@@ -194,6 +206,7 @@ def similarity_search(
194206
TableNotFoundError: If table doesn't exist
195207
"""
196208
validate_top_k(top_k)
209+
logger.debug(f"Performing similarity search with top_k={top_k}")
197210
try:
198211
cursor = self.connection.cursor()
199212
cursor.execute(
@@ -212,9 +225,11 @@ def similarity_search(
212225
[serialize_f32(embedding), top_k],
213226
)
214227
results = cursor.fetchall()
228+
logger.debug(f"Similarity search returned {len(results)} results")
215229
return [(row["rowid"], row["text"], row["distance"]) for row in results]
216230
except sqlite3.OperationalError as e:
217231
if "no such table" in str(e).lower():
232+
logger.error(f"Table '{self.table}' not found during similarity search")
218233
raise TableNotFoundError(
219234
f"Table '{self.table}' or '{self.table}_vec' does not exist. "
220235
"Call create_table() first."
@@ -242,6 +257,7 @@ def add(
242257
TableNotFoundError: If table doesn't exist
243258
"""
244259
validate_embeddings_match(texts, embeddings, metadata)
260+
logger.debug(f"Adding {len(texts)} records to table '{self.table}'")
245261
try:
246262
max_id = self.connection.execute(
247263
f"SELECT max(rowid) as rowid FROM {self.table}"
@@ -266,9 +282,12 @@ def add(
266282
results = self.connection.execute(
267283
f"SELECT rowid FROM {self.table} WHERE rowid > {max_id}"
268284
)
269-
return [row["rowid"] for row in results]
285+
rowids = [row["rowid"] for row in results]
286+
logger.info(f"Added {len(rowids)} records to table '{self.table}'")
287+
return rowids
270288
except sqlite3.OperationalError as e:
271289
if "no such table" in str(e).lower():
290+
logger.error(f"Table '{self.table}' not found during add operation")
272291
raise TableNotFoundError(
273292
f"Table '{self.table}' does not exist. Call create_table() first."
274293
) from e
@@ -380,6 +399,7 @@ def update(
380399
embedding: Embeddings | None = None,
381400
) -> bool:
382401
"""Update fields of a record by rowid; return True if a row changed."""
402+
logger.debug(f"Updating record with rowid={rowid}")
383403
sets = []
384404
params: list[Any] = []
385405
if text is not None:
@@ -400,31 +420,43 @@ def update(
400420
cur = self.connection.cursor()
401421
cur.execute(sql, params)
402422
self.connection.commit()
403-
return cur.rowcount > 0
423+
updated = cur.rowcount > 0
424+
if updated:
425+
logger.debug(f"Successfully updated record with rowid={rowid}")
426+
return updated
404427

405428
def delete_by_id(self, rowid: int) -> bool:
406429
"""Delete a single record by rowid; return True if a row was removed."""
430+
logger.debug(f"Deleting record with rowid={rowid}")
407431
cur = self.connection.cursor()
408432
cur.execute(f"DELETE FROM {self.table} WHERE rowid = ?", [rowid])
409433
self.connection.commit()
410-
return cur.rowcount > 0
434+
deleted = cur.rowcount > 0
435+
if deleted:
436+
logger.debug(f"Successfully deleted record with rowid={rowid}")
437+
return deleted
411438

412439
def delete_many(self, rowids: list[int]) -> int:
413440
"""Delete multiple records by rowids; return number of rows removed."""
414441
if not rowids:
415442
return 0
443+
logger.debug(f"Deleting {len(rowids)} records")
416444
placeholders = ",".join(["?"] * len(rowids))
417445
cur = self.connection.cursor()
418446
cur.execute(
419447
f"DELETE FROM {self.table} WHERE rowid IN ({placeholders})",
420448
rowids,
421449
)
422450
self.connection.commit()
423-
return cur.rowcount
451+
deleted_count = cur.rowcount
452+
logger.info(f"Deleted {deleted_count} records from table '{self.table}'")
453+
return deleted_count
424454

425455
def close(self) -> None:
426456
"""Close the underlying SQLite connection, suppressing close errors."""
427457
try:
458+
logger.debug(f"Closing connection for table '{self.table}'")
428459
self.connection.close()
429-
except Exception:
430-
pass
460+
logger.info(f"Connection closed for table '{self.table}'")
461+
except Exception as e:
462+
logger.warning(f"Error closing connection: {e}")

sqlite_vec_client/logger.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Logging configuration for sqlite-vec-client."""
2+
3+
import logging
4+
import os
5+
6+
# Get log level from environment variable, default to WARNING
7+
LOG_LEVEL = os.environ.get("SQLITE_VEC_CLIENT_LOG_LEVEL", "WARNING").upper()
8+
9+
# Create logger
10+
logger = logging.getLogger("sqlite_vec_client")
11+
logger.setLevel(LOG_LEVEL)
12+
13+
# Only add handler if none exists (avoid duplicate handlers)
14+
if not logger.handlers:
15+
handler = logging.StreamHandler()
16+
formatter = logging.Formatter(
17+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
18+
)
19+
handler.setFormatter(formatter)
20+
logger.addHandler(handler)
21+
22+
23+
def get_logger() -> logging.Logger:
24+
"""Get the configured logger instance.
25+
26+
Returns:
27+
Logger instance for sqlite-vec-client
28+
"""
29+
return logger

tests/test_logger.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Tests for logging functionality."""
2+
3+
import logging
4+
5+
from sqlite_vec_client import get_logger
6+
7+
8+
class TestLogger:
9+
"""Test logging configuration."""
10+
11+
def test_get_logger_returns_logger(self):
12+
"""Test that get_logger returns a Logger instance."""
13+
logger = get_logger()
14+
assert isinstance(logger, logging.Logger)
15+
assert logger.name == "sqlite_vec_client"
16+
17+
def test_logger_has_handler(self):
18+
"""Test that logger has at least one handler."""
19+
logger = get_logger()
20+
assert len(logger.handlers) > 0
21+
22+
def test_logger_default_level(self):
23+
"""Test that logger respects environment variable."""
24+
logger = get_logger()
25+
# Default is WARNING if not set
26+
assert logger.level in [
27+
logging.WARNING,
28+
logging.DEBUG,
29+
logging.INFO,
30+
logging.ERROR,
31+
logging.CRITICAL,
32+
]
33+
34+
def test_logger_level_can_be_changed(self):
35+
"""Test that logger level can be changed programmatically."""
36+
logger = get_logger()
37+
original_level = logger.level
38+
39+
logger.setLevel(logging.DEBUG)
40+
assert logger.level == logging.DEBUG
41+
42+
logger.setLevel(logging.INFO)
43+
assert logger.level == logging.INFO
44+
45+
# Restore original level
46+
logger.setLevel(original_level)

0 commit comments

Comments
 (0)