Init
This commit is contained in:
commit
3204f44cde
34
doradash/.dockerignore
Normal file
34
doradash/.dockerignore
Normal file
@ -0,0 +1,34 @@
|
||||
# Include any files or directories that you don't want to be copied to your
|
||||
# container here (e.g., local build artifacts, temporary files, etc.).
|
||||
#
|
||||
# For more help, visit the .dockerignore file reference guide at
|
||||
# https://docs.docker.com/go/build-context-dockerignore/
|
||||
|
||||
**/.DS_Store
|
||||
**/__pycache__
|
||||
**/.venv
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
25
doradash/API.md
Normal file
25
doradash/API.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Design doc for API
|
||||
|
||||
Stuff we're gonna need:
|
||||
|
||||
- GET for each of the four metrics, plus most recent "rating".
|
||||
- POST for data-generating events: deployment, outage/restoration.
|
||||
|
||||
Given that this service relies on having data *pushed* to it, we can only ever return metrics based on the most recent deployment or outage/restoration event.
|
||||
|
||||
So with that in mind, we have the following design for each of our endpoints:
|
||||
|
||||
| Method | Description | Endpoint | Request Payload | Response Payload |
|
||||
|:------:|:-----------:|:--------:|:---------------:|:----------------:|
|
||||
| GET | Get deployment frequency | /api/metrics/deployment_frequency | - | {"TIMESTAMP", "COUNT", "UNIT"} |
|
||||
| GET | Get lead time for changes | /api/metrics/lead_time_for_changes | - | {"TIMESTAMP", "COMPUTED_TIME"} |
|
||||
| GET | Get time to restore service | /api/metrics/time_to_restore_service | - | {"TIMESTAMP", "COMPUTED_TIME"} |
|
||||
| GET | Get change failure rate | /api/metrics/change_failure_rate | - | {"TIMESTAMP", "RATE"} |
|
||||
| GET | Get current rating | /api/metrics/vanity | - | {"TIMESTAMP", "RATING"} |
|
||||
| POST | Post new deployment event | /api/events/deployment | {"TIMESTAMP", "{INCLUDED_GIT_HASHES}", "OLDEST_COMMIT_TIMESTAMP", "DEPLOY_RETURN_STATUS"} | OK |
|
||||
| POST | Post new service availability change event | /api/events/service_availability | {"TIMESTAMP", "SERVICE_ID", "EVENT_TYPE"} |
|
||||
|
||||
### Notes
|
||||
- As-is, this API leaves no room for versioning, publisher IDs, or meaningful correlation between deployments and service availability changes.
|
||||
- As-is, we have no identification, authentication, or authorization systems.
|
||||
- As-is, we have no way to view the dataset from which the values are calculated.
|
51
doradash/Dockerfile
Normal file
51
doradash/Dockerfile
Normal file
@ -0,0 +1,51 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Comments are provided throughout this file to help you get started.
|
||||
# If you need more help, visit the Dockerfile reference guide at
|
||||
# https://docs.docker.com/go/dockerfile-reference/
|
||||
|
||||
# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7
|
||||
|
||||
ARG PYTHON_VERSION=3.12.2
|
||||
FROM python:${PYTHON_VERSION}-slim as base
|
||||
|
||||
# Prevents Python from writing pyc files.
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# Keeps Python from buffering stdout and stderr to avoid situations where
|
||||
# the application crashes without emitting any logs due to buffering.
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Create a non-privileged user that the app will run under.
|
||||
# See https://docs.docker.com/go/dockerfile-user-best-practices/
|
||||
ARG UID=10001
|
||||
RUN adduser \
|
||||
--disabled-password \
|
||||
--gecos "" \
|
||||
--home "/nonexistent" \
|
||||
--shell "/sbin/nologin" \
|
||||
--no-create-home \
|
||||
--uid "${UID}" \
|
||||
appuser
|
||||
|
||||
# Download dependencies as a separate step to take advantage of Docker's caching.
|
||||
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
|
||||
# Leverage a bind mount to requirements.txt to avoid having to copy them into
|
||||
# into this layer.
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
--mount=type=bind,source=requirements.txt,target=requirements.txt \
|
||||
python -m pip install -r requirements.txt
|
||||
|
||||
# Switch to the non-privileged user to run the application.
|
||||
USER appuser
|
||||
|
||||
# Copy the source code into the container.
|
||||
COPY . .
|
||||
|
||||
# Expose the port that the application listens on.
|
||||
EXPOSE 8000
|
||||
|
||||
# Run the application.
|
||||
CMD uvicorn app.main:app --reload-dir /app --host 0.0.0.0 --port 8000
|
22
doradash/README.Docker.md
Normal file
22
doradash/README.Docker.md
Normal file
@ -0,0 +1,22 @@
|
||||
### Building and running your application
|
||||
|
||||
When you're ready, start your application by running:
|
||||
`docker compose up --build`.
|
||||
|
||||
Your application will be available at http://localhost:8000.
|
||||
|
||||
### Deploying your application to the cloud
|
||||
|
||||
First, build your image, e.g.: `docker build -t myapp .`.
|
||||
If your cloud uses a different CPU architecture than your development
|
||||
machine (e.g., you are on a Mac M1 and your cloud provider is amd64),
|
||||
you'll want to build the image for that platform, e.g.:
|
||||
`docker build --platform=linux/amd64 -t myapp .`.
|
||||
|
||||
Then, push it to your registry, e.g. `docker push myregistry.com/myapp`.
|
||||
|
||||
Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/)
|
||||
docs for more detail on building and pushing.
|
||||
|
||||
### References
|
||||
* [Docker's Python guide](https://docs.docker.com/language/python/)
|
2
doradash/README.md
Normal file
2
doradash/README.md
Normal file
@ -0,0 +1,2 @@
|
||||
# Doradash - A simple API server for tracking DORA metrics
|
||||
Configure your Git, CI/CD, and observability platforms to integrate with Doradash to get a readout of your DORA metrics: deployment frequency, lead time for changes, time to restore service, and change failure rate.
|
BIN
doradash/app/__pycache__/main.cpython-312.pyc
Normal file
BIN
doradash/app/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
BIN
doradash/app/__pycache__/main_test.cpython-312-pytest-8.1.1.pyc
Normal file
BIN
doradash/app/__pycache__/main_test.cpython-312-pytest-8.1.1.pyc
Normal file
Binary file not shown.
120
doradash/app/main.py
Normal file
120
doradash/app/main.py
Normal file
@ -0,0 +1,120 @@
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from pydantic.functional_validators import field_validator
|
||||
import re
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
class Deployment(BaseModel):
|
||||
event_timestamp: datetime = None # should look like 2024-03-12T14:29:46-0700
|
||||
hashes: list = None # each should match an sha1 hash format regex(\b[0-9a-f]{5,40}\b)
|
||||
timestamp_oldest_commit: datetime = None # should look like 2024-03-12T14:29:46-0700
|
||||
deploy_return_status: str = None # should be "Success", "Failure", or "Invalid"
|
||||
|
||||
@field_validator("event_timestamp","timestamp_oldest_commit")
|
||||
def validate_datetime(cls, d):
|
||||
# oh lord jesus datetime validation
|
||||
date_text = str(d)
|
||||
iso8601_regex = r"^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$"
|
||||
if re.match(iso8601_regex, date_text):
|
||||
return d
|
||||
else:
|
||||
raise ValueError(f"date must be in ISO-8601 format: {d}")
|
||||
|
||||
@field_validator("hashes")
|
||||
def validate_hashes(cls, hashes):
|
||||
if not len(hashes) > 0:
|
||||
raise ValueError(f"commit hash list cannot be empty")
|
||||
for h in hashes:
|
||||
if not re.match(r"\b[0-9a-f]{5,40}\b", h):
|
||||
raise ValueError(f"hash not valid sha1: {h}")
|
||||
else:
|
||||
return hashes
|
||||
|
||||
@field_validator("deploy_return_status")
|
||||
def validate_return_status(cls, status):
|
||||
if status not in ["Success", "Failure", "Invalid"]:
|
||||
raise ValueError(f"return_status must be one of \"Success\", \"Failure\", or \"Invalid\": {status}")
|
||||
else:
|
||||
return status
|
||||
|
||||
class ServiceAvailabilityChange(BaseModel):
|
||||
event_timestamp: datetime # should look like 2024-03-12T14:29:46-0700
|
||||
service_id: str # practically arbitrary, but maybe useful for later
|
||||
event_type: str # should be "outage" or "restoration"
|
||||
|
||||
@field_validator("event_type")
|
||||
def validate_balanced_events(cls,event_type):
|
||||
# since all inputs are validated one at a time, we can simplify the balancing logic
|
||||
# we can use a naive algorithm (count outages, count restorations) here because we validate each input one at a time
|
||||
|
||||
stack = []
|
||||
for event in service_events:
|
||||
if event.event_type == "outage":
|
||||
stack.append(event)
|
||||
else:
|
||||
if not stack or (\
|
||||
event.event_type == 'restoration' and \
|
||||
stack[-1] != 'outage'\
|
||||
):
|
||||
raise ValueError("no preceding outage for restoration event")
|
||||
stack.pop()
|
||||
|
||||
# please replace "store the dataset in an array in memory" before deploying
|
||||
deployments = []
|
||||
service_events = []
|
||||
|
||||
@app.post("/api/events/deployment")
|
||||
def append_deployment(deployment: Deployment):
|
||||
deployments.append(deployment)
|
||||
return deployment
|
||||
|
||||
@app.post("/api/events/service_availability")
|
||||
def append_service_availability(service_event: ServiceAvailabilityChange):
|
||||
service_events.append(service_event)
|
||||
return service_event
|
||||
|
||||
@app.get("/api/metrics/deployment_frequency")
|
||||
def get_deployment_frequency():
|
||||
deploys_in_day = {}
|
||||
for deployment in deployments:
|
||||
if deployment.event_timestamp.date() in deploys_in_day:
|
||||
deploys_in_day[deployment.event_timestamp.date()] += 1
|
||||
else:
|
||||
deploys_in_day[deployment.event_timestamp.date()] = 1
|
||||
return len(deployments) / len(deploys_in_day)
|
||||
|
||||
@app.get("/api/metrics/lead_time_for_changes")
|
||||
def get_lead_time_for_changes():
|
||||
time_deltas = []
|
||||
for deployment in deployments:
|
||||
time_delta = deployment.event_timestamp - deployment.timestamp_oldest_commit
|
||||
time_deltas.append(time_delta.seconds)
|
||||
lead_time_for_changes = sum(time_deltas) / len(time_deltas)
|
||||
return str(timedelta(seconds=lead_time_for_changes)) # standardize output format?
|
||||
|
||||
@app.get("/api/metrics/time_to_restore_service")
|
||||
def get_time_to_restore_service():
|
||||
# check for balanced events (a preceding outage for each restoration)
|
||||
# for each balanced root-level event, get the time delta from first outage to final restoration
|
||||
# append time delta to array of time deltas
|
||||
# return average of time deltas array
|
||||
|
||||
|
||||
|
||||
@app.get("/api/metrics/change_failure_rate")
|
||||
def get_change_failure_rate():
|
||||
success_counter = 0
|
||||
failure_counter = 0
|
||||
for deployment in deployments:
|
||||
if deployment.deploy_return_status == "Invalid":
|
||||
pass
|
||||
elif deployment.deploy_return_status == "Success":
|
||||
success_counter += 1
|
||||
else:
|
||||
failure_counter += 1
|
||||
return failure_counter / (success_counter + failure_counter)
|
||||
|
||||
# @app.get("/api/metrics/vanity")
|
||||
# def get_vanity():
|
18
doradash/app/main_test.py
Normal file
18
doradash/app/main_test.py
Normal file
@ -0,0 +1,18 @@
|
||||
from datetime import datetime
|
||||
import json
|
||||
import requests
|
||||
|
||||
valid_deployment = {
|
||||
"event_timestamp": str(datetime.now()),
|
||||
"hashes": ["d7d8937e8f169727852dea77bae30a8749fe21fc"],
|
||||
"oldest_commit_timestamp": str(datetime.now()),
|
||||
"deploy_return_status": "Success"
|
||||
}
|
||||
|
||||
def test_valid_deployment():
|
||||
#payload =
|
||||
endpoint = "http://127.0.0.1:8000/api/events/deployment"
|
||||
response = requests.post(endpoint, json=json.dumps(valid_deployment))
|
||||
print(response)
|
||||
print(valid_deployment)
|
||||
#assert response.status_code == 200
|
49
doradash/docker-compose.yaml
Normal file
49
doradash/docker-compose.yaml
Normal file
@ -0,0 +1,49 @@
|
||||
# Comments are provided throughout this file to help you get started.
|
||||
# If you need more help, visit the Docker Compose reference guide at
|
||||
# https://docs.docker.com/go/compose-spec-reference/
|
||||
|
||||
# Here the instructions define your application as a service called "server".
|
||||
# This service is built from the Dockerfile in the current directory.
|
||||
# You can add other services your application may depend on here, such as a
|
||||
# database or a cache. For examples, see the Awesome Compose repository:
|
||||
# https://github.com/docker/awesome-compose
|
||||
services:
|
||||
doradash:
|
||||
build:
|
||||
context: .
|
||||
ports:
|
||||
- 8000:8000
|
||||
|
||||
# The commented out section below is an example of how to define a PostgreSQL
|
||||
# database that your application can use. `depends_on` tells Docker Compose to
|
||||
# start the database before your application. The `db-data` volume persists the
|
||||
# database data between container restarts. The `db-password` secret is used
|
||||
# to set the database password. You must create `db/password.txt` and add
|
||||
# a password of your choosing to it before running `docker compose up`.
|
||||
# depends_on:
|
||||
# db:
|
||||
# condition: service_healthy
|
||||
# db:
|
||||
# image: postgres
|
||||
# restart: always
|
||||
# user: postgres
|
||||
# secrets:
|
||||
# - db-password
|
||||
# volumes:
|
||||
# - db-data:/var/lib/postgresql/data
|
||||
# environment:
|
||||
# - POSTGRES_DB=example
|
||||
# - POSTGRES_PASSWORD_FILE=/run/secrets/db-password
|
||||
# expose:
|
||||
# - 5432
|
||||
# healthcheck:
|
||||
# test: [ "CMD", "pg_isready" ]
|
||||
# interval: 10s
|
||||
# timeout: 5s
|
||||
# retries: 5
|
||||
# volumes:
|
||||
# db-data:
|
||||
# secrets:
|
||||
# db-password:
|
||||
# file: db/password.txt
|
||||
|
4
doradash/requirements.txt
Normal file
4
doradash/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
uvicorn==0.28.0
|
||||
fastapi==0.110.0
|
||||
pydantic==2.6.4
|
||||
pytest==8.1.1
|
6
doradash/test_deployments.sh
Executable file
6
doradash/test_deployments.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
curl -X 'POST' 'http://127.0.0.1:8000/api/events/deployment' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"event_timestamp": "2024-03-12T22:02:38.689Z","hashes": ["6ece311c24dd6a4b3dbbf8525a3a61854a32838d","d7d8937e8f169727852dea77bae30a8749fe21fc"],"timestamp_oldest_commit": "2024-03-11T22:02:38.689Z","deploy_return_status": "Failure"}'
|
||||
curl -X 'POST' 'http://127.0.0.1:8000/api/events/deployment' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"event_timestamp": "2024-03-12T23:03:38.689Z","hashes": ["f5521851965c4866c5dc0e8edc9d5e2a40b5ebe6","b8c3bb11a978dbcbe507c53c62f715a728cdfd52"],"timestamp_oldest_commit": "2024-03-10T22:05:38.689Z","deploy_return_status": "Success"}'
|
||||
curl -X 'POST' 'http://127.0.0.1:8000/api/events/deployment' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"event_timestamp": "2024-03-11T21:03:38.689Z","hashes": ["ae35c9c0e4f71ddf280bd297c42f04f2c0ce3838","d53e974d7e60295ed36c38a57870d1a6bfc7e399"],"timestamp_oldest_commit": "2024-03-11T20:05:38.689Z","deploy_return_status": "Success"}'
|
||||
curl -X 'POST' 'http://127.0.0.1:8000/api/events/deployment' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"event_timestamp": "2024-03-10T23:03:38.689Z","hashes": ["b6a707faa68bc987ae549c0f36d053a412bd40da","b6a707faa68bc987ae549c0f36d053a412bd40da"],"timestamp_oldest_commit": "2024-03-10T14:05:38.689Z","deploy_return_status": "Success"}'
|
||||
curl -X 'POST' 'http://127.0.0.1:8000/api/events/deployment' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"event_timestamp": "2024-02-10T23:03:38.689Z","hashes": ["94036270dd329559b58edc6f8780e03bd94509a3","b6d1abc911c08778424fb244de1f172f54905b81"],"timestamp_oldest_commit": "2024-02-09T14:05:38.689Z","deploy_return_status": "Invalid"}'
|
11
doradash/test_service_events.sh
Executable file
11
doradash/test_service_events.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
curl -X 'POST' 'http://127.0.0.1:8000/api/events/service_availability' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"event_timestamp": "2024-03-11T18:02:00.000Z","service_id": "plex","event_type": "outage"}'
|
||||
curl -X 'POST' 'http://127.0.0.1:8000/api/events/service_availability' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"event_timestamp": "2024-03-11T19:02:00.000Z","service_id": "plex","event_type": "restoration"}'
|
||||
curl -X 'POST' 'http://127.0.0.1:8000/api/events/service_availability' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"event_timestamp": "2024-03-11T20:02:00.000Z","service_id": "nextcloud","event_type": "outage"}'
|
||||
curl -X 'POST' 'http://127.0.0.1:8000/api/events/service_availability' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"event_timestamp": "2024-03-11T21:02:00.000Z","service_id": "nextcloud","event_type": "restoration"}'
|
||||
curl -X 'POST' 'http://127.0.0.1:8000/api/events/service_availability' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"event_timestamp": "2024-03-11T22:02:00.000Z","service_id": "nextcloud","event_type": "outage"}'
|
||||
curl -X 'POST' 'http://127.0.0.1:8000/api/events/service_availability' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"event_timestamp": "2024-03-12T01:02:00.000Z","service_id": "nextcloud","event_type": "restoration"}'
|
||||
curl -X 'POST' 'http://127.0.0.1:8000/api/events/service_availability' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"event_timestamp": "2024-03-12T02:02:00.000Z","service_id": "plex","event_type": "outage"}'
|
||||
curl -X 'POST' 'http://127.0.0.1:8000/api/events/service_availability' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"event_timestamp": "2024-03-12T03:02:00.000Z","service_id": "plex","event_type": "restoration"}'
|
||||
curl -X 'POST' 'http://127.0.0.1:8000/api/events/service_availability' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"event_timestamp": "2024-03-12T04:02:00.000Z","service_id": "plex","event_type": "outage"}'
|
||||
curl -X 'POST' 'http://127.0.0.1:8000/api/events/service_availability' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"event_timestamp": "2024-03-12T04:02:00.000Z","service_id": "plex","event_type": "restoration"}'
|
Loading…
x
Reference in New Issue
Block a user