django-nativemojo 0.1.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_nativemojo-0.1.10.dist-info/LICENSE +19 -0
- django_nativemojo-0.1.10.dist-info/METADATA +96 -0
- django_nativemojo-0.1.10.dist-info/NOTICE +8 -0
- django_nativemojo-0.1.10.dist-info/RECORD +194 -0
- django_nativemojo-0.1.10.dist-info/WHEEL +4 -0
- mojo/__init__.py +3 -0
- mojo/apps/account/__init__.py +1 -0
- mojo/apps/account/admin.py +91 -0
- mojo/apps/account/apps.py +16 -0
- mojo/apps/account/migrations/0001_initial.py +77 -0
- mojo/apps/account/migrations/0002_user_is_email_verified_user_is_phone_verified.py +23 -0
- mojo/apps/account/migrations/0003_group_mojo_secrets_user_mojo_secrets.py +23 -0
- mojo/apps/account/migrations/__init__.py +0 -0
- mojo/apps/account/models/__init__.py +3 -0
- mojo/apps/account/models/group.py +98 -0
- mojo/apps/account/models/member.py +95 -0
- mojo/apps/account/models/pkey.py +18 -0
- mojo/apps/account/models/user.py +211 -0
- mojo/apps/account/rest/__init__.py +3 -0
- mojo/apps/account/rest/group.py +25 -0
- mojo/apps/account/rest/user.py +47 -0
- mojo/apps/account/utils/__init__.py +0 -0
- mojo/apps/account/utils/jwtoken.py +72 -0
- mojo/apps/account/utils/passkeys.py +54 -0
- mojo/apps/fileman/README.md +549 -0
- mojo/apps/fileman/__init__.py +0 -0
- mojo/apps/fileman/apps.py +15 -0
- mojo/apps/fileman/backends/__init__.py +117 -0
- mojo/apps/fileman/backends/base.py +319 -0
- mojo/apps/fileman/backends/filesystem.py +397 -0
- mojo/apps/fileman/backends/s3.py +398 -0
- mojo/apps/fileman/examples/configurations.py +378 -0
- mojo/apps/fileman/examples/usage_example.py +665 -0
- mojo/apps/fileman/management/__init__.py +1 -0
- mojo/apps/fileman/management/commands/__init__.py +1 -0
- mojo/apps/fileman/management/commands/cleanup_expired_uploads.py +222 -0
- mojo/apps/fileman/models/__init__.py +7 -0
- mojo/apps/fileman/models/file.py +292 -0
- mojo/apps/fileman/models/manager.py +227 -0
- mojo/apps/fileman/models/render.py +0 -0
- mojo/apps/fileman/rest/__init__ +0 -0
- mojo/apps/fileman/rest/__init__.py +23 -0
- mojo/apps/fileman/rest/fileman.py +13 -0
- mojo/apps/fileman/rest/upload.py +92 -0
- mojo/apps/fileman/utils/__init__.py +19 -0
- mojo/apps/fileman/utils/upload.py +616 -0
- mojo/apps/incident/__init__.py +1 -0
- mojo/apps/incident/handlers/__init__.py +3 -0
- mojo/apps/incident/handlers/event_handlers.py +142 -0
- mojo/apps/incident/migrations/0001_initial.py +83 -0
- mojo/apps/incident/migrations/0002_rename_bundle_ruleset_bundle_minutes_event_hostname_and_more.py +44 -0
- mojo/apps/incident/migrations/0003_alter_event_model_id.py +18 -0
- mojo/apps/incident/migrations/0004_alter_incident_model_id.py +18 -0
- mojo/apps/incident/migrations/__init__.py +0 -0
- mojo/apps/incident/models/__init__.py +3 -0
- mojo/apps/incident/models/event.py +135 -0
- mojo/apps/incident/models/incident.py +33 -0
- mojo/apps/incident/models/rule.py +247 -0
- mojo/apps/incident/parsers/__init__.py +0 -0
- mojo/apps/incident/parsers/ossec/__init__.py +1 -0
- mojo/apps/incident/parsers/ossec/core.py +82 -0
- mojo/apps/incident/parsers/ossec/parsed.py +23 -0
- mojo/apps/incident/parsers/ossec/rules.py +124 -0
- mojo/apps/incident/parsers/ossec/utils.py +169 -0
- mojo/apps/incident/reporter.py +42 -0
- mojo/apps/incident/rest/__init__.py +2 -0
- mojo/apps/incident/rest/event.py +23 -0
- mojo/apps/incident/rest/ossec.py +22 -0
- mojo/apps/logit/__init__.py +0 -0
- mojo/apps/logit/admin.py +37 -0
- mojo/apps/logit/migrations/0001_initial.py +32 -0
- mojo/apps/logit/migrations/0002_log_duid_log_payload_log_username.py +28 -0
- mojo/apps/logit/migrations/0003_log_level.py +18 -0
- mojo/apps/logit/migrations/__init__.py +0 -0
- mojo/apps/logit/models/__init__.py +1 -0
- mojo/apps/logit/models/log.py +57 -0
- mojo/apps/logit/rest.py +9 -0
- mojo/apps/metrics/README.md +79 -0
- mojo/apps/metrics/__init__.py +12 -0
- mojo/apps/metrics/redis_metrics.py +331 -0
- mojo/apps/metrics/rest/__init__.py +1 -0
- mojo/apps/metrics/rest/base.py +152 -0
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/metrics/utils.py +227 -0
- mojo/apps/notify/README.md +91 -0
- mojo/apps/notify/README_NOTIFICATIONS.md +566 -0
- mojo/apps/notify/__init__.py +0 -0
- mojo/apps/notify/admin.py +52 -0
- mojo/apps/notify/handlers/__init__.py +0 -0
- mojo/apps/notify/handlers/example_handlers.py +516 -0
- mojo/apps/notify/handlers/ses/__init__.py +25 -0
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +25 -0
- mojo/apps/notify/handlers/ses/message.py +86 -0
- mojo/apps/notify/management/__init__.py +0 -0
- mojo/apps/notify/management/commands/__init__.py +1 -0
- mojo/apps/notify/management/commands/process_notifications.py +370 -0
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +12 -0
- mojo/apps/notify/models/account.py +128 -0
- mojo/apps/notify/models/attachment.py +24 -0
- mojo/apps/notify/models/bounce.py +68 -0
- mojo/apps/notify/models/complaint.py +40 -0
- mojo/apps/notify/models/inbox.py +113 -0
- mojo/apps/notify/models/inbox_message.py +173 -0
- mojo/apps/notify/models/outbox.py +129 -0
- mojo/apps/notify/models/outbox_message.py +288 -0
- mojo/apps/notify/models/template.py +30 -0
- mojo/apps/notify/providers/__init__.py +0 -0
- mojo/apps/notify/providers/aws.py +73 -0
- mojo/apps/notify/rest/__init__.py +0 -0
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +2 -0
- mojo/apps/notify/utils/notifications.py +404 -0
- mojo/apps/notify/utils/parsing.py +202 -0
- mojo/apps/notify/utils/render.py +144 -0
- mojo/apps/tasks/README.md +118 -0
- mojo/apps/tasks/__init__.py +11 -0
- mojo/apps/tasks/manager.py +489 -0
- mojo/apps/tasks/rest/__init__.py +2 -0
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +62 -0
- mojo/apps/tasks/runner.py +174 -0
- mojo/apps/tasks/tq_handlers.py +14 -0
- mojo/decorators/__init__.py +3 -0
- mojo/decorators/auth.py +25 -0
- mojo/decorators/cron.py +31 -0
- mojo/decorators/http.py +132 -0
- mojo/decorators/validate.py +14 -0
- mojo/errors.py +88 -0
- mojo/helpers/__init__.py +0 -0
- mojo/helpers/aws/__init__.py +0 -0
- mojo/helpers/aws/client.py +8 -0
- mojo/helpers/aws/s3.py +268 -0
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/cron.py +79 -0
- mojo/helpers/crypto/__init__.py +4 -0
- mojo/helpers/crypto/aes.py +60 -0
- mojo/helpers/crypto/hash.py +59 -0
- mojo/helpers/crypto/privpub/__init__.py +1 -0
- mojo/helpers/crypto/privpub/hybrid.py +97 -0
- mojo/helpers/crypto/privpub/rsa.py +104 -0
- mojo/helpers/crypto/sign.py +36 -0
- mojo/helpers/crypto/too.l.py +25 -0
- mojo/helpers/crypto/utils.py +26 -0
- mojo/helpers/daemon.py +94 -0
- mojo/helpers/dates.py +69 -0
- mojo/helpers/dns/__init__.py +0 -0
- mojo/helpers/dns/godaddy.py +62 -0
- mojo/helpers/filetypes.py +128 -0
- mojo/helpers/logit.py +310 -0
- mojo/helpers/modules.py +95 -0
- mojo/helpers/paths.py +63 -0
- mojo/helpers/redis.py +10 -0
- mojo/helpers/request.py +89 -0
- mojo/helpers/request_parser.py +269 -0
- mojo/helpers/response.py +14 -0
- mojo/helpers/settings.py +146 -0
- mojo/helpers/sysinfo.py +140 -0
- mojo/helpers/ua.py +0 -0
- mojo/middleware/__init__.py +0 -0
- mojo/middleware/auth.py +26 -0
- mojo/middleware/logging.py +55 -0
- mojo/middleware/mojo.py +21 -0
- mojo/migrations/0001_initial.py +32 -0
- mojo/migrations/__init__.py +0 -0
- mojo/models/__init__.py +2 -0
- mojo/models/meta.py +262 -0
- mojo/models/rest.py +538 -0
- mojo/models/secrets.py +59 -0
- mojo/rest/__init__.py +1 -0
- mojo/rest/info.py +26 -0
- mojo/serializers/__init__.py +0 -0
- mojo/serializers/models.py +165 -0
- mojo/serializers/openapi.py +188 -0
- mojo/urls.py +38 -0
- mojo/ws4redis/README.md +174 -0
- mojo/ws4redis/__init__.py +2 -0
- mojo/ws4redis/client.py +283 -0
- mojo/ws4redis/connection.py +327 -0
- mojo/ws4redis/exceptions.py +32 -0
- mojo/ws4redis/redis.py +183 -0
- mojo/ws4redis/servers/__init__.py +0 -0
- mojo/ws4redis/servers/base.py +86 -0
- mojo/ws4redis/servers/django.py +171 -0
- mojo/ws4redis/servers/uwsgi.py +63 -0
- mojo/ws4redis/settings.py +45 -0
- mojo/ws4redis/utf8validator.py +128 -0
- mojo/ws4redis/websocket.py +403 -0
- testit/__init__.py +0 -0
- testit/client.py +147 -0
- testit/faker.py +20 -0
- testit/helpers.py +198 -0
- testit/runner.py +262 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
from mojo import decorators as md
|
2
|
+
from mojo.apps.incident.models import Incident, Event, RuleSet, Rule
|
3
|
+
|
4
|
+
|
5
|
+
@md.URL('incident')
|
6
|
+
@md.URL('incident/<int:pk>')
|
7
|
+
def on_incident(request, pk=None):
|
8
|
+
return Incident.on_rest_request(request, pk)
|
9
|
+
|
10
|
+
@md.URL('event')
|
11
|
+
@md.URL('event/<int:pk>')
|
12
|
+
def on_event(request, pk=None):
|
13
|
+
return Event.on_rest_request(request, pk)
|
14
|
+
|
15
|
+
@md.URL('event/ruleset')
|
16
|
+
@md.URL('event/ruleset/<int:pk>')
|
17
|
+
def on_event_ruleset(request, pk=None):
|
18
|
+
return RuleSet.on_rest_request(request, pk)
|
19
|
+
|
20
|
+
@md.URL('event/ruleset/rule')
|
21
|
+
@md.URL('event/ruleset/rule/<int:pk>')
|
22
|
+
def on_event_ruleset_rule(request, pk=None):
|
23
|
+
return Rule.on_rest_request(request, pk)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from mojo import decorators as md
|
2
|
+
from mojo.apps.incident.parsers import ossec
|
3
|
+
from mojo import JsonResponse
|
4
|
+
from mojo.apps.incident import reporter
|
5
|
+
|
6
|
+
|
7
|
+
@md.POST('ossec/alert')
|
8
|
+
def on_ossec_alert(request):
|
9
|
+
ossec_alert = ossec.parse(request.DATA)
|
10
|
+
# add the request ip
|
11
|
+
ossec_alert["request_ip"] = request.ip
|
12
|
+
reporter.report_event(ossec_alert.text, category="ossec", **ossec_alert)
|
13
|
+
return JsonResponse({"status": True})
|
14
|
+
|
15
|
+
|
16
|
+
@md.POST('ossec/alert/batch')
|
17
|
+
def on_ossec_alert_batch(request):
|
18
|
+
ossec_alerts = ossec.parse(request.DATA)
|
19
|
+
for alert in ossec_alerts:
|
20
|
+
alert["request_ip"] = request.ip
|
21
|
+
reporter.report_event(alert.text, category="ossec", **alert)
|
22
|
+
return JsonResponse({"status": True})
|
File without changes
|
mojo/apps/logit/admin.py
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
from mojo.apps.logit.models import Log
|
2
|
+
import json
|
3
|
+
from django.utils.safestring import mark_safe
|
4
|
+
|
5
|
+
from django.contrib import admin
|
6
|
+
|
7
|
+
# from django.contrib.sites.models import Site
|
8
|
+
|
9
|
+
# Unregister Group and Site from admin
|
10
|
+
# admin.site.unregister(Group)
|
11
|
+
# admin.site.unregister(Site)
|
12
|
+
|
13
|
+
@admin.register(Log)
|
14
|
+
class LogAdmin(admin.ModelAdmin):
|
15
|
+
exclude = ("log",)
|
16
|
+
list_display = ("created", "kind", "method", "path", "ip", "uid", "model_name", "model_id", "log_summary")
|
17
|
+
list_filter = ("kind", "ip", "uid", "model_name", "model_id", "created")
|
18
|
+
search_fields = ("log", "path", "ip", "model_name")
|
19
|
+
readonly_fields = ("created", "kind", "path", "ip", "uid", "model_name", "model_id", "user_agent", "method")
|
20
|
+
date_hierarchy = "created"
|
21
|
+
ordering = ("-created",)
|
22
|
+
|
23
|
+
def formatted_log(self, obj):
|
24
|
+
"""Attempt to format the log as JSON if possible, otherwise return raw log."""
|
25
|
+
if obj.log:
|
26
|
+
try:
|
27
|
+
json_data = json.loads(obj.log) # Try parsing as JSON
|
28
|
+
formatted_json = json.dumps(json_data, indent=4, sort_keys=True)
|
29
|
+
return mark_safe(f"<pre>{formatted_json}</pre>") # Wrap in <pre> for better formatting
|
30
|
+
except json.JSONDecodeError:
|
31
|
+
return obj.log # Return raw log if not JSON
|
32
|
+
return None
|
33
|
+
|
34
|
+
def log_summary(self, obj):
|
35
|
+
"""Show a shortened version of the log for better readability."""
|
36
|
+
return obj.log[:75] + "..." if obj.log and len(obj.log) > 75 else obj.log
|
37
|
+
log_summary.short_description = "Log Preview"
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-05-29 23:34
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
import mojo.models.rest
|
5
|
+
|
6
|
+
|
7
|
+
class Migration(migrations.Migration):
|
8
|
+
|
9
|
+
initial = True
|
10
|
+
|
11
|
+
dependencies = [
|
12
|
+
]
|
13
|
+
|
14
|
+
operations = [
|
15
|
+
migrations.CreateModel(
|
16
|
+
name='Log',
|
17
|
+
fields=[
|
18
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
19
|
+
('created', models.DateTimeField(auto_now_add=True, db_index=True)),
|
20
|
+
('kind', models.CharField(db_index=True, default=None, max_length=200, null=True)),
|
21
|
+
('method', models.CharField(default=None, max_length=200, null=True)),
|
22
|
+
('path', models.TextField(db_index=True, default=None, null=True)),
|
23
|
+
('ip', models.CharField(db_index=True, default=None, max_length=32, null=True)),
|
24
|
+
('uid', models.IntegerField(db_index=True, default=0)),
|
25
|
+
('user_agent', models.TextField(default=None, null=True)),
|
26
|
+
('log', models.TextField(default=None, null=True)),
|
27
|
+
('model_name', models.TextField(db_index=True, default=None, null=True)),
|
28
|
+
('model_id', models.IntegerField(db_index=True, default=0)),
|
29
|
+
],
|
30
|
+
bases=(models.Model, mojo.models.rest.MojoModel),
|
31
|
+
),
|
32
|
+
]
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-05-31 23:05
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('logit', '0001_initial'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.AddField(
|
14
|
+
model_name='log',
|
15
|
+
name='duid',
|
16
|
+
field=models.TextField(default=None, null=True),
|
17
|
+
),
|
18
|
+
migrations.AddField(
|
19
|
+
model_name='log',
|
20
|
+
name='payload',
|
21
|
+
field=models.TextField(default=None, null=True),
|
22
|
+
),
|
23
|
+
migrations.AddField(
|
24
|
+
model_name='log',
|
25
|
+
name='username',
|
26
|
+
field=models.TextField(default=None, null=True),
|
27
|
+
),
|
28
|
+
]
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-05-31 23:48
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('logit', '0002_log_duid_log_payload_log_username'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.AddField(
|
14
|
+
model_name='log',
|
15
|
+
name='level',
|
16
|
+
field=models.CharField(db_index=True, default='low', max_length=12),
|
17
|
+
),
|
18
|
+
]
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
from .log import *
|
@@ -0,0 +1,57 @@
|
|
1
|
+
from mojo.models import MojoModel
|
2
|
+
from django.db import models as dm
|
3
|
+
from mojo.helpers import logit
|
4
|
+
# logger = logit.get_logger("requests", "requests.log")
|
5
|
+
|
6
|
+
|
7
|
+
class Log(dm.Model, MojoModel):
|
8
|
+
created = dm.DateTimeField(auto_now_add=True, db_index=True)
|
9
|
+
level = dm.CharField(max_length=12, default="info", db_index=True)
|
10
|
+
kind = dm.CharField(max_length=200, default=None, null=True, db_index=True)
|
11
|
+
method = dm.CharField(max_length=200, default=None, null=True)
|
12
|
+
path = dm.TextField(default=None, null=True, db_index=True)
|
13
|
+
payload = dm.TextField(default=None, null=True)
|
14
|
+
ip = dm.CharField(max_length=32, default=None, null=True, db_index=True)
|
15
|
+
duid = dm.TextField(default=None, null=True)
|
16
|
+
uid = dm.IntegerField(default=0, db_index=True)
|
17
|
+
username = dm.TextField(default=None, null=True)
|
18
|
+
user_agent = dm.TextField(default=None, null=True)
|
19
|
+
log = dm.TextField(default=None, null=True)
|
20
|
+
model_name = dm.TextField(default=None, null=True, db_index=True)
|
21
|
+
model_id = dm.IntegerField(default=0, db_index=True)
|
22
|
+
# expires = dm.DateTimeField(db_index=True)
|
23
|
+
|
24
|
+
@classmethod
|
25
|
+
def logit(cls, request, log, kind="log", model_name=None, model_id=0, level="info", **kwargs):
|
26
|
+
if not isinstance(log, (bytes, str)):
|
27
|
+
log = f"INVALID LOG TYPE: attempting to log type: {type(log)}"
|
28
|
+
log = log.decode("utf-8") if isinstance(log, bytes) else log
|
29
|
+
log = logit.mask_sensitive_data(log)
|
30
|
+
|
31
|
+
uid, username, ip_address, path, method, duid = 0, None, None, None, None, None
|
32
|
+
if request:
|
33
|
+
username = request.user.username if request.user.is_authenticated else None
|
34
|
+
uid = request.user.pk if request.user.is_authenticated else 0
|
35
|
+
path = request.path
|
36
|
+
duid = request.duid
|
37
|
+
ip_address = request.ip
|
38
|
+
method = request.method
|
39
|
+
|
40
|
+
path = kwargs.get("path", path)
|
41
|
+
method = kwargs.get("method", method)
|
42
|
+
duid = kwargs.get("duid", duid)
|
43
|
+
|
44
|
+
return cls.objects.create(
|
45
|
+
level=level,
|
46
|
+
kind=kind,
|
47
|
+
method=method,
|
48
|
+
path=path,
|
49
|
+
ip=ip_address,
|
50
|
+
uid=uid,
|
51
|
+
duid=duid,
|
52
|
+
username=username,
|
53
|
+
log=log,
|
54
|
+
user_agent=request.user_agent,
|
55
|
+
model_name=model_name,
|
56
|
+
model_id=model_id
|
57
|
+
)
|
mojo/apps/logit/rest.py
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
# Redis Metrics
|
2
|
+
|
3
|
+
The `redis_metrics` module is designed to record and retrieve metrics using Redis as the data store. This package helps in maintaining metrics at various time granularities (e.g., hours, days, months, etc.) and organizing them into categories for better management.
|
4
|
+
|
5
|
+
Ensure that Redis is running, and your Django project is configured to connect to it.
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
### Recording Metrics
|
10
|
+
|
11
|
+
The `record_metrics` function is used to record events or occurrences by incrementing counters in Redis.
|
12
|
+
|
13
|
+
#### Parameters
|
14
|
+
|
15
|
+
- **slug (str)**: The base identifier for the metric.
|
16
|
+
- **when (datetime)**: The timestamp when the event occurred.
|
17
|
+
- **count (int, optional)**: The number by which to increment the metric. Default is 0.
|
18
|
+
- **group (optional)**: Reserved for future use.
|
19
|
+
- **category (optional)**: Used to group slugs into categories.
|
20
|
+
- **min_granulariy (str, optional)**: The smallest time unit for metric tracking (e.g., "hours"). Default is "hours".
|
21
|
+
- **max_granularity (str, optional)**: The largest time unit for metric tracking (e.g., "years"). Default is "years".
|
22
|
+
- **args (*args)**: Additional arguments used in slug generation.
|
23
|
+
|
24
|
+
#### Example
|
25
|
+
|
26
|
+
```python
|
27
|
+
from datetime import datetime
|
28
|
+
from metrics.redis_metrics import record_metrics
|
29
|
+
|
30
|
+
# Record a metric 'page_views' for current time, incremented by 1.
|
31
|
+
record_metrics(slug="page_views", when=datetime.now(), count=1)
|
32
|
+
|
33
|
+
# Record a metric 'user_signups' with category 'activity', incremented by 5 at specific time.
|
34
|
+
record_metrics(slug="user_signups", when=datetime(2023,10,10), count=5, category="activity")
|
35
|
+
|
36
|
+
# Record metrics with different granularities
|
37
|
+
record_metrics(slug="app_usage", when=datetime.now(), count=10, min_granulariy="minutes", max_granularity="days")
|
38
|
+
```
|
39
|
+
|
40
|
+
### Retrieving Metrics
|
41
|
+
|
42
|
+
Retrieve the recorded metrics using a variety of retrieval methods provided by the module.
|
43
|
+
|
44
|
+
#### Get Metrics from Slug
|
45
|
+
|
46
|
+
Fetch the metrics for a specific slug between two datetime ranges at a given granularity.
|
47
|
+
|
48
|
+
#### Example
|
49
|
+
|
50
|
+
```python
|
51
|
+
from datetime import datetime
|
52
|
+
from metrics.redis_metrics import get_metrics
|
53
|
+
|
54
|
+
# Fetch 'page_views' metrics from October 1, 2023, to October 10, 2023, with hourly granularity.
|
55
|
+
metrics = get_metrics(slug="page_views", dt_start=datetime(2023, 10, 1), dt_end=datetime(2023, 10, 10), granularity="hours")
|
56
|
+
print(metrics)
|
57
|
+
```
|
58
|
+
|
59
|
+
### Categories
|
60
|
+
|
61
|
+
You can organize your metrics into categories for more structured management.
|
62
|
+
|
63
|
+
#### Example
|
64
|
+
|
65
|
+
```python
|
66
|
+
from metrics.redis_metrics import get_category_slugs, get_categories
|
67
|
+
|
68
|
+
# Get all the slugs in the 'activity' category.
|
69
|
+
activity_slugs = get_category_slugs(category="activity")
|
70
|
+
print(activity_slugs)
|
71
|
+
|
72
|
+
# Get all categories.
|
73
|
+
categories = get_categories()
|
74
|
+
print(categories)
|
75
|
+
```
|
76
|
+
|
77
|
+
## Conclusion
|
78
|
+
|
79
|
+
Redis Metrics is a robust utility for tracking application-specific metrics over various time periods with ease. The flexibility provided through time granularity and categorization makes it ideal for applications requiring detailed metric tracking and analysis.
|
@@ -0,0 +1,331 @@
|
|
1
|
+
from . import utils
|
2
|
+
from mojo.helpers import redis, dates
|
3
|
+
from mojo.helpers.settings import settings
|
4
|
+
import datetime
|
5
|
+
from objict import objict, nobjict
|
6
|
+
|
7
|
+
|
8
|
+
def record(slug, when=None, count=1, category=None, account="global",
|
9
|
+
min_granularity="hours", max_granularity="years", timezone=None):
|
10
|
+
"""
|
11
|
+
Records metrics in Redis by incrementing counters for various time granularities.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
slug (str): The base identifier for the metric.
|
15
|
+
when (datetime, optional): The time at which the event occurred. Defaults to current time if not provided.
|
16
|
+
count (int, optional): The count to increment the metric by. Defaults to 1.
|
17
|
+
category (str, optional): The category to which the metric belongs. Useful for grouping similar metrics.
|
18
|
+
account (str, optional): The account under which the metric is recorded. Defaults to "global".
|
19
|
+
min_granularity (str, optional): The minimum time granularity (e.g., "hours"). Defaults to "hours".
|
20
|
+
max_granularity (str, optional): The maximum time granularity (e.g., "years"). Defaults to "years".
|
21
|
+
timezone (str, optional): The timezone to use as the base for the granularity calculations.
|
22
|
+
*kwargs: Additional arguments to be used in slug generation.
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
None: This function doesn't return a value. It performs its operations on Redis directly.
|
26
|
+
"""
|
27
|
+
when = utils.normalize_datetime(when, timezone)
|
28
|
+
# Get Redis connection
|
29
|
+
redis_conn = redis.get_connection()
|
30
|
+
pipeline = redis_conn.pipeline()
|
31
|
+
if category is not None:
|
32
|
+
add_category_slug(category, slug, pipeline, account)
|
33
|
+
add_metrics_slug(slug, pipeline, account)
|
34
|
+
# Generate granularities
|
35
|
+
granularities = utils.generate_granularities(min_granularity, max_granularity)
|
36
|
+
# Process each granularity
|
37
|
+
for granularity in granularities:
|
38
|
+
# Generate slug for the current granularity
|
39
|
+
generated_slug = utils.generate_slug(slug, when, granularity, account)
|
40
|
+
# Add count to the slug in Redis
|
41
|
+
pipeline.incr(generated_slug, count)
|
42
|
+
exp_at = utils.get_expires_at(granularity, slug, category)
|
43
|
+
if exp_at:
|
44
|
+
pipeline.expireat(generated_slug, exp_at)
|
45
|
+
pipeline.execute()
|
46
|
+
|
47
|
+
|
48
|
+
def fetch(slug, dt_start=None, dt_end=None, granularity="hours",
|
49
|
+
redis_con=None, account="global", with_labels=False, dr_slugs=None):
|
50
|
+
"""
|
51
|
+
Fetches metrics from Redis based on slugs within a specified date range and granularity.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
slug (str or list): The slug(s) identifying metrics to fetch.
|
55
|
+
dt_start (datetime, optional): The start of the date range. Defaults to None.
|
56
|
+
dt_end (datetime, optional): The end of the date range. Defaults to None.
|
57
|
+
granularity (str, optional): The time granularity to use. Defaults to "hours".
|
58
|
+
redis_con: The Redis connection instance (optional).
|
59
|
+
account (str, optional): The account under which the metric is recorded. Defaults to "global".
|
60
|
+
with_labels (bool, optional): If True, includes timestamp labels in response data. Defaults to False.
|
61
|
+
dr_slugs: Pre-generated slugs for the date range. Defaults to None.
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
list or nobjict: Returns a list of values or a structured nobjict with periods and data if `with_labels` is True.
|
65
|
+
"""
|
66
|
+
if redis_con is None:
|
67
|
+
redis_con = redis.get_connection()
|
68
|
+
if isinstance(slug, (list, set)):
|
69
|
+
resp = nobjict()
|
70
|
+
if with_labels:
|
71
|
+
resp.data = {}
|
72
|
+
resp.labels = utils.periods_from_dr_slugs(utils.generate_slugs_for_range(
|
73
|
+
slug[0], dt_start, dt_end, granularity, account))
|
74
|
+
for s in slug:
|
75
|
+
values = fetch(s, dt_start, dt_end, granularity, redis_con, account)
|
76
|
+
if with_labels:
|
77
|
+
resp.data[s] = values
|
78
|
+
else:
|
79
|
+
resp[s] = values
|
80
|
+
return resp
|
81
|
+
dr_slugs = utils.generate_slugs_for_range(slug, dt_start, dt_end, granularity, account)
|
82
|
+
values = [int(met) if met is not None else 0 for met in redis_con.mget(dr_slugs)]
|
83
|
+
if not with_labels:
|
84
|
+
return values
|
85
|
+
return nobjict(labels=utils.periods_from_dr_slugs(dr_slugs), data={slug: values})
|
86
|
+
|
87
|
+
def add_metrics_slug(slug, redis_con=None, account="global"):
|
88
|
+
"""
|
89
|
+
Adds a metric slug to a Redis set for the specified account.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
slug (str): The slug to add.
|
93
|
+
redis_con: The Redis connection instance (optional).
|
94
|
+
account (str): The account to which the slug should be added. Defaults to "global".
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
None
|
98
|
+
"""
|
99
|
+
if redis_con is None:
|
100
|
+
redis_con = redis.get_connection()
|
101
|
+
redis_con.sadd(utils.generate_slugs_key(account), slug)
|
102
|
+
|
103
|
+
|
104
|
+
def delete_metrics_slug(slug, account="global", redis_con=None):
|
105
|
+
"""
|
106
|
+
Deletes a specific slug from the Redis set for a given account.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
slug (str): The slug to delete.
|
110
|
+
redis_con: The Redis connection instance (optional).
|
111
|
+
account (str): The account from which the slug should be removed. Defaults to "global".
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
None
|
115
|
+
"""
|
116
|
+
if redis_con is None:
|
117
|
+
redis_con = redis.get_connection()
|
118
|
+
redis_con.srem(utils.generate_slugs_key(account), slug)
|
119
|
+
|
120
|
+
# now lets delete all keys with our slug prefix
|
121
|
+
prefix = utils.generate_slug_prefix(slug, account)
|
122
|
+
return __delete_keys_with_prefix(prefix, redis_con)
|
123
|
+
|
124
|
+
|
125
|
+
def __delete_keys_with_prefix(prefix, redis_conn):
|
126
|
+
cursor = b'0'
|
127
|
+
total_deleted = 0
|
128
|
+
while cursor != b'0':
|
129
|
+
cursor, keys = redis_conn.scan(cursor=cursor, match=f"{prefix}*")
|
130
|
+
if keys:
|
131
|
+
total_deleted += len(keys)
|
132
|
+
redis_conn.delete(*keys)
|
133
|
+
return total_deleted
|
134
|
+
|
135
|
+
def get_account_slugs(account, redis_con=None):
|
136
|
+
"""
|
137
|
+
Retrieves all slugs associated with a specific account from Redis.
|
138
|
+
|
139
|
+
Args:
|
140
|
+
account (str): The account for which to retrieve slugs.
|
141
|
+
redis_con: The Redis connection instance (optional).
|
142
|
+
|
143
|
+
Returns:
|
144
|
+
set: A set of decoded slugs belonging to the specified account.
|
145
|
+
"""
|
146
|
+
if redis_con is None:
|
147
|
+
redis_con = redis.get_connection()
|
148
|
+
return {s.decode() for s in redis_con.smembers(utils.generate_slugs_key(account))}
|
149
|
+
|
150
|
+
|
151
|
+
def add_category_slug(category, slug, redis_con=None, account="global"):
|
152
|
+
"""
|
153
|
+
Adds a slug to a category set in Redis and indexes the category.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
category (str): The category to which the slug should be added.
|
157
|
+
slug (str): The slug to add.
|
158
|
+
redis_con: The Redis connection instance (optional).
|
159
|
+
account (str): The account under which the category resides. Defaults to "global".
|
160
|
+
|
161
|
+
Returns:
|
162
|
+
None
|
163
|
+
"""
|
164
|
+
if redis_con is None:
|
165
|
+
redis_con = redis.get_connection()
|
166
|
+
redis_con.sadd(utils.generate_category_slug(account, category), slug)
|
167
|
+
redis_con.sadd(utils.generate_category_key(account), category)
|
168
|
+
|
169
|
+
|
170
|
+
def get_category_slugs(category, redis_con=None, account="global"):
|
171
|
+
"""
|
172
|
+
Retrieves all slugs associated with a specific category from Redis.
|
173
|
+
|
174
|
+
Args:
|
175
|
+
category (str): The category for which to retrieve slugs.
|
176
|
+
redis_con: The Redis connection instance (optional).
|
177
|
+
account (str): The account under which the category resides. Defaults to "global".
|
178
|
+
|
179
|
+
Returns:
|
180
|
+
set: A set of decoded slugs belonging to the specified category.
|
181
|
+
"""
|
182
|
+
if redis_con is None:
|
183
|
+
redis_con = redis.get_connection()
|
184
|
+
return {s.decode() for s in redis_con.smembers(utils.generate_category_slug(account, category))}
|
185
|
+
|
186
|
+
|
187
|
+
def delete_category(category, redis_con=None, account="global"):
|
188
|
+
"""
|
189
|
+
Deletes a specific category from Redis, including all associated slugs.
|
190
|
+
|
191
|
+
Args:
|
192
|
+
category (str): The category to delete.
|
193
|
+
redis_con: The Redis connection instance (optional).
|
194
|
+
account (str): The account under which the category resides. Defaults to "global".
|
195
|
+
|
196
|
+
Returns:
|
197
|
+
None
|
198
|
+
"""
|
199
|
+
if redis_con is None:
|
200
|
+
redis_con = redis.get_connection()
|
201
|
+
category_slug = utils.generate_category_slug(account, category)
|
202
|
+
pipeline = redis_con.pipeline()
|
203
|
+
pipeline.delete(category_slug) # Deletes the entire set
|
204
|
+
pipeline.srem(utils.generate_category_key(account), category) # Remove the category name from index
|
205
|
+
pipeline.execute()
|
206
|
+
|
207
|
+
|
208
|
+
def get_categories(redis_con=None, account="global"):
|
209
|
+
"""
|
210
|
+
Retrieves all categories for a specific account from Redis.
|
211
|
+
|
212
|
+
Args:
|
213
|
+
redis_con: The Redis connection instance (optional).
|
214
|
+
account (str): The account for which to retrieve categories. Defaults to "global".
|
215
|
+
|
216
|
+
Returns:
|
217
|
+
set: A set of decoded category names belonging to the specified account.
|
218
|
+
"""
|
219
|
+
if redis_con is None:
|
220
|
+
redis_con = redis.get_connection()
|
221
|
+
return {s.decode() for s in redis_con.smembers(utils.generate_category_key(account))}
|
222
|
+
|
223
|
+
|
224
|
+
def fetch_by_category(category, dt_start=None, dt_end=None, granularity="hours",
|
225
|
+
redis_con=None, account="global", with_labels=False):
|
226
|
+
"""
|
227
|
+
Fetches metrics for all slugs within a specified category, date range, and granularity.
|
228
|
+
|
229
|
+
Args:
|
230
|
+
category (str): The category for which to fetch metrics.
|
231
|
+
dt_start (datetime, optional): The start date for fetching metrics.
|
232
|
+
dt_end (datetime, optional): The end date for fetching metrics.
|
233
|
+
granularity (str, optional): The granularity of the metrics. Defaults to "hours".
|
234
|
+
redis_con: The Redis connection instance (optional).
|
235
|
+
account (str, optional): The account under which the category resides. Defaults to "global".
|
236
|
+
with_labels (bool, optional): If True, includes timestamp labels in response data. Defaults to False.
|
237
|
+
|
238
|
+
Returns:
|
239
|
+
list or nobjict: Fetches and returns metric data using the `fetch` function.
|
240
|
+
"""
|
241
|
+
return fetch(get_category_slugs(category, redis_con, account), with_labels=with_labels, account=account)
|
242
|
+
|
243
|
+
|
244
|
+
def set_view_perms(account, perms, redis_con=None):
|
245
|
+
"""
|
246
|
+
Sets view permissions for a specific account.
|
247
|
+
|
248
|
+
Args:
|
249
|
+
account (str): The account for which to set permissions.
|
250
|
+
perms (str or list, optional): Permissions to set. If list, it will be converted to a comma-separated string.
|
251
|
+
redis_con: The Redis connection instance (optional).
|
252
|
+
|
253
|
+
Returns:
|
254
|
+
None
|
255
|
+
"""
|
256
|
+
if redis_con is None:
|
257
|
+
redis_con = redis.get_connection()
|
258
|
+
view_perm_key = utils.generate_perm_view_key(account)
|
259
|
+
if perms is None:
|
260
|
+
redis_con.delete(view_perm_key)
|
261
|
+
else:
|
262
|
+
if isinstance(perms, list):
|
263
|
+
perms = ','.join(perms)
|
264
|
+
redis_con.set(view_perm_key, perms)
|
265
|
+
|
266
|
+
|
267
|
+
def set_write_perms(account, perms, redis_con=None):
|
268
|
+
"""
|
269
|
+
Sets write permissions for a specific account.
|
270
|
+
|
271
|
+
Args:
|
272
|
+
account (str): The account for which to set permissions.
|
273
|
+
perms (str or list, optional): Permissions to set. If list, it will be converted to a comma-separated string.
|
274
|
+
redis_con: The Redis connection instance (optional).
|
275
|
+
|
276
|
+
Returns:
|
277
|
+
None
|
278
|
+
"""
|
279
|
+
if redis_con is None:
|
280
|
+
redis_con = redis.get_connection()
|
281
|
+
write_perm_key = utils.generate_perm_write_key(account)
|
282
|
+
if perms is None:
|
283
|
+
redis_con.delete(write_perm_key)
|
284
|
+
else:
|
285
|
+
if isinstance(perms, list):
|
286
|
+
perms = ','.join(perms)
|
287
|
+
redis_con.set(write_perm_key, perms)
|
288
|
+
|
289
|
+
|
290
|
+
def get_view_perms(account, redis_con=None):
|
291
|
+
"""
|
292
|
+
Retrieves view permissions for a specific account.
|
293
|
+
|
294
|
+
Args:
|
295
|
+
account (str): The account for which to retrieve permissions.
|
296
|
+
redis_con: The Redis connection instance (optional).
|
297
|
+
|
298
|
+
Returns:
|
299
|
+
str or list or None: The permissions for the specified account. Returns None if no permissions are set.
|
300
|
+
"""
|
301
|
+
if redis_con is None:
|
302
|
+
redis_con = redis.get_connection()
|
303
|
+
view_perm_key = utils.generate_perm_view_key(account)
|
304
|
+
perms = redis_con.get(view_perm_key)
|
305
|
+
if perms:
|
306
|
+
perms = perms.decode('utf-8')
|
307
|
+
if ',' in perms:
|
308
|
+
perms = perms.split(',')
|
309
|
+
return perms
|
310
|
+
|
311
|
+
|
312
|
+
def get_write_perms(account, redis_con=None):
|
313
|
+
"""
|
314
|
+
Retrieves write permissions for a specific account.
|
315
|
+
|
316
|
+
Args:
|
317
|
+
account (str): The account for which to retrieve permissions.
|
318
|
+
redis_con: The Redis connection instance (optional).
|
319
|
+
|
320
|
+
Returns:
|
321
|
+
str or list or None: The permissions for the specified account. Returns None if no permissions are set.
|
322
|
+
"""
|
323
|
+
if redis_con is None:
|
324
|
+
redis_con = redis.get_connection()
|
325
|
+
write_perm_key = utils.generate_perm_write_key(account)
|
326
|
+
perms = redis_con.get(write_perm_key)
|
327
|
+
if perms:
|
328
|
+
perms = perms.decode('utf-8')
|
329
|
+
if ',' in perms:
|
330
|
+
perms = perms.split(',')
|
331
|
+
return perms
|
@@ -0,0 +1 @@
|
|
1
|
+
from .base import *
|