oncall-engine/dev/scripts/generate-fake-data/main.py
Joey Orlando 98241b9a10
fake-data generation script + fixes for django-silk and django-debug-toolbar (#1128)
# What this PR does

## Main stuff

- add Python script to populate local Grafana/OnCall setup w/ large
amounts of fake data. Right now the data types that can be generated
are:
- teams and Admin users via the Grafana API (must be synced manually by
going into the UI before going onto the next step)
- Calendar Schedules which have three 8h oncall-shifts, via the OnCall
public API
- fixes `django-debug-toolbar` when being run in `docker-compose`
locally

## Other stuff
- documents how to easily modify the Grafana `docker-compose` container
provisioning configuration
- document solutions for two backend setup related issues encountered
when running the engine/celery workers locally, outside of
`docker-compose`, on an Apple silicon Mac
- fixes small bug in `grafana_plugin.helpers.client.APIClient.call_api`
where it would call `response.json()` for all requests, regardless of
whether or not the response actually contained data or not
- in `engine/settings/dev.py`, properly setup `django-silk` and document
the steps to use it locally
- make it possible to log out debug SQL queries by specifying
`DEV_DEBUG_VIEW_SQL_QUERIES` env var, rather than having to uncomment
out a section of `settings/dev.py`

## Which issue(s) this PR fixes

- Some local setup issues when trying to use `django-silk` and
`django-debug-toolbar`
- Makes it much easier to populate your local setup with a lot of fake
data
- Makes it possible to easily modify your local grafana's provisioning
configuration

## Checklist

- [ ] Tests updated (N/A)
- [ ] Documentation added (N/A)
- [ ] `CHANGELOG.md` updated (N/A)
2023-01-20 09:19:41 +01:00

305 lines
9.2 KiB
Python

import argparse
import asyncio
import math
import random
import typing
import uuid
from datetime import datetime
import aiohttp
from faker import Faker
from tqdm.asyncio import tqdm
fake = Faker()
TEAMS_USERS_COMMAND = "generate_teams_and_users"
SCHEDULES_ONCALL_SHIFTS_COMMAND = "generate_schedules_and_oncall_shifts"
GRAFANA_API_URL = None
ONCALL_API_URL = None
ONCALL_API_TOKEN = None
class OnCallApiUser(typing.TypedDict):
id: str
class OnCallApiOnCallShift(typing.TypedDict):
id: str
class OnCallApiListUsersResponse(typing.TypedDict):
results: typing.List[OnCallApiUser]
class GrafanaAPIUser(typing.TypedDict):
id: int
def _generate_unique_email() -> str:
user = fake.profile()
return f'{uuid.uuid4()}-{user["mail"]}'
async def _grafana_api_request(
http_session: aiohttp.ClientSession, method: str, url: str, **request_kwargs
) -> typing.Awaitable[typing.Dict]:
resp = await http_session.request(
method, f"{GRAFANA_API_URL}{url}", **request_kwargs
)
return await resp.json()
async def _oncall_api_request(
http_session: aiohttp.ClientSession, method: str, url: str, **request_kwargs
) -> typing.Awaitable[typing.Dict]:
resp = await http_session.request(
method,
f"{ONCALL_API_URL}{url}",
headers={"Authorization": ONCALL_API_TOKEN},
**request_kwargs,
)
return await resp.json()
def generate_team(
http_session: aiohttp.ClientSession, org_id: int
) -> typing.Callable[[], typing.Awaitable[typing.Dict]]:
"""
https://grafana.com/docs/grafana/latest/developers/http_api/team/#add-team
"""
def _generate_team() -> typing.Awaitable[typing.Dict]:
return _grafana_api_request(
http_session,
"POST",
"/api/teams",
json={
"name": str(uuid.uuid4()),
"email": _generate_unique_email(),
"orgId": org_id,
},
)
return _generate_team
def generate_user(
http_session: aiohttp.ClientSession, org_id: int
) -> typing.Callable[[], typing.Awaitable[typing.Dict]]:
"""
https://grafana.com/docs/grafana/latest/developers/http_api/admin/#global-users
"""
async def _generate_user() -> typing.Awaitable[typing.Dict]:
user = fake.profile()
# create the user in grafana
grafana_user: GrafanaAPIUser = await _grafana_api_request(
http_session,
"POST",
"/api/admin/users",
json={
"name": user["name"],
"email": _generate_unique_email(),
"login": str(uuid.uuid4()),
"password": fake.password(length=20),
"OrgId": org_id,
},
)
# update the user's basic role in grafana to Admin
# https://grafana.com/docs/grafana/latest/developers/http_api/org/#updates-the-given-user
await _grafana_api_request(
http_session,
"PATCH",
f'/api/org/users/{grafana_user["id"]}',
json={"role": "Admin"},
)
return grafana_user
return _generate_user
def generate_schedule(
http_session: aiohttp.ClientSession, oncall_shift_ids: typing.List[str]
) -> typing.Callable[[], typing.Awaitable[typing.Dict]]:
def _generate_schedule() -> typing.Awaitable[typing.Dict]:
# Create a schedule
# https://grafana.com/docs/oncall/latest/oncall-api-reference/schedules/#create-a-schedule
return _oncall_api_request(
http_session,
"POST",
"/api/v1/schedules",
json={
"name": f"Schedule {uuid.uuid4()}",
"type": "calendar",
"time_zone": "UTC",
"shifts": oncall_shift_ids,
},
)
return _generate_schedule
def _bulk_generate_data(
iterations: int,
data_generator_func: typing.Callable[[], typing.Awaitable[typing.Dict]],
) -> typing.Awaitable[typing.List[typing.Dict]]:
return tqdm.gather(
*[asyncio.ensure_future(data_generator_func()) for _ in range(iterations)]
)
async def _generate_grafana_teams_and_users(
args: argparse.Namespace, http_session: aiohttp.ClientSession
) -> typing.Awaitable[None]:
global GRAFANA_API_URL
GRAFANA_API_URL = args.grafana_api_url
org_id = args.grafana_org_id
print("Generating team(s)")
await _bulk_generate_data(args.teams, generate_team(http_session, org_id))
print("Generating user(s)")
await _bulk_generate_data(args.users, generate_user(http_session, org_id))
print(
f"""
Grafana teams and users generated
Now head to the OnCall plugin and manually visit the plugin to trigger a sync. This will sync grafana
teams/users to OnCall. Once completed, you can run the {SCHEDULES_ONCALL_SHIFTS_COMMAND} command.
"""
)
async def _generate_oncall_schedules_and_oncall_shifts(
args: argparse.Namespace, http_session: aiohttp.ClientSession
) -> typing.Awaitable[None]:
global ONCALL_API_URL, ONCALL_API_TOKEN
ONCALL_API_URL = args.oncall_api_url
ONCALL_API_TOKEN = args.oncall_api_token
today = datetime.now()
print("Fetching users from OnCall API")
# Fetch users from the OnCall API
users: OnCallApiListUsersResponse = await _oncall_api_request(
http_session, "GET", "/api/v1/users"
)
user_ids: typing.List[str] = [u["id"] for u in users["results"]]
num_users = len(user_ids)
print(f"Fetched {num_users} user(s) from the OnCall API")
async def _create_oncall_shift(shift_start_time: str) -> typing.Awaitable[str]:
"""
Creates an eight hour shift.
`shift_start_time` - ex. 09:00:00, 15:00:00
https://grafana.com/docs/oncall/latest/oncall-api-reference/on_call_shifts/#create-an-oncall-shift
"""
new_shift: OnCallApiOnCallShift = await _oncall_api_request(
http_session,
"POST",
"/api/v1/on_call_shifts",
json={
"name": f"On call shift{uuid.uuid4()}",
"type": "rolling_users",
"start": today.strftime(f"%Y-%m-%dT{shift_start_time}"),
"time_zone": "UTC",
"duration": 60 * 60 * 8, # 8 hours
"frequency": "daily",
"week_start": "MO",
"rolling_users": [
[u] for u in random.choices(user_ids, k=math.floor(num_users / 2))
],
"start_rotation_from_user_index": 0,
"team_id": None,
},
)
oncall_shift_id = new_shift["id"]
print(f"Generated OnCall shift w/ ID {oncall_shift_id}")
return oncall_shift_id
print("Creating three 8h on-call shifts")
morning_shift_id = await _create_oncall_shift("00:00:00")
afternoon_shift_id = await _create_oncall_shift("08:00:00")
evening_shift_id = await _create_oncall_shift("16:00:00")
print("Generating schedules(s)")
await _bulk_generate_data(
args.schedules,
generate_schedule(
http_session, [morning_shift_id, afternoon_shift_id, evening_shift_id]
),
)
async def main() -> typing.Awaitable[None]:
parser = argparse.ArgumentParser(
description="Set of commands to help generate fake data in a Grafana OnCall setup."
)
subparsers = parser.add_subparsers(help="sub-command help")
grafana_command_parser = subparsers.add_parser(
TEAMS_USERS_COMMAND,
description="Command to generate teams and users in Grafana",
)
grafana_command_parser.set_defaults(func=_generate_grafana_teams_and_users)
grafana_command_parser.add_argument(
"--grafana-api-url",
help="Grafana API URL. This should include the basic authentication username/password in the URL. ex. http://oncall:oncall@localhost:3000",
default="http://oncall:oncall@localhost:3000",
)
grafana_command_parser.add_argument(
"--grafana-org-id",
help="Org ID, in Grafana, of the org that you would like to generate data for",
type=int,
default=1,
)
grafana_command_parser.add_argument(
"-t", "--teams", help="Number of teams to generate", default=10, type=int
)
grafana_command_parser.add_argument(
"-u", "--users", help="Number of users to generate", default=1_000, type=int
)
oncall_command_parser = subparsers.add_parser(
SCHEDULES_ONCALL_SHIFTS_COMMAND,
description="Command to generate schedules and on-call shifts in OnCall",
)
oncall_command_parser.set_defaults(
func=_generate_oncall_schedules_and_oncall_shifts
)
oncall_command_parser.add_argument(
"--oncall-api-url",
help="OnCall API URL",
default="http://localhost:8080",
)
oncall_command_parser.add_argument(
"--oncall-api-token", help="OnCall API token", required=True
)
oncall_command_parser.add_argument(
"-s",
"--schedules",
help="Number of schedules to generate",
default=100,
type=int,
)
args = parser.parse_args()
async with aiohttp.ClientSession(
connector=aiohttp.TCPConnector(limit=5)
) as session:
await args.func(args, session)
if __name__ == "__main__":
asyncio.run(main())