Skip to content

Commit 38fbc3a

Browse files
committed
Json Exporter
1 parent b053276 commit 38fbc3a

12 files changed

Lines changed: 271 additions & 20 deletions

File tree

examples/async_example.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import asyncio
2-
import sys
32
import os
3+
import sys
4+
45
from pyquerytracker import TrackQuery
5-
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
6+
7+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
68

79

810
# Example 1: Basic async function

examples/core/async_usage.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import logging
3+
34
from pyquerytracker import TrackQuery, configure
45

56
# Configure logging to see the output

examples/exporter/csv_exporter_1.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import time
33

44
from pyquerytracker import TrackQuery
5-
from pyquerytracker.config import configure, ExportType
5+
from pyquerytracker.config import ExportType, configure
66

77
os.makedirs("logs-csv", exist_ok=True)
88

@@ -22,17 +22,13 @@ def process_data(x, y):
2222

2323
@TrackQuery()
2424
def failing_task():
25-
time.sleep(0.03)
25+
time.sleep(0.03) # not slow
2626
raise RuntimeError("This failed intentionally.")
2727

2828

29-
# Run
30-
print("Result:", process_data(5, 7))
31-
print("Result:", process_data(5, 7))
32-
print("Result:", process_data(5, 7))
33-
print("Result:", process_data(5, 7))
34-
print("Result:", process_data(5, 7))
29+
# Run tasks
3530
print("Result:", process_data(5, 7))
31+
print("Result:", process_data(1, 2))
3632

3733
try:
3834
failing_task()

examples/exporter/json_exporter.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import os
2+
import time
3+
4+
from pyquerytracker import TrackQuery
5+
from pyquerytracker.config import ExportType, configure
6+
7+
os.makedirs("logs-json", exist_ok=True)
8+
9+
configure(
10+
slow_log_threshold_ms=50.0,
11+
slow_log_level=20, # INFO
12+
export_type=ExportType.JSON,
13+
export_path="logs-json/query_logs.json",
14+
)
15+
16+
17+
@TrackQuery()
18+
def process_data(x, y):
19+
time.sleep(0.08) # triggers slow log
20+
return x + y
21+
22+
23+
@TrackQuery()
24+
def failing_task():
25+
time.sleep(0.03) # not slow
26+
raise RuntimeError("This failed intentionally.")
27+
28+
29+
# Run tasks
30+
print("Result:", process_data(5, 7))
31+
print("Result:", process_data(1, 2))
32+
33+
try:
34+
failing_task()
35+
except Exception:
36+
pass

pyquerytracker/exporter/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from abc import ABC, abstractmethod
2+
23
from pyquerytracker.config import Config
34

45

pyquerytracker/exporter/csv_exporter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import os
44
from threading import Lock
55

6-
from pyquerytracker.utils.logger import QueryLogger
76
from pyquerytracker.exporter.base import Exporter
7+
from pyquerytracker.utils.logger import QueryLogger
88

99
logger = QueryLogger.get_logger()
1010

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import atexit
2+
import json
3+
import os
4+
from threading import Lock
5+
6+
from pyquerytracker.exporter.base import Exporter
7+
from pyquerytracker.utils.logger import QueryLogger
8+
9+
logger = QueryLogger.get_logger()
10+
11+
12+
class JsonExporter(Exporter):
13+
def __init__(self, config):
14+
super().__init__(config)
15+
self.config = config
16+
self._lock = Lock()
17+
self._buffer = []
18+
atexit.register(self.flush)
19+
20+
def append(self, data: dict):
21+
with self._lock:
22+
self._buffer.append(data)
23+
24+
def flush(self):
25+
with self._lock:
26+
if not self._buffer:
27+
return
28+
29+
os.makedirs(os.path.dirname(self.config.export_path), exist_ok=True)
30+
31+
existing_data = []
32+
if os.path.exists(self.config.export_path):
33+
try:
34+
with open(self.config.export_path, "r", encoding="utf-8") as f:
35+
existing_data = json.load(f)
36+
if not isinstance(existing_data, list):
37+
existing_data = []
38+
except json.JSONDecodeError:
39+
pass
40+
41+
existing_data.extend(self._buffer)
42+
43+
with open(self.config.export_path, "w", encoding="utf-8") as f:
44+
json.dump(existing_data, f, indent=2)
45+
46+
logger.info(f"Flushed {len(self._buffer)} logs to JSON")
47+
self._buffer.clear()

pyquerytracker/exporter/manager.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
from pyquerytracker.config import Config, ExportType
2-
from pyquerytracker.exporter.csv_exporter import CsvExporter
32
from pyquerytracker.exporter.base import Exporter
3+
from pyquerytracker.exporter.csv_exporter import CsvExporter
4+
from pyquerytracker.exporter.json_exporter import JsonExporter
45

56

67
class ExporterManager:
78
_exporter: Exporter = None
89

9-
@staticmethod
10-
def create_exporter(config: Config) -> Exporter:
11-
if config.export_type == ExportType.CSV:
12-
exporter = CsvExporter(config)
13-
ExporterManager.set(exporter)
14-
return exporter
15-
raise ValueError("Unsupported export type")
10+
_exporter_classes = {
11+
ExportType.CSV: CsvExporter,
12+
ExportType.JSON: JsonExporter,
13+
}
14+
15+
@classmethod
16+
def create_exporter(cls, config: Config) -> Exporter:
17+
exporter_cls = cls._exporter_classes.get(config.export_type)
18+
if not exporter_cls:
19+
raise ValueError(f"Unsupported export type: {config.export_type}")
20+
exporter = exporter_cls(config)
21+
cls.set(exporter)
22+
return exporter
1623

