Skip to content

Instantly share code, notes, and snippets.

@cnicodeme
Last active November 1, 2023 17:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cnicodeme/bdb5502b36a38fcb475dc940627677e1 to your computer and use it in GitHub Desktop.
Save cnicodeme/bdb5502b36a38fcb475dc940627677e1 to your computer and use it in GitHub Desktop.
Generate statistics for Cloudwatch based on the number of SpamAssassin Childs activity
# -*- config:utf-8 -*-
import boto3, asyncio, sys, os, datetime
def get_sa_max_children(default=12):
try:
options = None
with open('/etc/default/spamassassin', 'r') as f:
for line in f:
if line.find('OPTIONS=') == 0:
options = line.strip()
break
args = iter(options[9:].split(' '))
for arg in args:
if arg[0:4] == '--max-children=':
return int(arg[15:])
elif arg == '-m':
return int(next(args, None))
except Exception:
pass
return default
def get_activity(idles):
block = 1024
log_file = '/var/log/mail.info'
file_size = os.stat(log_file)[6]
start_position = None
with open(log_file, 'r') as f:
f.seek(file_size, 0) # Go to end
data = ''
for i in range(1, 100):
# We go up until we find the pattern
f.seek((file_size - (block * i)), 0) # we go back {block} from the current position
part = f.read(block)
data = part + data
start_position = data.rfind('prefork: child states:')
if start_position > 60:
# This is to ensure the position is after the last line
break
if start_position is None:
return None, None
state = None
for line in data.split('\n')[::-1]:
if line.find('prefork: child states:') > -1:
before, after = line.split('prefork: child states:', 1)
parsed_date = None
try:
# Trying date format 'Wed Nov 1 17:10:39 2023'
dt = before[0:before.find(' [')].strip()
parsed_date = datetime.datetime.strptime(dt, "%a %b %d %H:%M:%S %Y")
except ValueError:
# Might be 'Nov 1 17:11:44'
dt = before[0:before.find(' [')].strip()
dt = dt[0:dt.rfind(' ')] # remove spamd[xxx]
dt = dt[0:dt.rfind(' ')] # remove ip-XXX
try:
parsed_date = datetime.datetime.strptime(dt, "%b %d %H:%M:%S")
parsed_date = parsed_date.replace(year=datetime.datetime.now().year)
except ValueError:
pass
if parsed_date and parsed_date < datetime.datetime.now() - datetime.timedelta(minutes=15):
return None, None
state = after.strip()
break
busy = 0
for child in list(state):
if child == 'B':
busy += 1
idles -= 1
activity = 100 if busy > idles else (busy / idles) * 100
print('')
print('Idles: {}'.format(idles))
print('Busy: {}'.format(busy))
print('Activity: {}%'.format(activity))
return idles, busy, activity
async def main(debug=False, is_delayed=False):
default_idles = get_sa_max_children()
if debug:
return get_activity(default_idles)
for i in range(0, 5):
idles, busy, activity = get_activity(default_idles)
try:
assert busy is not None
assert activity is not None
boto3.client('cloudwatch', region_name='eu-west-3').put_metric_data(
Namespace='SpamAssassin Delayed statistics' if is_delayed else 'SpamAssassin statistics',
MetricData=[
{
'MetricName': 'Idle',
'Unit': 'Count',
'Value': idles,
'StorageResolution': 1,
'Timestamp': datetime.datetime.utcnow()
},
{
'MetricName': 'Busy',
'Unit': 'Count',
'Value': busy,
'StorageResolution': 1,
'Timestamp': datetime.datetime.utcnow()
},
{
'MetricName': 'Activity',
'Unit': 'Percent',
'Value': activity,
'StorageResolution': 1,
'Timestamp': datetime.datetime.utcnow()
},
]
)
except Exception:
# An error occured
pass
await asyncio.sleep(10) # We wait 10 seconds
if __name__ == '__main__':
is_debug = '--debug' in sys.argv
is_delayed = '--delayed' in sys.argv
asyncio.get_event_loop().run_until_complete(main(is_debug, is_delayed))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment