Skip to content

Commit 5d934d5

Browse files
authored
add regex-sampling (#17)
1 parent cf67b63 commit 5d934d5

File tree

4 files changed

+130
-11
lines changed

4 files changed

+130
-11
lines changed

moesifpythonrequest/app_config/app_config.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
from datetime import datetime
22
from moesifapi.exceptions.api_exception import *
33
import json
4+
from .regex_config_helper import RegexConfigHelper
45

56

67
# Application Configuration
78
class AppConfig:
9+
def __init__(self):
10+
self.regex_config_helper = RegexConfigHelper()
11+
return
812

913
def get_config(self, api_client, debug):
1014
"""Get Config"""
@@ -27,19 +31,36 @@ def parse_configuration(self, config, debug):
2731
print('Error while parsing the configuration object, setting the sample rate to default')
2832
return None, 100, datetime.utcnow()
2933

30-
def get_sampling_percentage(self, config, user_id, company_id):
34+
def get_sampling_percentage(self, event_data, config, user_id, company_id):
3135
"""Get sampling percentage"""
3236

33-
config_body = json.loads(config.raw_body)
37+
if config is not None:
38+
try:
39+
config_body = json.loads(config.raw_body)
3440

35-
user_sample_rate = config_body.get('user_sample_rate', None)
41+
user_sample_rate = config_body.get('user_sample_rate', None)
3642

37-
company_sample_rate = config_body.get('company_sample_rate', None)
43+
company_sample_rate = config_body.get('company_sample_rate', None)
3844

39-
if user_id and user_sample_rate and user_id in user_sample_rate:
40-
return user_sample_rate[user_id]
45+
regex_config = config_body.get('regex_config', None)
4146

42-
if company_id and company_sample_rate and company_id in company_sample_rate:
43-
return company_sample_rate[company_id]
47+
if regex_config:
48+
config_mapping = self.regex_config_helper.prepare_config_mapping(event_data)
49+
regex_sample_rate = self.regex_config_helper.fetch_sample_rate_on_regex_match(regex_config, config_mapping)
50+
if regex_sample_rate:
51+
return regex_sample_rate
4452

45-
return config_body.get('sample_rate', 100)
53+
if user_id and user_sample_rate and user_id in user_sample_rate:
54+
return user_sample_rate[user_id]
55+
56+
if company_id and company_sample_rate and company_id in company_sample_rate:
57+
return company_sample_rate[company_id]
58+
59+
return config_body.get('sample_rate', 100)
60+
61+
except Exception as e:
62+
print("Error while parsing user or company sample rate")
63+
print(e)
64+
65+
# Use default
66+
return 100
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import re
2+
3+
4+
class RegexConfigHelper:
5+
6+
def __init__(self):
7+
pass
8+
9+
@classmethod
10+
def prepare_config_mapping(cls, event):
11+
"""
12+
Function to prepare config mapping
13+
Args:
14+
event: Event to be logged
15+
Return:
16+
regex_config: Regex config mapping
17+
"""
18+
regex_config = {}
19+
20+
# Config mapping for request.verb
21+
if event.request.verb:
22+
regex_config["request.verb"] = event.request.verb
23+
24+
# Config mapping for request.uri
25+
if event.request.uri:
26+
extracted = re.match(r"http[s]*://[^/]+(/[^?]+)", event.request.uri)
27+
if extracted is not None:
28+
route_mapping = extracted.group(1)
29+
else:
30+
route_mapping = '/'
31+
regex_config["request.route"] = route_mapping
32+
33+
# Config mapping for request.ip_address
34+
if event.request.ip_address:
35+
regex_config["request.ip_address"] = event.request.ip_address
36+
37+
# Config mapping for response.status
38+
if event.response.status:
39+
regex_config["response.status"] = event.response.status
40+
41+
return regex_config
42+
43+
@classmethod
44+
def regex_match(cls, event_value, condition_value):
45+
"""
46+
Function to perform the regex matching with event value and condition value
47+
Args:
48+
event_value: Value associated with event (request)
49+
condition_value: Value associated with the regex config condition
50+
Return:
51+
regex_matched: Regex matched value to determine if the regex match was successful
52+
"""
53+
extracted = re.search(condition_value, event_value)
54+
if extracted is not None:
55+
return extracted.group(0)
56+
57+
def fetch_sample_rate_on_regex_match(self, regex_configs, config_mapping):
58+
"""
59+
Function to fetch the sample rate and determine if request needs to be block or not
60+
Args:
61+
regex_configs: Regex configs
62+
config_mapping: Config associated with the request
63+
Return:
64+
sample_rate: Sample rate
65+
"""
66+
# Iterate through the list of regex configs
67+
for regex_rule in regex_configs:
68+
# Fetch the sample rate
69+
sample_rate = regex_rule["sample_rate"]
70+
# Fetch the conditions
71+
conditions = regex_rule["conditions"]
72+
# Bool flag to determine if the regex conditions are matched
73+
regex_matched = None
74+
# Create a table to hold the conditions mapping (path and value)
75+
condition_table = {}
76+
# Iterate through the regex rule conditions and map the path and value
77+
for condition in conditions:
78+
# Add condition path -> value to the condition table
79+
condition_table[condition["path"]] = condition["value"]
80+
# Iterate through conditions table and perform `and` operation between each conditions
81+
for path, values in condition_table.items():
82+
# Check if the path exists in the request config mapping
83+
if config_mapping[path]:
84+
# Fetch the value of the path in request config mapping
85+
event_data = config_mapping[path]
86+
# Perform regex matching with event value
87+
regex_matched = self.regex_match(event_data, values)
88+
else:
89+
# Path does not exists in request config mapping, so no need to match regex condition rule
90+
regex_matched = False
91+
# If one of the rule does not match, skip the condition & avoid matching other rules for the same condition
92+
if not regex_matched:
93+
break
94+
# If regex conditions matched, return sample rate
95+
if regex_matched:
96+
return sample_rate
97+
# If regex conditions are not matched, return sample rate as None and will use default sample rate
98+
return None

moesifpythonrequest/send_moesif/send_moesif.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def send_moesif_async(self, event_model):
4949
print("Can not execute MASK_EVENT_MODEL function. Please check moesif settings.")
5050

5151
random_percentage = random.random() * 100
52-
gv.sampling_percentage = gv.app_config.get_sampling_percentage(gv.config, event_model.user_id, event_model.company_id)
52+
gv.sampling_percentage = gv.app_config.get_sampling_percentage(event_model, gv.config, event_model.user_id, event_model.company_id)
5353

5454
if gv.sampling_percentage >= random_percentage:
5555
event_model.weight = 1 if gv.sampling_percentage == 0 else math.floor(100 / gv.sampling_percentage)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
# Versions should comply with PEP440. For a discussion on single-sourcing
2929
# the version across setup.py and the project code, see
3030
# https://packaging.python.org/en/latest/single_source_version.html
31-
version='0.2.0',
31+
version='0.3.0',
3232

3333
description='Moesif Python request',
3434
long_description=long_description,

0 commit comments

Comments
 (0)