docs: add README.md and initial commit
- add code - add GitHub workflows - add GitHub repo files
This commit is contained in:
parent
ffc14cb984
commit
dff316c725
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve vAnalytics
|
||||
title: '[BUG] '
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
- OS: [e.g. Windows, macOS, Linux]
|
||||
- vAnalytics Version: [e.g. v1.4.2]
|
||||
- Python Version (if running from source): [e.g. 3.9]
|
||||
- vLLM version(s)
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here. Include any relevant log outputs or error messages.
|
||||
|
||||
**Checklist:**
|
||||
- [ ] I have checked the existing issues to make sure this is not a duplicate
|
||||
- [ ] I have included all relevant information to reproduce the issue
|
||||
- [ ] I am running the latest version of vAnalytics
|
|
@ -0,0 +1,8 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "sunday"
|
||||
open-pull-requests-limit: 10
|
|
@ -0,0 +1,20 @@
|
|||
name: Black
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.py'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.py'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: psf/black@stable
|
||||
with:
|
||||
options: "--check --verbose"
|
||||
src: "./src"
|
|
@ -0,0 +1,99 @@
|
|||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.txt'
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.txt'
|
||||
schedule:
|
||||
- cron: '21 20 * * 6'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: python
|
||||
build-mode: none
|
||||
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analysis step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||
'languages you are analyzing, replace this with the commands to build' \
|
||||
'your code, for example:'
|
||||
echo ' make bootstrap'
|
||||
echo ' make release'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
|
@ -0,0 +1,59 @@
|
|||
name: Dependency Audit
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**/requirements.txt'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/requirements.txt'
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # Run daily at midnight UTC
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pip-audit
|
||||
|
||||
- name: Run pip-audit
|
||||
run: |
|
||||
pip-audit -r requirements.txt > audit_output.txt
|
||||
continue-on-error: true
|
||||
|
||||
- name: Display audit results
|
||||
run: cat audit_output.txt
|
||||
|
||||
- name: Create detailed report
|
||||
run: |
|
||||
echo "Pip Audit Report" > detailed_report.txt
|
||||
echo "==================" >> detailed_report.txt
|
||||
echo "" >> detailed_report.txt
|
||||
echo "Date: $(date)" >> detailed_report.txt
|
||||
echo "" >> detailed_report.txt
|
||||
echo "Audit Results:" >> detailed_report.txt
|
||||
cat audit_output.txt >> detailed_report.txt
|
||||
echo "" >> detailed_report.txt
|
||||
echo "Environment:" >> detailed_report.txt
|
||||
python --version >> detailed_report.txt
|
||||
pip --version >> detailed_report.txt
|
||||
echo "" >> detailed_report.txt
|
||||
echo "Requirements:" >> detailed_report.txt
|
||||
cat requirements.txt >> detailed_report.txt
|
||||
|
||||
- name: Upload audit results
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: pip-audit-report
|
||||
path: detailed_report.txt
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
name: Pylint
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.py'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.py'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install $(grep -v "^torch" requirements.txt | tr '\n' ' ')
|
||||
pip install pylint
|
||||
- name: Analysing the code with pylint
|
||||
run: |
|
||||
pylint $(git ls-files '*.py') --disable=all --enable=E0001,E0100,E0101,E0102,E0103,E0104,E0105,E0107,E0108,E0110,E0111,E0112,E0113,E0114,E0115,E0116,E0117,E0118,E0202,E0203,E0211,E0213,E0236,E0237,E0238,E0239,E0240,E0241,E0301,E0302,E0303,E0401,E0402,E0701,E0702,E0703,E0704,E0710,E0711,E0712,E1003,E1101,E1102,E1111,E1120,E1121,E1123,E1124,E1125,E1126,E1127,E1128,E1129,E1130,E1131,E1132,E1133,E1134,E1135,E1136,E1137,E1138,E1139,E1200,E1201,E1205,E1206,E1300,E1301,E1302,E1303,E1304,E1305,E1306,E1310,E1700,E1701,W0311,W0312,W0611,W0612,W0613,W0702,W1401,W1402,C0123,C0200,C0325,C0411,C0412 --fail-under=5
|
|
@ -0,0 +1,72 @@
|
|||
name: Radon Code Metrics
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- '**.py'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.py'
|
||||
|
||||
jobs:
|
||||
radon:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install radon
|
||||
run: pip install radon
|
||||
|
||||
- name: Run radon
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
CHANGED_FILES=$(git ls-files '*.py')
|
||||
else
|
||||
CHANGED_FILES=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep '\.py$' || echo "")
|
||||
fi
|
||||
|
||||
echo "Files to be analyzed:"
|
||||
echo "$CHANGED_FILES"
|
||||
|
||||
if [ -n "$CHANGED_FILES" ]; then
|
||||
echo "Running Cyclomatic Complexity check..."
|
||||
radon cc $CHANGED_FILES -a -s -n F --exclude "AutoGGUF.quantize_model"
|
||||
|
||||
echo "Running Maintainability Index check..."
|
||||
radon mi $CHANGED_FILES -s -n F
|
||||
else
|
||||
echo "No Python files to analyze."
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
- name: Check radon output
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
CHANGED_FILES=$(git ls-files '*.py')
|
||||
else
|
||||
CHANGED_FILES=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep '\.py$' || echo "")
|
||||
fi
|
||||
|
||||
if [ -n "$CHANGED_FILES" ]; then
|
||||
CC_OUTPUT=$(radon cc $CHANGED_FILES -a -s -n F --exclude "AutoGGUF.quantize_model")
|
||||
MI_OUTPUT=$(radon mi $CHANGED_FILES -s -n F)
|
||||
|
||||
if [ -n "$CC_OUTPUT" ] || [ -n "$MI_OUTPUT" ]; then
|
||||
echo "Radon detected code complexity or maintainability issues:"
|
||||
[ -n "$CC_OUTPUT" ] && echo "$CC_OUTPUT"
|
||||
[ -n "$MI_OUTPUT" ] && echo "$MI_OUTPUT"
|
||||
exit 1
|
||||
else
|
||||
echo "No code complexity or maintainability issues detected."
|
||||
fi
|
||||
else
|
||||
echo "No Python files to analyze."
|
||||
fi
|
|
@ -129,6 +129,7 @@ venv/
|
|||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
.idea/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.10.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: v1.1.9
|
||||
hooks:
|
||||
- id: remove-crlf
|
|
@ -0,0 +1 @@
|
|||
# Changelog
|
|
@ -0,0 +1,127 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement in the Discussions tab.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
|
@ -0,0 +1,62 @@
|
|||
# Contributing to vAnalytics
|
||||
|
||||
First off, thanks for taking the time to contribute! 🎉👍
|
||||
|
||||
## How Can I Contribute?
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
- Use the issue tracker to report bugs
|
||||
- Describe the bug in detail
|
||||
- Include screenshots if possible
|
||||
|
||||
### Suggesting Enhancements
|
||||
|
||||
- Use the issue tracker to suggest enhancements
|
||||
- Explain why this enhancement would be useful
|
||||
|
||||
### Your First Code Contribution
|
||||
|
||||
You can find issues labeled with "good first issue" in the Issues tab as a starting point. Code refactors and optimizations are also appreciated, although if there's a vulnrability please report it privately in the Security tab. For feature PRs, please make a discussion first to make sure your feature can be added and continously maintained.
|
||||
|
||||
1. Fork the repo
|
||||
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
||||
3. Install pre-commit: (`pip install pre-commit`)
|
||||
4. Set up the git hook scripts: (`pre-commit install`)
|
||||
5. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
||||
6. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||
7. Open a Pull Request
|
||||
|
||||
## Styleguides
|
||||
|
||||
### Git Commit Messages
|
||||
|
||||
- Use the present tense ("Add feature" not "Added feature")
|
||||
- Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
|
||||
- Limit the first line to 72 characters or fewer
|
||||
|
||||
### Commit Types:
|
||||
|
||||
```
|
||||
feat: Added new feature
|
||||
fix: Fixed a bug
|
||||
docs: Updated documentation
|
||||
style: Code style changes (formatting, etc.)
|
||||
refactor: Code refactoring
|
||||
perf: Performance improvements
|
||||
test: Added or modified tests
|
||||
build: Changes to build system or external dependencies
|
||||
ci: Changes to CI configuration files and scripts
|
||||
chore: Other changes that don't modify src or test files
|
||||
```
|
||||
|
||||
### Python Styleguide
|
||||
|
||||
- Follow PEP 8
|
||||
- Please use Black to format your code first
|
||||
- Use meaningful variable names
|
||||
- Comment your code, but don't overdo it
|
||||
|
||||
## Questions?
|
||||
|
||||
Feel free to contact the project maintainers if you have any questions.
|
2
LICENSE
2
LICENSE
|
@ -186,7 +186,7 @@
|
|||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
Copyright 2024 leafspark
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
39
README.md
39
README.md
|
@ -1,2 +1,37 @@
|
|||
# vAnalytics
|
||||
time series analytics for vLLM
|
||||
# vAnalytics - time series analytics for vLLM
|
||||
|
||||
<!-- Project Status -->
|
||||
[](https://github.com/leafspark/vAnalytics/releases)
|
||||
[](https://github.com/leafspark/vAnalytics/commits)
|
||||
[]()
|
||||
|
||||
<!-- Project Info -->
|
||||
[](https://github.com/ggerganov/llama.cpp)
|
||||

|
||||
[]()
|
||||
[](https://github.com/leafspark/vAnalytics/blob/main/LICENSE)
|
||||
|
||||
<!-- Repository Stats -->
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
<!-- Contribution -->
|
||||
[](https://github.com/psf/black)
|
||||
[](https://github.com/leafspark/vAnalytics/issues)
|
||||
[](https://github.com/leafspark/vAnalytics/pulls)
|
||||
|
||||
vAnalytics provides a web interface to help easily monitor vLLM instance metrics. It allows users to easily monitor multiple vLLM instances, as well as being easy to setup and configure.
|
||||
|
||||
## Features
|
||||
- Specify vLLM backends easily using name and host configuration
|
||||
- Uses SQLite for easy database management
|
||||
- Intuitive and includes error handling
|
||||
- Flexible schemas and data plotting using Plotly
|
||||
|
||||
## Usage
|
||||
Configure your instances in monitor.py, then use `python src/monitor.py`. This will start monitoring in a `/data` folder, where it will store SQLite databases with your model name.
|
||||
|
||||
To start the web interface, execute `python src/graph.py`. The web interface is avaliable at `localhost:4412`.
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
|-----------------|--------------------|
|
||||
| stable (v1.0.0) | :white_check_mark: |
|
||||
|
||||
Beta versions are not supported, and may have unknown security issues.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Use the Issues tab, or for severe vulnerabilities please contact the maintainers via email.
|
|
@ -0,0 +1,7 @@
|
|||
numpy~=1.26.4
|
||||
uvicorn~=0.30.6
|
||||
requests~=2.32.3
|
||||
pandas~=2.2.3
|
||||
plotly~=5.24.1
|
||||
flask~=3.0.3
|
||||
zstd~=1.5.5.1
|
|
@ -0,0 +1,12 @@
|
|||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='vAnalytics',
|
||||
version='v1.0.0',
|
||||
packages=[''],
|
||||
url='https://github.com/leafspark/vAnalytics',
|
||||
license='apache-2.0',
|
||||
author='leafspark',
|
||||
author_email='',
|
||||
description='time series analytics for vLLM'
|
||||
)
|
|
@ -0,0 +1,138 @@
|
|||
import os
|
||||
import json
|
||||
import argparse
|
||||
import sqlite3
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
def get_latest_rows(db_file, hours=1):
|
||||
now = datetime.now()
|
||||
one_hour_ago = now - timedelta(hours=hours)
|
||||
one_hour_ago_timestamp = int(one_hour_ago.timestamp())
|
||||
|
||||
conn = sqlite3.connect(db_file)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(
|
||||
f"SELECT timestamp, data FROM json_data WHERE timestamp >= {one_hour_ago_timestamp} ORDER BY timestamp DESC"
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
conn.close()
|
||||
|
||||
if not rows:
|
||||
print(
|
||||
f"No rows found in the last {hours} hour(s). Showing info for last 5 rows:"
|
||||
)
|
||||
conn = sqlite3.connect(db_file)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
f"SELECT timestamp, data FROM json_data ORDER BY timestamp DESC LIMIT 5"
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
for timestamp, _ in rows:
|
||||
print(f" {datetime.fromtimestamp(timestamp)}")
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
def extract_stats(data_json):
|
||||
try:
|
||||
data = json.loads(data_json)
|
||||
total_prompt_tokens = float(data["vllm:prompt_tokens_total"][0]["value"])
|
||||
total_generation_tokens = float(
|
||||
data["vllm:generation_tokens_total"][0]["value"]
|
||||
)
|
||||
total_requests = sum(
|
||||
float(item["value"]) for item in data["vllm:request_success_total"]
|
||||
)
|
||||
avg_prompt_throughput = float(
|
||||
data["vllm:avg_prompt_throughput_toks_per_s"][0]["value"]
|
||||
)
|
||||
avg_generation_throughput = float(
|
||||
data["vllm:avg_generation_throughput_toks_per_s"][0]["value"]
|
||||
)
|
||||
gpu_cache_usage_perc = float(data["vllm:gpu_cache_usage_perc"][0]["value"])
|
||||
num_requests_running = float(data["vllm:num_requests_running"][0]["value"])
|
||||
|
||||
except (KeyError, IndexError, json.JSONDecodeError) as e:
|
||||
print(f"Error extracting stats from data: {str(e)}")
|
||||
return None
|
||||
|
||||
return {
|
||||
"total_prompt_tokens": total_prompt_tokens,
|
||||
"total_generation_tokens": total_generation_tokens,
|
||||
"total_requests": total_requests,
|
||||
"avg_prompt_throughput": avg_prompt_throughput,
|
||||
"avg_generation_throughput": avg_generation_throughput,
|
||||
"gpu_cache_usage_perc": gpu_cache_usage_perc,
|
||||
"num_requests_running": num_requests_running,
|
||||
}
|
||||
|
||||
|
||||
def main(db_file, hours):
|
||||
latest_rows = get_latest_rows(db_file, hours)
|
||||
|
||||
if not latest_rows:
|
||||
print(f"No rows found for the last {hours} hour(s).")
|
||||
return
|
||||
|
||||
print(f"Processing {len(latest_rows)} rows.")
|
||||
|
||||
valid_stats = [
|
||||
extract_stats(data)
|
||||
for _, data in latest_rows
|
||||
if extract_stats(data) is not None
|
||||
]
|
||||
|
||||
if not valid_stats:
|
||||
print("No valid statistics could be extracted from the rows.")
|
||||
return
|
||||
|
||||
first_stats = valid_stats[-1] # Oldest row
|
||||
last_stats = valid_stats[0] # Newest row
|
||||
|
||||
tokens_processed = (
|
||||
last_stats["total_prompt_tokens"]
|
||||
- first_stats["total_prompt_tokens"]
|
||||
+ last_stats["total_generation_tokens"]
|
||||
- first_stats["total_generation_tokens"]
|
||||
)
|
||||
requests_processed = last_stats["total_requests"] - first_stats["total_requests"]
|
||||
|
||||
avg_prompt_throughput = sum(
|
||||
stat["avg_prompt_throughput"] for stat in valid_stats
|
||||
) / len(valid_stats)
|
||||
avg_generation_throughput = sum(
|
||||
stat["avg_generation_throughput"] for stat in valid_stats
|
||||
) / len(valid_stats)
|
||||
avg_num_requests_running = sum(
|
||||
stat["num_requests_running"] for stat in valid_stats
|
||||
) / len(valid_stats)
|
||||
avg_gpu_cache_usage_perc = sum(
|
||||
stat["gpu_cache_usage_perc"] for stat in valid_stats
|
||||
) / len(valid_stats)
|
||||
|
||||
print(f"\nStats for the last {hours} hour(s):")
|
||||
print(f"Tokens processed: {tokens_processed:,.0f}")
|
||||
print(f"Requests processed: {requests_processed:,.0f}")
|
||||
print(f"Average prompt throughput: {avg_prompt_throughput:.2f} tokens/s")
|
||||
print(f"Average generation throughput: {avg_generation_throughput:.2f} tokens/s")
|
||||
print(
|
||||
f"Average number of requests running: {avg_num_requests_running:.2f} requests"
|
||||
)
|
||||
print(f"Average GPU cache usage percent: {avg_gpu_cache_usage_perc * 100:.2f}%")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Extract stats from a SQLite database for a specified time period"
|
||||
)
|
||||
parser.add_argument("db_file", help="Path to the SQLite database file")
|
||||
parser.add_argument(
|
||||
"--hours", type=int, default=1, help="Number of hours to look back (default: 1)"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args.db_file, args.hours)
|
|
@ -0,0 +1,323 @@
|
|||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import numpy as np
|
||||
import os
|
||||
import pandas as pd
|
||||
import plotly.graph_objects as go
|
||||
import plotly.offline as pyo
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Flask, render_template, request, send_file
|
||||
from functools import lru_cache
|
||||
from plotly.subplots import make_subplots
|
||||
from plotly.subplots import make_subplots
|
||||
from scipy.interpolate import make_interp_spline
|
||||
|
||||
# Set up logging with a higher level
|
||||
logging.basicConfig(
|
||||
level=logging.WARNING, # Changed from DEBUG to WARNING
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
|
||||
# Global variable to store the cached data
|
||||
cached_data = {}
|
||||
last_modified_times = {}
|
||||
|
||||
|
||||
async def load_data_from_db(filepath):
|
||||
global cached_data
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(filepath)
|
||||
cursor = conn.cursor()
|
||||
|
||||
twenty_four_hours_ago = datetime.now() - timedelta(hours=24)
|
||||
timestamp_24h_ago = int(twenty_four_hours_ago.timestamp())
|
||||
|
||||
cursor.execute(
|
||||
"SELECT data, timestamp FROM json_data WHERE timestamp >= ?",
|
||||
(timestamp_24h_ago,),
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
model_name = os.path.splitext(os.path.basename(filepath))[0]
|
||||
|
||||
# Optimize data structure creation
|
||||
new_data = {}
|
||||
for row in rows:
|
||||
data = json.loads(row[0])
|
||||
timestamp = datetime.fromtimestamp(row[1])
|
||||
|
||||
for metric_name, metric_data in data.items():
|
||||
if metric_name not in new_data:
|
||||
new_data[metric_name] = {}
|
||||
if model_name not in new_data[metric_name]:
|
||||
new_data[metric_name][model_name] = []
|
||||
new_data[metric_name][model_name].append((timestamp, metric_data))
|
||||
|
||||
# Update cached_data efficiently
|
||||
for metric_name, model_data in new_data.items():
|
||||
if metric_name not in cached_data:
|
||||
cached_data[metric_name] = model_data
|
||||
else:
|
||||
cached_data[metric_name].update(model_data)
|
||||
|
||||
except sqlite3.Error as e:
|
||||
logging.error(f"SQLite error in {filepath}: {e}")
|
||||
except json.JSONDecodeError as e:
|
||||
logging.error(f"JSON decode error in {filepath}: {e}")
|
||||
except Exception as e:
|
||||
logging.error(f"Error processing file {filepath}: {e}")
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
async def load_data():
|
||||
data_dir = "./data"
|
||||
|
||||
tasks = []
|
||||
for filename in os.listdir(data_dir):
|
||||
if filename.endswith(".sqlite"):
|
||||
filepath = os.path.join(data_dir, filename)
|
||||
tasks.append(load_data_from_db(filepath))
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
logging.info(f"Loaded data for {len(cached_data)} metrics")
|
||||
if len(cached_data) == 0:
|
||||
logging.warning(
|
||||
"No data was loaded. Check if SQLite files exist and contain recent data."
|
||||
)
|
||||
|
||||
|
||||
async def background_data_loader():
|
||||
while True:
|
||||
await load_data()
|
||||
await asyncio.sleep(30) # Check for updates every 30 seconds
|
||||
|
||||
|
||||
def start_background_loop(loop):
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_forever()
|
||||
|
||||
|
||||
# Start the background data loader
|
||||
threading.Thread(target=background_data_loader, daemon=True).start()
|
||||
|
||||
|
||||
def create_trace(model_name, metric_name, data_points, row, col):
|
||||
return (
|
||||
go.Scattergl(
|
||||
x=[point[0] for point in data_points],
|
||||
y=[point[1] for point in data_points],
|
||||
mode="lines",
|
||||
name=f"{model_name} - {metric_name}",
|
||||
),
|
||||
row,
|
||||
col,
|
||||
)
|
||||
|
||||
|
||||
def create_plots(selected_model):
|
||||
global cached_data
|
||||
start_time = time.time()
|
||||
|
||||
all_data = {}
|
||||
selected_models = selected_model.split(",")
|
||||
for metric, data in cached_data.items():
|
||||
all_data[metric] = {
|
||||
model: data[model] for model in selected_models if model in data
|
||||
}
|
||||
|
||||
data_prep_time = time.time() - start_time
|
||||
print(f"Data preparation took {data_prep_time:.2f} seconds")
|
||||
|
||||
num_metrics = len(all_data)
|
||||
if num_metrics == 0:
|
||||
logging.warning("No valid data found.")
|
||||
return None
|
||||
|
||||
num_cols = 2
|
||||
num_rows = (num_metrics + num_cols - 1) // num_cols
|
||||
fig = make_subplots(
|
||||
rows=num_rows, cols=num_cols, subplot_titles=list(all_data.keys())
|
||||
)
|
||||
|
||||
subplot_creation_time = time.time() - start_time - data_prep_time
|
||||
print(f"Subplot creation took {subplot_creation_time:.2f} seconds")
|
||||
|
||||
now = datetime.now()
|
||||
twenty_four_hours_ago = now - timedelta(hours=24)
|
||||
|
||||
trace_creation_start = time.time()
|
||||
|
||||
with ThreadPoolExecutor() as executor:
|
||||
futures = []
|
||||
for index, (metric_name, model_data) in enumerate(all_data.items()):
|
||||
row = index // num_cols + 1
|
||||
col = index % num_cols + 1
|
||||
|
||||
for model_name, metric_data_list in model_data.items():
|
||||
if isinstance(metric_data_list[0][1], list):
|
||||
for label_set in metric_data_list[0][1]:
|
||||
data_points = []
|
||||
for timestamp, metric_data in metric_data_list:
|
||||
if timestamp >= twenty_four_hours_ago:
|
||||
for data_point in metric_data:
|
||||
if data_point["labels"] == label_set["labels"]:
|
||||
try:
|
||||
value = float(data_point["value"])
|
||||
data_points.append((timestamp, value))
|
||||
except ValueError:
|
||||
logging.warning(
|
||||
f"Invalid numeric value for {model_name} - {metric_name}: {data_point['value']}"
|
||||
)
|
||||
if not data_points:
|
||||
continue
|
||||
data_points.sort(key=lambda x: x[0])
|
||||
futures.append(
|
||||
executor.submit(
|
||||
create_trace,
|
||||
model_name,
|
||||
str(label_set["labels"]),
|
||||
data_points,
|
||||
row,
|
||||
col,
|
||||
)
|
||||
)
|
||||
else:
|
||||
data_points = []
|
||||
for timestamp, metric_data in metric_data_list:
|
||||
if timestamp >= twenty_four_hours_ago:
|
||||
try:
|
||||
value = float(metric_data)
|
||||
data_points.append((timestamp, value))
|
||||
except ValueError:
|
||||
logging.warning(
|
||||
f"Invalid numeric value for {model_name} - {metric_name}: {metric_data}"
|
||||
)
|
||||
if not data_points:
|
||||
continue
|
||||
data_points.sort(key=lambda x: x[0])
|
||||
futures.append(
|
||||
executor.submit(
|
||||
create_trace, model_name, metric_name, data_points, row, col
|
||||
)
|
||||
)
|
||||
|
||||
for future in as_completed(futures):
|
||||
trace, row, col = future.result()
|
||||
fig.add_trace(trace, row=row, col=col)
|
||||
|
||||
trace_creation_time = time.time() - trace_creation_start
|
||||
print(f"Trace creation took {trace_creation_time:.2f} seconds")
|
||||
|
||||
layout_update_start = time.time()
|
||||
fig.update_layout(
|
||||
height=300 * num_rows,
|
||||
showlegend=True,
|
||||
template="plotly_dark",
|
||||
font=dict(family="Arial", size=10, color="white"),
|
||||
paper_bgcolor="rgb(30, 30, 30)",
|
||||
plot_bgcolor="rgb(30, 30, 30)",
|
||||
)
|
||||
fig.update_xaxes(title_text="Time", tickformat="%Y-%m-%d %H:%M:%S")
|
||||
fig.update_yaxes(title_text="Value")
|
||||
fig.update_traces(hovertemplate="%{x|%Y-%m-%d %H:%M:%S}<br>%{y:.3f}")
|
||||
|
||||
layout_update_time = time.time() - layout_update_start
|
||||
print(f"Layout update took {layout_update_time:.2f} seconds")
|
||||
|
||||
total_time = time.time() - start_time
|
||||
print(f"Total plot creation took {total_time:.2f} seconds")
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route("/", methods=["GET", "POST"])
|
||||
def index():
|
||||
data_dir = "./data"
|
||||
model_names = [
|
||||
name[:-7]
|
||||
for name in os.listdir(data_dir)
|
||||
if name.endswith(".sqlite") and os.path.isfile(os.path.join(data_dir, name))
|
||||
]
|
||||
|
||||
if request.method == "POST":
|
||||
selected_model = request.form.get("model_select")
|
||||
else:
|
||||
selected_model = model_names[0] if model_names else None
|
||||
|
||||
plot_div = None
|
||||
error_message = None
|
||||
if selected_model:
|
||||
try:
|
||||
fig = create_plots(selected_model)
|
||||
if fig is not None:
|
||||
fig.update_layout(showlegend=False)
|
||||
plot_div = pyo.plot(fig, output_type="div", include_plotlyjs=True)
|
||||
else:
|
||||
error_message = "No data available for the selected model."
|
||||
except Exception as e:
|
||||
logging.error(f"Error creating plot: {str(e)}")
|
||||
error_message = (
|
||||
"An error occurred while creating the plot. Please try again later."
|
||||
)
|
||||
|
||||
command = [
|
||||
"python",
|
||||
"get_data.py",
|
||||
"--hours",
|
||||
"24",
|
||||
f".\\data\\{selected_model}.sqlite",
|
||||
]
|
||||
|
||||
result = subprocess.run(command, capture_output=True, text=True)
|
||||
else:
|
||||
result = None
|
||||
|
||||
return render_template(
|
||||
"index.html",
|
||||
plot_div=plot_div,
|
||||
model_name=selected_model,
|
||||
model_names=model_names,
|
||||
result=result.stdout if result else None,
|
||||
error_message=error_message,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/favicon.ico")
|
||||
def favicon():
|
||||
return send_file("favicon.ico", mimetype="image/vnd.microsoft.icon")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
from asgiref.wsgi import WsgiToAsgi
|
||||
|
||||
# Initial data load
|
||||
logging.info("Starting initial data load")
|
||||
asyncio.run(load_data())
|
||||
logging.info("Initial data load complete")
|
||||
|
||||
# Create a new event loop for the background task
|
||||
loop = asyncio.new_event_loop()
|
||||
|
||||
def start_background_loop():
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.create_task(background_data_loader())
|
||||
loop.run_forever()
|
||||
|
||||
t = threading.Thread(target=start_background_loop, daemon=True)
|
||||
t.start()
|
||||
|
||||
asgi_app = WsgiToAsgi(app)
|
||||
uvicorn.run(asgi_app, host="0.0.0.0", port=4421)
|
|
@ -0,0 +1,140 @@
|
|||
import requests
|
||||
import time
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import zstd
|
||||
import sqlite3
|
||||
|
||||
print("Starting monitor.")
|
||||
|
||||
# Set up basic configuration for logging
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
filename="monitor.log", # Log to a file named monitor.log
|
||||
filemode="a",
|
||||
) # Append to the log file
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
|
||||
# Model information
|
||||
models = {
|
||||
"Example-Model-22B-FP8-dynamic": "http://112.83.15.44:8883",
|
||||
"Mistral-7B-bf16": "http://57.214.142.199:8090",
|
||||
}
|
||||
|
||||
|
||||
def call_metrics_endpoint(model_name, base_url):
|
||||
url = f"{base_url}/metrics"
|
||||
logging.debug(f"Calling metrics endpoint: {url}")
|
||||
try:
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
|
||||
}
|
||||
response = requests.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
logging.debug(f"Received successful response from {url}")
|
||||
return response.text
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error(f"Error calling {url}: {e}")
|
||||
return f"Error calling {url}: {e}"
|
||||
|
||||
|
||||
def normalize_metrics(metrics_data):
|
||||
"""Normalizes the metrics data from vLLM."""
|
||||
normalized_data = {}
|
||||
lines = metrics_data.strip().split("\n")
|
||||
for line in lines:
|
||||
if line.startswith("#"): # Ignore comment lines
|
||||
continue
|
||||
parts = line.split(" ")
|
||||
metric_name = parts[0]
|
||||
metric_value_str = parts[1]
|
||||
|
||||
# Try to convert to decimal, otherwise keep as string
|
||||
try:
|
||||
metric_value = float(metric_value_str)
|
||||
if metric_name.endswith("_total") or metric_name.endswith("_count"):
|
||||
metric_value = int(metric_value)
|
||||
elif "e+" in metric_value_str or "e-" in metric_value_str:
|
||||
metric_value = "{:.10f}".format(metric_value)
|
||||
except ValueError:
|
||||
metric_value = metric_value_str
|
||||
|
||||
# Extract labels from metric name
|
||||
if "{" in metric_name:
|
||||
metric_name, labels_str = metric_name[:-1].split("{")
|
||||
labels = {}
|
||||
for label_pair in labels_str.split(","):
|
||||
key, value = label_pair.split("=")
|
||||
labels[key.strip('"')] = value.strip('"')
|
||||
if metric_name not in normalized_data:
|
||||
normalized_data[metric_name] = []
|
||||
normalized_data[metric_name].append(
|
||||
{"labels": labels, "value": metric_value}
|
||||
)
|
||||
else:
|
||||
normalized_data[metric_name] = metric_value
|
||||
|
||||
return normalized_data
|
||||
|
||||
|
||||
def log_response(model_name, response_data):
|
||||
timestamp = int(datetime.now().timestamp())
|
||||
normalized_data = normalize_metrics(response_data)
|
||||
|
||||
db_filename = f"./data/{model_name}.sqlite"
|
||||
os.makedirs(os.path.dirname(db_filename), exist_ok=True)
|
||||
|
||||
max_retries = 3
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
conn = sqlite3.connect(db_filename)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create table if it doesn't exist
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS json_data
|
||||
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
data TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL)
|
||||
"""
|
||||
)
|
||||
|
||||
# Insert the data
|
||||
cursor.execute(
|
||||
"INSERT INTO json_data (data, timestamp) VALUES (?, ?)",
|
||||
(json.dumps(normalized_data), timestamp),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logging.debug(f"Saved metrics data to {db_filename}")
|
||||
break # Exit the retry loop if successful
|
||||
except sqlite3.OperationalError as e:
|
||||
if "database is locked" in str(e):
|
||||
logging.warning(
|
||||
f"Database locked for {model_name}, retrying in 5 seconds... (Attempt {retry+1}/{max_retries})"
|
||||
)
|
||||
time.sleep(5) # Wait before retrying
|
||||
else:
|
||||
logging.error(f"Error writing to database for {model_name}: {e}")
|
||||
break # Exit the retry loop for other errors
|
||||
|
||||
|
||||
while True:
|
||||
for model_name, base_url in models.items():
|
||||
response_data = call_metrics_endpoint(model_name, base_url)
|
||||
if response_data and not response_data.startswith(
|
||||
"Error"
|
||||
): # Check for valid data
|
||||
logging.info(f"Metrics for {model_name} valid") # Log metrics to console
|
||||
log_response(model_name, response_data)
|
||||
|
||||
logging.debug("Waiting for 30 seconds...")
|
||||
time.sleep(30)
|
Loading…
Reference in New Issue