Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NDEV-2475 collect metrics for requests to Solana #51

Merged
merged 9 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 106 additions & 1 deletion .github/workflows/deploy.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import multiprocessing
import os
import re
import time
import statistics
import sys
from collections import defaultdict

import docker
import subprocess
Expand All @@ -15,6 +16,10 @@
from python_terraform import Terraform
from paramiko import SSHClient
from scp import SCPClient
try:
import pandas as pd
except ImportError:
print("Please install pandas library: 'pip install pandas' and 'pip install tabulate' for requests statistics")

try:
import click
Expand Down Expand Up @@ -62,6 +67,8 @@ def cli():
__file__).parent / "full_test_suite")
VERSION_BRANCH_TEMPLATE = r"[vt]{1}\d{1,2}\.\d{1,2}\.x.*"

SOLANA_REQUESTS_TITLE = "<summary>Solana Requests Statistics</summary>"


def docker_compose(args: str):
command = f'docker login -u {DOCKER_USERNAME} -p {DOCKER_PASSWORD}'
Expand Down Expand Up @@ -467,5 +474,103 @@ def process_output(output):
raise SystemError(message)


# Regular expression to match the log format
log_pattern = re.compile(r"{.*}")


def extract_method(request_body):
try:
request_json = json.loads(request_body)
return request_json.get("method", "unknown")
except json.JSONDecodeError:
return "unknown"


def parse_log_file(log_file_path) -> dict:
# Read and parse the log file
stats = defaultdict(lambda: {"times": list()})

lines = (line for line in log_file_path.split("\n") if log_pattern.match(line))
log_entries = (json.loads(line) for line in lines)
formated_requests = (
{
"request_time": float(log_entry.get("request_time", 0)),
"method": extract_method(log_entry.get("jsonrpc_method", "")),
}
for log_entry in log_entries if extract_method(log_entry.get("jsonrpc_method", "")) != "unknown"
)
for formated_request in formated_requests:
method = formated_request["method"]
stats[method]["times"].append(formated_request["request_time"])
return stats


def calculate_stats(stats):
formated_stats = {key: {} for key in stats.keys()}
for method, data in stats.items():
formated_stats[method]["count"] = len(data["times"])
formated_stats[method]["average_time"] = statistics.mean(data["times"])
formated_stats[method]["max_time"] = max(data["times"])
formated_stats[method]["min_time"] = min(data["times"])
formated_stats[method]["median_time"] = statistics.median(data["times"])
return {k: v for k, v in sorted(formated_stats.items(), key=lambda item: item[1]["count"], reverse=True)}


@cli.command("parse_logs", help="Get logs from nginx")
@click.option("--solana_ip", default="localhost", help="Solana IP")
def parse_logs(solana_ip):
try:
content = requests.get(f"http://{solana_ip}:8080/logs/access.log").text
except requests.exceptions.InvalidURL as e:
print(f"Error: {e}")
sys.exit(1)
stats = parse_log_file(content)
calculated_stats = calculate_stats(stats)
df = pd.DataFrame.from_dict(calculated_stats, orient="index", columns=["count", "min_time", "max_time", "average_time", "median_time"])
print(df.to_markdown())


class GithubClient:

def __init__(self, token):
self.headers = {"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28"}

def remove_comment_with_title(self, pull_request, title):
response = requests.get(pull_request, headers=self.headers)
if response.status_code != 200:
raise RuntimeError(f"Attempt to get comments on a PR failed: {response.text}")
comments = response.json()
for comment in comments:
if f"<details>{title}" in comment["body"]:
response = requests.delete(comment["url"], headers=self.headers)
if response.status_code != 204:
raise RuntimeError(f"Attempt to delete a comment on a PR failed: {response.text} {response.request.url}")

def add_comment_to_pr(self, msg, pull_request, title = SOLANA_REQUESTS_TITLE, remove_previous_comments=True):
if remove_previous_comments:
self.remove_comment_with_title(pull_request, title)
message = f"\n\n{msg}\n\n"
if title:
message = f"<details>{title}\n\n{message}</details>"
data = {"body": message}
click.echo(f"Sent data: {data}")
click.echo(f"Headers: {self.headers}")
response = requests.post(pull_request, json=data, headers=self.headers)
click.echo(f"Status code: {response.status_code}")
if response.status_code != 201:
raise RuntimeError(f"Attempt to leave a comment on a PR failed: {response.text}")


@cli.command("post_comment", help="Post comment to the PR")
@click.option("--message", help="Message to post")
@click.option("--pull_request", help="Pull Request URL")
@click.option("--token", help="Github token")
def post_comment(message, pull_request, token):
gh_client = GithubClient(token)
gh_client.add_comment_to_pr(message, pull_request)


if __name__ == "__main__":
cli()
12 changes: 12 additions & 0 deletions .github/workflows/full_test_suite/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ resource "hcloud_server" "solana" {
data.hcloud_ssh_key.ci-ssh-key.id
]

provisioner "file" {
source = "../../../docker-compose/nginx.conf"
destination = "/tmp/nginx.conf"

connection {
type = "ssh"
user = "root"
host = hcloud_server.solana.ipv4_address
private_key = file("~/.ssh/ci-stands")
}
}

