Skip to content
This repository has been archived by the owner on Jul 25, 2024. It is now read-only.

Commit

Permalink
Merge pull request #92 from communitiesuk/fmd-232-email-report
Browse files Browse the repository at this point in the history
FMD-232: email report option
  • Loading branch information
gidsg authored Mar 7, 2024
2 parents 2a825ff + 78170a0 commit 877dd42
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 99 deletions.
18 changes: 9 additions & 9 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,7 @@ commonmark==0.9.1
# -r requirements.txt
# rich
coverage[toml]==7.2.3
# via
# coverage
# pytest-cov
# via pytest-cov
cryptography==42.0.4
# via
# -r requirements.txt
Expand All @@ -90,6 +88,10 @@ dnspython==2.3.0
# via
# -r requirements.txt
# email-validator
docopt==0.6.2
# via
# -r requirements.txt
# notifications-python-client
email-validator==2.0.0.post2
# via -r requirements.txt
flake8==6.0.0
Expand Down Expand Up @@ -151,10 +153,6 @@ govuk-frontend-jinja==2.6.0
# govuk-frontend-wtf
govuk-frontend-wtf==2.4.0
# via -r requirements.txt
greenlet==3.0.3
# via
# -r requirements.txt
# sqlalchemy
gunicorn==20.1.0
# via
# -r requirements.txt
Expand Down Expand Up @@ -202,6 +200,8 @@ mccabe==0.7.0
# via flake8
mypy-extensions==1.0.0
# via black
notifications-python-client==9.0.0
# via -r requirements.txt
packaging==23.1
# via
# build
Expand Down Expand Up @@ -240,7 +240,7 @@ pyjwt[crypto]==2.7.0
# via
# -r requirements.txt
# funding-service-design-utils
# pyjwt
# notifications-python-client
pyproject-hooks==1.0.0
# via build
pytest==7.3.1
Expand Down Expand Up @@ -290,6 +290,7 @@ requests==2.31.0
# via
# -r requirements.txt
# funding-service-design-utils
# notifications-python-client
# python-consul
# requests-mock
requests-mock==1.10.0
Expand All @@ -307,7 +308,6 @@ sentry-sdk[flask]==1.22.2
# via
# -r requirements.txt
# funding-service-design-utils
# sentry-sdk
six==1.16.0
# via
# -r requirements.txt
Expand Down
1 change: 1 addition & 0 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ govuk-frontend-jinja==2.6.0
govuk-frontend-wtf==2.4.0
gunicorn==20.1.0
flask-WTF==1.1.1
notifications-python-client==9.0.0
13 changes: 7 additions & 6 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ deepmerge==1.1.0
# via govuk-frontend-wtf
dnspython==2.3.0
# via email-validator
docopt==0.6.2
# via notifications-python-client
email-validator==2.0.0.post2
# via -r requirements.in
flask==2.3.2
Expand Down Expand Up @@ -86,8 +88,6 @@ govuk-frontend-jinja==2.6.0
# govuk-frontend-wtf
govuk-frontend-wtf==2.4.0
# via -r requirements.in
greenlet==3.0.3
# via sqlalchemy
gunicorn==20.1.0
# via
# -r requirements.in
Expand Down Expand Up @@ -120,6 +120,8 @@ markupsafe==2.1.2
# mako
# werkzeug
# wtforms
notifications-python-client==9.0.0
# via -r requirements.in
pycparser==2.21
# via cffi
pyee==6.0.0
Expand All @@ -129,7 +131,7 @@ pygments==2.15.1
pyjwt[crypto]==2.7.0
# via
# funding-service-design-utils
# pyjwt
# notifications-python-client
python-consul==1.1.0
# via flipper-client
python-dateutil==2.8.2
Expand All @@ -151,15 +153,14 @@ redis==4.5.4
requests==2.31.0
# via
# funding-service-design-utils
# notifications-python-client
# python-consul
rich==12.6.0
# via funding-service-design-utils
s3transfer==0.6.1
# via boto3
sentry-sdk[flask]==1.22.2
# via
# funding-service-design-utils
# sentry-sdk
# via funding-service-design-utils
six==1.16.0
# via
# python-consul
Expand Down
243 changes: 159 additions & 84 deletions scripts/extract_download_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,65 +3,51 @@
For script options, run the script with '--help' argument.
"""

import argparse
import csv
import datetime
import io
import json
import os
import time
from optparse import OptionParser
from io import StringIO
from typing import List

from boto3 import client

parser = OptionParser(
usage="Output a report of downloads (requires AWS authentication)"
)
parser.add_option(
"-e",
"--environment",
dest="environment",
default="test",
help="Specify the environment (default: test)",
)
parser.add_option(
"-d",
"--days",
dest="days",
type=int,
default=30,
help="Specify the number of days (default: 30)",
)
parser.add_option(
"-f",
"--filename",
dest="filename",
default="output.csv",
help="Specify the output filename",
)

(options, args) = parser.parse_args()

ENVIRONMENT = options.environment
DAYS = options.days

print("Starting script")

FIELD_NAMES = [
"timestamp",
"user_id",
"email",
"funds",
"file_format",
"organisations",
"regions",
"outcome_categories",
"rp_start",
"rp_end",
]
OUTPUT_FILENAME = options.filename


def cloudwatch_logs_to_rows_dict(data: List[dict]) -> List[dict]:
def parse_item(item: dict) -> dict:
from dateutil.relativedelta import relativedelta
from notifications_python_client import prepare_upload
from notifications_python_client.notifications import NotificationsAPIClient


def send_notify(
from_date: datetime.datetime,
to_date: datetime.datetime,
file_buffer: io.BytesIO,
api_key: str = os.getenv("NOTIFY_API_KEY"),
template_id: str = "196e5553-886c-40bd-ac9a-981a7868301b",
email_address: str = os.getenv("NOTIFY_SEND_EMAIL", "[email protected]"),
):
if not api_key:
raise KeyError("Notify API key is required to send email")
from_date_formatted = from_date.replace(microsecond=0).isoformat()
to_date_formatted = to_date.replace(microsecond=0).isoformat()
notifications_client = NotificationsAPIClient(api_key)
notifications_client.send_email_notification(
email_address=email_address,
template_id=template_id,
personalisation={
"from_date": from_date_formatted,
"to_date": to_date_formatted,
"link_to_file": prepare_upload(
file_buffer,
filename=f"{from_date_formatted}-{to_date_formatted}-download-report.csv",
),
},
)


def cloudwatch_logs_to_rows(data: List[List[dict]]) -> List[dict]:
def parse_item(item: List[dict]) -> dict:
message = json.loads([i for i in item if i["field"] == "@message"][0]["value"])
user_id = message["user_id"]
email = message.get("email")
Expand All @@ -77,36 +63,125 @@ def parse_item(item: dict) -> dict:
return [parse_item(item) for item in data]


cloudwatch_logs_client = client("logs", region_name="eu-west-2")

now = datetime.datetime.now()
d = datetime.timedelta(days=DAYS)
start_time = now - d

query_id = cloudwatch_logs_client.start_query(
logGroupName=f"/copilot/post-award-{ENVIRONMENT}-data-frontend",
queryString="""fields @timestamp, @message
| sort @timestamp desc
| limit 1000
| filter request_type = 'download'""",
startTime=int(datetime.datetime.timestamp(start_time)),
endTime=int(datetime.datetime.timestamp(now)),
)["queryId"]

# Poll until query is complete
response = None

while response is None or response["status"] == "Running":
print("Waiting for query to complete ...")
time.sleep(1)
response = cloudwatch_logs_client.get_query_results(queryId=query_id)

rows_dict = cloudwatch_logs_to_rows_dict(response["results"])

# Open the CSV file
with open(OUTPUT_FILENAME, "w", newline="") as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=FIELD_NAMES)
def rows_to_csv(data: List[dict], field_names: List[str]) -> StringIO:
csv_buffer = StringIO()
writer = csv.DictWriter(csv_buffer, fieldnames=field_names)
writer.writeheader()
writer.writerows(rows_dict)

print(f"File written to {OUTPUT_FILENAME}")
writer.writerows(data)
return csv_buffer


def main(args):

ENVIRONMENT = args.environment

FIELD_NAMES = [
"timestamp",
"user_id",
"email",
"funds",
"file_format",
"organisations",
"regions",
"outcome_categories",
"rp_start",
"rp_end",
]
OUTPUT_FILENAME = args.filename

end_time = datetime.datetime.now()

if args.days is not None:
start_time = end_time + relativedelta(days=-args.days)
else:
start_time = end_time + relativedelta(months=-args.months)

cloudwatch_logs_client = client("logs", region_name="eu-west-2")

query_id = cloudwatch_logs_client.start_query(
logGroupName=f"/copilot/post-award-{ENVIRONMENT}-data-frontend",
queryString="""fields @timestamp, @message
| sort @timestamp asc
| limit 1000
| filter request_type = 'download'""",
startTime=int(datetime.datetime.timestamp(start_time)),
endTime=int(datetime.datetime.timestamp(end_time)),
)["queryId"]

# Poll until query is complete
response = None

while response is None or response["status"] == "Running":
print("Waiting for query to complete ...")
time.sleep(1)
response = cloudwatch_logs_client.get_query_results(queryId=query_id)

rows = cloudwatch_logs_to_rows(response["results"])
csv_file = rows_to_csv(rows, FIELD_NAMES)

if args.email:
send_notify(start_time, end_time, io.BytesIO(csv_file.getvalue().encode()))
print("File sent via Notify")

if not args.disable_write_file:
with open(OUTPUT_FILENAME, "w", newline="") as output_file:
output_file.write(csv_file.getvalue())

print(f"File written to {OUTPUT_FILENAME}")


if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Output a report of downloads (requires AWS authentication)",
)

time_option_group = parser.add_mutually_exclusive_group()
time_option_group.add_argument(
"-d",
"--days",
dest="days",
type=int,
help="""Specify the number of days to include in the report, counting backwards from today.
This option generates a report covering the specified period.""",
)
time_option_group.add_argument(
"-m",
"--months",
dest="months",
type=int,
default=1,
help="""Specify the number of months to include in the report, counting backwards from today.
This option generates a report covering the specified period. (default: 1)""",
)
parser.add_argument(
"-e",
"--environment",
dest="environment",
default="test",
help="Specify the environment (default: test)",
)

parser.add_argument(
"-f",
"--filename",
dest="filename",
default="output.csv",
help="Specify the output filename",
)

parser.add_argument(
"--email",
action="store_true",
dest="email",
help="Send an email notification (default: False)",
)

parser.add_argument(
"--disable-write-file",
action="store_true",
dest="disable_write_file",
help="Write file to disk",
)

print("Starting script")
main(parser.parse_args())

0 comments on commit 877dd42

Please sign in to comment.