Skip to content

Commit 0830453

Browse files
authored
Merge pull request #122 from graphsense/feature/tag_access_count
Feature/tag access count
2 parents 34c0bcc + aca7119 commit 0830453

File tree

15 files changed

+4014
-3887
lines changed

15 files changed

+4014
-3887
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ all: format lint
22

33
-include .env
44

5-
GS_REST_SERVICE_VERSIONM ?= "25.11.4"
6-
GS_REST_SERVICE_VERSION ?= "1.15.4"
5+
GS_REST_SERVICE_VERSIONM ?= "25.12.0rc2"
6+
GS_REST_SERVICE_VERSION ?= "1.16.0rc2"
77

88
GS_REST_DEV_PORT ?= 9000
99
NUM_WORKERS ?= 1

clients/python/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ GraphSense API provides programmatic access to various ledgers' addresses, entit
33

44
This Python package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
55

6-
- API version: 1.15.4
7-
- Package version: 1.15.4
6+
- API version: 1.16.0rc2
7+
- Package version: 1.16.0rc2
88
- Build package: org.openapitools.codegen.languages.PythonClientCodegen
99

1010
## Requirements.

clients/python/graphsense/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"""
1111

1212

13-
__version__ = "1.15.4"
13+
__version__ = "1.16.0rc2"
1414

1515
# import ApiClient
1616
from graphsense.api_client import ApiClient

clients/python/graphsense/api_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def __init__(self, configuration=None, header_name=None, header_value=None,
7676
self.default_headers[header_name] = header_value
7777
self.cookie = cookie
7878
# Set default User-Agent.
79-
self.user_agent = 'OpenAPI-Generator/1.15.4/python'
79+
self.user_agent = 'OpenAPI-Generator/1.16.0rc2/python'
8080

8181
def __enter__(self):
8282
return self

clients/python/graphsense/configuration.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -405,8 +405,8 @@ def to_debug_report(self):
405405
return "Python SDK Debug Report:\n"\
406406
"OS: {env}\n"\
407407
"Python Version: {pyversion}\n"\
408-
"Version of the API: 1.15.4\n"\
409-
"SDK Package Version: 1.15.4".\
408+
"Version of the API: 1.16.0rc2\n"\
409+
"SDK Package Version: 1.16.0rc2".\
410410
format(env=sys.platform, pyversion=sys.version)
411411

412412
def get_host_settings(self):

clients/python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "graphsense-python"
3-
version = "1.15.4"
3+
version = "1.16.0rc2"
44
description = "GraphSense API"
55
readme = { file = "README.md", content-type = "text/markdown; charset=UTF-8; variant=GFM" }
66
requires-python = ">=3.6"

gsrest/__init__.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,28 @@ def factory_internal(
145145

146146
# Initialize service container after db connection is established
147147
async def setup_services(app):
148+
config = app["config"]
149+
if config.tag_access_logger and config.tag_access_logger.enabled:
150+
app.logger.info("Tag access logging is enabled.")
151+
from redis import asyncio as aioredis
152+
153+
redis_url = config.tag_access_logger.redis_url or "redis://localhost"
154+
app.logger.info(
155+
f"Connecting to Redis at {redis_url} for tag access logging."
156+
)
157+
redis_client = await aioredis.from_url(redis_url)
158+
log_tag_access_prefix = config.tag_access_logger.prefix
159+
else:
160+
redis_client = None
161+
log_tag_access_prefix = None
148162
app["services"] = ServiceContainer(
149-
config=app["config"],
163+
config=config,
150164
db=app["db"],
151165
tagstore_engine=app["gs-tagstore"],
152166
concepts_cache_service=ConceptsCacheService(app, app.logger),
153167
logger=app.logger,
168+
redis_client=redis_client,
169+
log_tag_access_prefix=log_tag_access_prefix,
154170
)
155171
yield
156172

gsrest/builtin/plugins/obfuscate_tags/obfuscate_tags.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import re
2+
from functools import partial
23

34
from aiohttp import web
45
from graphsenselib.tagstore.algorithms.obfuscate import (
@@ -18,7 +19,6 @@
1819
from openapi_server.models.search_result_level4 import SearchResultLevel4
1920
from openapi_server.models.search_result_level5 import SearchResultLevel5
2021
from openapi_server.models.search_result_level6 import SearchResultLevel6
21-
from functools import partial
2222

2323
GROUPS_HEADER_NAME = "X-Consumer-Groups"
2424
NO_OBFUSCATION_MARKER_PATTERN = re.compile(r"(private|tags-private)")

gsrest/config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ class LoggingConfig(BaseSettings):
2929
)
3030

3131

32+
class TagAccessLoggerConfig(BaseSettings):
33+
enabled: bool = Field(default=False, description="Enable tag access logging")
34+
prefix: str = Field(
35+
default="tag_access",
36+
description="Prefix for Redis keys used in tag access logging",
37+
)
38+
redis_url: Optional[str] = Field(
39+
default=None, description="Redis URL for tag access logging"
40+
)
41+
42+
3243
class GSRestConfig(BaseSettings):
3344
model_config = ConfigDict(env_prefix="GSREST_", case_sensitive=False, extra="allow")
3445

@@ -92,6 +103,10 @@ class GSRestConfig(BaseSettings):
92103
default_factory=dict, description="Slack info hook"
93104
)
94105

106+
tag_access_logger: Optional[TagAccessLoggerConfig] = Field(
107+
default=None, description="Tag access logger configuration"
108+
)
109+
95110
def get_plugin_config(self, plugin_name: str) -> Optional[Dict[str, Any]]:
96111
"""Get configuration for a specific plugin"""
97112
return getattr(self, plugin_name, None)

gsrest/dependencies.py

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Any, Optional
1+
import time
2+
from typing import Any, Optional, Tuple
23

34
from graphsenselib.db.asynchronous.services.addresses_service import AddressesService
45
from graphsenselib.db.asynchronous.services.blocks_service import BlocksService
@@ -13,6 +14,7 @@
1314
from graphsenselib.db.asynchronous.services.tokens_service import TokensService
1415
from graphsenselib.db.asynchronous.services.txs_service import TxsService
1516
from graphsenselib.tagstore.db import TagstoreDbAsync, Taxonomies
17+
from graphsenselib.tagstore.db.queries import TagPublic
1618

1719
from gsrest.builtin.plugins.obfuscate_tags.obfuscate_tags import (
1820
GROUPS_HEADER_NAME,
@@ -47,6 +49,78 @@ async def setup_cache(cls, db_engine: Any, app: Any):
4749
}
4850

4951

52+
class TagAccessLoggerTagstoreProxy:
53+
"""Adds logging for which tags are accessed from the tagstore
54+
it intercepts calls to the tagstore DB
55+
and logs returned tags to redis.
56+
"""
57+
58+
def __init__(
59+
self, tagstore_db: TagstoreDbAsync, redis_client: Any, key_prefix: str
60+
):
61+
self.tagstore_db = tagstore_db
62+
self.redis_client = redis_client
63+
self.key_prefix = key_prefix
64+
65+
def __getattr__(self, name):
66+
"""Proxy all method calls to the underlying tagstore_db"""
67+
attr = getattr(self.tagstore_db, name)
68+
69+
if callable(attr):
70+
71+
async def wrapper(*args, **kwargs):
72+
# Call the original method
73+
result = await attr(*args, **kwargs)
74+
75+
# Log tag access if this method returns TagPublic objects
76+
should_log, is_list = self._should_log_result(result)
77+
if self.redis_client and should_log:
78+
if is_list:
79+
for tag in result:
80+
await self._log_tag_access(name, tag, *args, **kwargs)
81+
else:
82+
await self._log_tag_access(name, result, *args, **kwargs)
83+
84+
return result
85+
86+
return wrapper
87+
else:
88+
return attr
89+
90+
def _should_log_result(self, result: Any) -> Tuple[bool, bool]:
91+
"""Determine if this result should be logged based on data type"""
92+
93+
if not result:
94+
return False, False
95+
96+
# Check if result is a PublicTag
97+
if isinstance(result, TagPublic):
98+
return True, False
99+
100+
# Check if result is a list of TagPublic objects
101+
if hasattr(result, "__iter__") and not isinstance(result, str):
102+
try:
103+
# Check if all items in the iterable are TagPublic objects
104+
for item in result:
105+
if isinstance(item, TagPublic):
106+
return True, True
107+
break # Only check first item for performance
108+
except (TypeError, StopIteration):
109+
pass
110+
111+
return False, False
112+
113+
async def _log_tag_access(self, method_name: str, tag: TagPublic, *args, **kwargs):
114+
"""Log tag access information to Redis"""
115+
116+
current_time = time.localtime()
117+
timestamp = time.strftime("%Y-%m-%d", current_time)
118+
key = "|".join(
119+
(self.key_prefix, timestamp, tag.creator, tag.network, tag.identifier)
120+
)
121+
await self.redis_client.incr(key)
122+
123+
50124
class ServiceContainer:
51125
def __init__(
52126
self,
@@ -55,10 +129,17 @@ def __init__(
55129
tagstore_engine: any,
56130
concepts_cache_service: ConceptsCacheService,
57131
logger: any,
132+
redis_client: Optional[Any] = None,
133+
log_tag_access_prefix: Optional[str] = None,
58134
):
135+
tsdb = TagstoreDbAsync(tagstore_engine)
59136
self.config = config
60137
self.db = db
61-
self.tagstore_db = TagstoreDbAsync(tagstore_engine)
138+
self.tagstore_db = (
139+
TagAccessLoggerTagstoreProxy(tsdb, redis_client, log_tag_access_prefix)
140+
if log_tag_access_prefix
141+
else tsdb
142+
)
62143
self.logger = logger
63144
self.category_cache_service = concepts_cache_service
64145

0 commit comments

Comments
 (0)