1724
@staticmethod
1825
def set(exporter: Exporter):
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import json
2+
import os
3+
import subprocess
4+
import tempfile
5+
6+
7+
def run_test_in_subprocess(script: str, export_path: str):
8+
subprocess.run(["python3", "-c", script], check=True)
9+
assert os.path.exists(export_path)
10+
with open(export_path) as f:
11+
return json.load(f)
12+
13+
14+
def test_successful_function_logs():
15+
with tempfile.TemporaryDirectory() as tmpdir:
16+
export_path = os.path.join(tmpdir, "log.json")
17+
18+
script = f"""
19+
import time
20+
from pyquerytracker import TrackQuery
21+
from pyquerytracker.config import configure, ExportType
22+
23+
configure(
24+
slow_log_threshold_ms=0.0,
25+
slow_log_level=10,
26+
export_type=ExportType.JSON,
27+
export_path=r"{export_path}",
28+
)
29+
30+
@TrackQuery()
31+
def foo(x, y):
32+
return x + y
33+
34+
foo(1, 2)
35+
"""
36+
37+
logs = run_test_in_subprocess(script, export_path)
38+
39+
assert isinstance(logs, list)
40+
functions_logged = {log.get("function_name") for log in logs}
41+
assert "foo" in functions_logged
42+
assert any(log.get("event") == "success" for log in logs) or any(
43+
log.get("event") == "slow_execution" for log in logs
44+
)
45+
46+
47+
def test_error_function_logs():
48+
with tempfile.TemporaryDirectory() as tmpdir:
49+
export_path = os.path.join(tmpdir, "log.json")
50+
51+
script = f"""
52+
import time
53+
from pyquerytracker import TrackQuery
54+
from pyquerytracker.config import configure, ExportType
55+
56+
configure(
57+
slow_log_threshold_ms=0.0,
58+
slow_log_level=10,
59+
export_type=ExportType.JSON,
60+
export_path=r"{export_path}",
61+
)
62+
63+
@TrackQuery()
64+
def bar():
65+
raise RuntimeError("error")
66+
67+
try:
68+
bar()
69+
except RuntimeError:
70+
pass
71+
"""
72+
73+
logs = run_test_in_subprocess(script, export_path)
74+
75+
assert isinstance(logs, list)
76+
functions_logged = {log.get("function_name") for log in logs}
77+
assert "bar" in functions_logged
78+
assert any(log.get("event") == "error" for log in logs)
79+
80+
81+
def test_slow_function_logs():
82+
with tempfile.TemporaryDirectory() as tmpdir:
83+
export_path = os.path.join(tmpdir, "log.json")
84+
85+
script = f"""
86+
import time
87+
from pyquerytracker import TrackQuery
88+
from pyquerytracker.config import configure, ExportType
89+
90+
# Set threshold low so the sleep triggers slow log
91+
configure(
92+
slow_log_threshold_ms=5.0,
93+
slow_log_level=10,
94+
export_type=ExportType.JSON,
95+
export_path=r"{export_path}",
96+
)
97+
98+
@TrackQuery()
99+
def slow_func():
100+
time.sleep(0.01)
101+
return "done"
102+
103+
slow_func()
104+
"""
105+
106+
logs = run_test_in_subprocess(script, export_path)
107+
108+
assert isinstance(logs, list)
109+
functions_logged = {log.get("function_name") for log in logs}
110+
assert "slow_func" in functions_logged
111+
assert any(log.get("event") == "slow_execution" for log in logs)
112+
113+
114+
def test_multiple_calls_logged():
115+
with tempfile.TemporaryDirectory() as tmpdir:
116+
export_path = os.path.join(tmpdir, "log.json")
117+
118+
script = f"""
119+
import time
120+
from pyquerytracker import TrackQuery
121+
from pyquerytracker.config import configure, ExportType
122+
123+
configure(
124+
slow_log_threshold_ms=0.0,
125+
slow_log_level=10,
126+
export_type=ExportType.JSON,
127+
export_path=r"{export_path}",
128+
)
129+
130+
@TrackQuery()
131+
def a():
132+
return 1
133+
134+
@TrackQuery()
135+
def b():
136+
raise ValueError("fail")
137+
138+
a()
139+
try:
140+
b()
141+
except Exception:
142+
pass
143+
a()
144+
"""
145+
146+
logs = run_test_in_subprocess(script, export_path)
147+
148+
assert isinstance(logs, list)
149+
functions_logged = [log.get("function_name") for log in logs]
150+
151+
# Because of overwrite behavior, logs might only contain last flush,
152+
# but often logs include multiple entries. So test at least one a, b occurrence:
153+
assert "a" in functions_logged or "b" in functions_logged
154+
# Check for presence of success and error events in the batch
155+
events = {log.get("event") for log in logs}
156+
assert "error" in events or "success" in events or "slow_execution" in events

tests/test_async_core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import asyncio
22
import logging
3+
34
import pytest
5+
46
from pyquerytracker import TrackQuery, configure
57

68
# Mark all tests in this file as asyncio

0 commit comments

Comments
 (0)