provisioner "file" {
source = "../../../docker-compose/docker-compose-ci.yml"
destination = "/tmp/docker-compose-ci.yml"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/full_test_suite/proxy_init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,14 @@ cd /tmp

# Set required environment variables
export REVISION=${proxy_image_tag}
export SOLANA_URL=http:\/\/${solana_ip}:8899
export SOLANA_URL=http:\/\/${solana_ip}:8080
export NEON_EVM_COMMIT=${neon_evm_commit}
export FAUCET_COMMIT=${faucet_model_commit}
export CI_PP_SOLANA_URL=${ci_pp_solana_url}
export DOCKERHUB_ORG_NAME=${dockerhub_org_name}
export PROXY_IMAGE_NAME=${proxy_image_name}



# Generate docker-compose override file
cat > docker-compose-ci.override.yml <<EOF
version: "3"
Expand Down Expand Up @@ -118,6 +117,7 @@ SOLANA_DATA='{"jsonrpc":"2.0","id":1,"method":"getHealth"}'
SOLANA_RESULT='"ok"'
wait_service "solana" $SOLANA_URL $SOLANA_DATA $SOLANA_RESULT


# Up all services
docker-compose -f docker-compose-ci.yml -f docker-compose-ci.override.yml up -d $SERVICES

Expand Down
21 changes: 18 additions & 3 deletions .github/workflows/full_test_suite/solana_init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,24 @@ services:
- "8900:8900"
- "8001:8001"
- "8001-8009:8001-8009/udp"

nginx:
image: nginx:latest
ports:
- "8080:8080"
expose:
- 8080
hostname: nginx
container_name: nginx
volumes:
- /var/log/nginx:/var/log/nginx
- /tmp/nginx.conf:/etc/nginx/nginx.conf
networks:
- net
entrypoint: >
/bin/sh -c "echo 'Nginx Configuration:' && cat /etc/nginx/nginx.conf && nginx -g 'daemon off;'"
EOF


# wake up Solana
docker-compose -f docker-compose-ci.yml -f docker-compose-ci.override.yml pull solana
docker-compose -f docker-compose-ci.yml -f docker-compose-ci.override.yml up -d solana
docker-compose -f docker-compose-ci.yml -f docker-compose-ci.override.yml pull nginx solana
docker-compose -f docker-compose-ci.yml -f docker-compose-ci.override.yml up -d nginx solana
40 changes: 40 additions & 0 deletions .github/workflows/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,45 @@ jobs:
solana_ip: ${{ needs.prepare-infrastructure.outputs.solana_ip }}
external_call: true

requests-report:
needs:
- prepare-infrastructure
- basic-tests
- build-image
- dapps-tests
- openzeppelin-tests
if: |
always() &&
contains(fromJSON('["success", "skipped"]'), needs.openzeppelin-tests.result) &&
contains(fromJSON('["success", "skipped"]'), needs.dapps-tests.result)
runs-on: test-runner
env:
SOLANA_IP: ${{ needs.prepare-infrastructure.outputs.solana_ip }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: "Request report"
id: request_report
run: |
stats=$(python3 ./.github/workflows/deploy.py parse_logs --solana_ip=${{ env.SOLANA_IP }})
echo "stats<<EOF" >> $GITHUB_OUTPUT
echo "$stats" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "stats=$stats"

if [[ "${{ github.event.inputs.initial_pr }}" != "" ]]; then
echo "pull_request=${{ github.event.inputs.initial_pr }}" >> $GITHUB_OUTPUT
else
echo "pull_request=${{ github.event.pull_request.issue_url }}/comments" >> $GITHUB_OUTPUT
fi;

- name: "Post a comment with the report"
run: |
python3 ./.github/workflows/deploy.py post_comment --message="${{ steps.request_report.outputs.stats }}" \
--pull_request="${{ steps.request_report.outputs.pull_request }}" \
--token=${{ secrets.GHTOKEN }}

# will be fixed by NDEV-2766
# economy-tests:
# if: needs.build-image.outputs.full_test_suite=='true'
Expand Down Expand Up @@ -391,6 +430,7 @@ jobs:
- prepare-infrastructure
- openzeppelin-tests
- basic-tests
- requests-report
- dapps-tests
- build-image
runs-on: test-runner
Expand Down
34 changes: 34 additions & 0 deletions docker-compose/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
events {
worker_connections 1024;
}

http {
log_format json_combined escape=json
'{ "@timestamp": "$time_iso8601", '
'"remote_addr": "$remote_addr", '
'"request": "$request", '
'"status": "$status", '
'"body_bytes_sent": "$body_bytes_sent", '
'"http_referer": "$http_referer", '
'"http_user_agent": "$http_user_agent", '
'"request_time": "$request_time", '
'"jsonrpc_method": "$request_body" }';

access_log /var/log/nginx/access.log json_combined buffer=8k flush=20s;

server {
listen 8080;

location / {
proxy_pass http://solana:8899;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location /logs {
alias /var/log/nginx/;
}
}
}
Loading