Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
053fcdc
OSN-1281. Add useEffect for value update
DoRightt Jan 20, 2026
327a9d1
OSN-1284. When selecting the Data Source filter, the perspective in s…
DoRightt Jan 20, 2026
92414aa
Pull request update/260120
stanfra Jan 20, 2026
37e806e
OSN-1265. Regenerated demo
nk-hystax Jan 23, 2026
0776dac
OSN-1266. Remove obsolete_images from the recommendation page
ka-hystax Jan 23, 2026
970c664
OSN-1287: update auto-assign workflow configuration
Jan 23, 2026
511a835
Reapply vm.sh + fixes
nexusriot Jan 26, 2026
ab4c596
OSN-1287: update auto-assign workflow configuration (#828)
km-hystax Jan 27, 2026
eaffb1c
Pull request update/260126
stanfra Jan 28, 2026
7059af2
OSN-1260. Rename Components folder to components
ek-hystax Jan 30, 2026
ed7f0a4
OSN-1322. Mark expired Power Schedules
DoRightt Jan 30, 2026
55a488c
Pull request update/260130
stanfra Jan 30, 2026
8f0a2e0
OSN-1228. Include GraphQL operation name in /api requests
ek-hystax Feb 2, 2026
9f01b93
OSN-1308. Fix "Lodash has Prototype Pollution Vulnerability in `_.uns…
ek-hystax Feb 2, 2026
59e4008
OSN-1323: fixed optscale-info output
nexusriot Feb 2, 2026
d6a2fce
OSN-1331. Fixed Alibaba LB discovery on LBs in one region
nk-hystax Feb 2, 2026
466c8cc
Pull request update/260203
stanfra Feb 3, 2026
4b0c604
OSN-1332. Updated CODEOWNERS
ek-hystax Feb 4, 2026
1e5919d
OSN-1303. Update react-router-dom to 6.30.3
ek-hystax Feb 4, 2026
2322919
OSN-1306. Updated storybook packages
ek-hystax Feb 4, 2026
e85264f
OSN-1300. Update dependencies to address qs vulnerability
ek-hystax Feb 4, 2026
cd33598
OSN-1296. Update jspdf and jspdf-autotable
ek-hystax Feb 4, 2026
4b6d535
OSN-1302. vite allows server.fs.deny bypass via backslash on Windows
DoRightt Feb 4, 2026
1ea86d2
OSN-1334. Fixed couldn't convert string to float: '' on instance_subs…
nk-hystax Feb 5, 2026
b0c19a8
OSN-1300. [jira-ui] Update body-parser to 1.20.4
ek-hystax Feb 5, 2026
d5a2258
OSN-1319. Update markdown packages
ek-hystax Feb 5, 2026
2ced549
OSN-1336. Fixed not cleaning some indexes on cleanelkdb
nk-hystax Feb 6, 2026
ca102d7
OSN-1333. Enhance instance subscription description with dynamic days…
ek-hystax Feb 6, 2026
8327e7f
Pull request update/260206
stanfra Feb 6, 2026
5fb4d3a
OSN-1314. Update js-yaml to 4.1.1
ek-hystax Feb 9, 2026
e1a8552
OSN-1305. Fix "glob CLI: Command injection via -c/--cmd executes matc…
ka-hystax Feb 10, 2026
4ca3a26
OSS-211. Use cloud adapter in gemini
nk-hystax Feb 16, 2026
59e2a2d
OSN-1335. Updated @apollo-server
ka-hystax Feb 17, 2026
d0de61a
OSN-1327. Integrate @hystax/eslint-config-ui
ka-hystax Feb 17, 2026
c29bcd6
OSN-1347. Fix not getting files in gemini
nk-hystax Feb 17, 2026
803a88a
Pull request update/260218
stanfra Feb 18, 2026
60e98ee
Update ReadMe - adding a block with a Quote
mirlena777 Feb 19, 2026
df31a37
OSN-1342. Update axios to 1.13.5
ek-hystax Feb 24, 2026
db76517
OSN-1349. Fixed gemini exception on many buckets
sd-hystax Feb 27, 2026
1475950
OSN-1358. Decrease available bucket count for selection to one run to 10
ka-hystax Mar 2, 2026
d7d31f0
Pull request update/260302
stanfra Mar 2, 2026
3962150
Update ReadMe description
mirlena777 Mar 4, 2026
e057985
Pull request update/260310
nexusriot Mar 10, 2026
661c043
feat: Add open telemetry integration to the rest_api
arturbalabanov Mar 10, 2026
78cf73e
OSN-1368. Increased last_error text length for more get more error in…
asm-hystax Mar 10, 2026
1a03a2d
OSN-1369. Added new user options APIs, new user_options table, unitte…
asm-hystax Mar 11, 2026
f4112d6
OSN-1372. Updated swagger errors, added option value json validation …
asm-hystax Mar 12, 2026
e1ce300
OSN-1372-fix. Added additional fixes to swagger, except all json conv…
asm-hystax Mar 12, 2026
90acf7c
Pull request update/260313
stanfra Mar 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 3 additions & 3 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Frontend only for ngui
# Frontend
ngui/ @hystax/ui
jira_ui/ @hystax/ui

# Backend for other directories
# Backend
auth/ @hystax/backend
bi_exporter/ @hystax/backend
bumischeduler/ @hystax/backend
Expand All @@ -13,7 +14,6 @@ gemini/ @hystax/backend
herald/ @hystax/backend
insider/ @hystax/backend
jira_bus/ @hystax/backend
jira_ui/ @hystax/backend
katara/ @hystax/backend
keeper/ @hystax/backend
metroculus/ @hystax/backend
Expand Down
48 changes: 41 additions & 7 deletions .github/workflows/auto-assign-author.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,50 @@ jobs:
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write # Required because assignees API works via issues
contents: read # Minimal read access to repository contents

steps:
- name: Assign PR author
uses: actions/github-script@v8
with:
script: |
const reporter = context.actor
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
assignees: [reporter]
})
// Repository owner (organization or user who owns the repo)
const owner = context.repo.owner;

// Repository name
const repo = context.repo.repo;

// Login of the pull request author
const prAuthor = context.payload.pull_request.user.login;

try {
// Check the permission level of the PR author
const { data: perm } =
await github.rest.repos.getCollaboratorPermissionLevel({
owner,
repo,
username: prAuthor
});

// If the author has write/maintain/admin rights → assign them to the PR
if (["write", "maintain", "admin"].includes(perm.permission)) {
await github.rest.issues.addAssignees({
owner,
repo,
issue_number: context.payload.pull_request.number,
assignees: [prAuthor]
});
console.log(`Assigned PR to ${prAuthor}`);
} else {
// If the author has insufficient rights → skip assignment
console.log(
`Skipping assignment for ${prAuthor}: permission=${perm.permission}`
);
}
} catch (error) {
// If the author is not a collaborator (e.g., PR from a fork) → skip without failing
console.log(
`Skipping assignment for ${prAuthor}: not a collaborator`
);
}
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ rest_api/.clickhouse
**/.pytest_cache/
**/.ruff_cache/
# Build files
**/build
**/dist
**/*egg-info/
**/*.tar.gz

# jira_ui
jira_ui/*/.env
jira_ui/*/node_modules
jira_ui/*/build/
jira_ui/*/build/

# Vagrant
optscale-deploy/.vagrant/
66 changes: 52 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
⭐ Drop a star to support OptScale ⭐
</p>

# FinOps and cloud cost management platform to run any cloud workload with optimal performance and cost
# Open-Source FinOps & Cloud Cost Optimization Platform

<p align="center">
<a href="documentation/images/cover-GitHub.png"><img src="documentation/images/FinOps-platform.png" width="40%" align="middle"></a>
</p>
OptScale is an open source FinOps platform that optimizes cloud costs and performance for any workload, providing effective cloud cost management for all types of organizations.

<br>OptScale is an open-source [FinOps and cloud cost optimization platform](https://hystax.com/optscale/finops-overview/) that helps engineering and finance teams control and reduce spend across AWS, Microsoft Azure, GCP, Alibaba Cloud, and Kubernetes clusters.
It provides deep visibility into infrastructure costs, automated optimization recommendations, and governance tools for R&D and data platforms.

<br>
<br>
<p align="center">
Expand All @@ -29,21 +32,56 @@ OptScale is an open source FinOps platform that optimizes cloud costs and perfor
![Average cloud cost savings](https://img.shields.io/badge/Average_cloud_cost_savings-38%25-yellow)

</div>

<br>

<div>
<br>
<img src="documentation/images/Max_Kuzkin.png" width="80" align="left" style="border-radius: 50%; margin-right: 15px">
<i>
“Hystax OptScale has been a game-changer for our FinOps practice. Its powerful capabilities, flexibility, and seamless integration have empowered us to deliver unprecedented transparency, control, and cost optimization for our clients. We truly value our partnership with Hystax and are excited to innovate further together.”
</i>
<div align="right">
<i><b>Max Kuzkin</b>, General Manager, SoftwareOne Platform</i>
</div>
</div>

<br>

<br>

## OptScale FinOps and cloud cost optimization capabilities
## Overview
OptScale connects to your cloud accounts and Kubernetes clusters, ingests billing and usage data, and analyzes infrastructure consumption to surface actionable insights that eliminate waste and optimize resource usage.
It supports multi-cloud environments and integrates with popular data platforms, including Databricks, Amazon S3, and Amazon Redshift.

<br>

## Key Features
### Cost optimization
<li>Unused and idle resource detection for VMs, volumes, databases, and other cloud resources</li>
<li>Rightsizing recommendations for overprovisioned instances and workloads</li>
<li>R&D resource power management to automatically stop non-production environments outside working hours</li>
<li>Commitment utilization analysis for Reserved Instances, Savings Plans, and Spot Instances</li>


### FinOps and governance
<li>FinOps dashboards for engineering, finance, and product teams to track and allocate cloud spend</li>
<li>Budgeting and alerting for cost anomalies, spikes, and budget overruns</li>
<li>Tagging and ownership visibility to attribute costs to teams, projects, and environments</li>
<li>Policy-driven governance and automation controls</li>


### Data and AI/ML workloads
<li>Databricks cost analytics with detailed visibility into cluster usage and idle time</li>
<li>S3 and object storage optimization (lifecycle, unused buckets, storage class recommendations)</li>


### Kubernetes and multi‑cloud
<li>Kubernetes cluster cost allocation per namespace, workload, and label with workload-level visibility</li>
<li>Multi-cloud support for AWS, Microsoft Azure, Google Cloud, and Alibaba Cloud from a single OptScale instance</li>

<li>Optimal utilization of Reserved Instances, Savings Plans, and Spot Instances</li>
<li>Unused resource detection</li>
<li>R&D resource power management and rightsizing</li>
<li>S3 duplicate object finder</li>
<li>Resource bottleneck identification</li>
<li>Optimal instance type and family selection</li>
<li>Databricks support</li>
<li>S3 and Redshift instrumentation</li>
<li>VM Power Schedules</li>
<br><br>Learn more about [OptScale features for FinOps and multi-cloud cost management](https://hystax.com/optscale/finops-capabilities-and-benefits/).


<br>You can check OptScale [live demo](https://my.optscale.com/live-demo) to explore product features on a pre-generated demo organization.
<br>Learn more about the Hystax OptScale platform and its capabilities at [our website](https://hystax.com).

Expand Down Expand Up @@ -83,7 +121,7 @@ NVMe SSD is recommended.

**OS Required**: [Ubuntu 24.04](https://releases.ubuntu.com/noble/).

_The current installation process should work also on Ubuntu 22.04_
_The current installation process should also work on Ubuntu 22.04_

#### Updating old installation
please follow [this document](documentation/update_to_24.04.md) to upgrade your existing installation on Ubuntu 20.04.
Expand Down
40 changes: 40 additions & 0 deletions auth/auth_server/alembic/versions/88f7bebcdcb9_add_user_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# pylint: disable=C0103
"""Add user option table

Revision ID: 88f7bebcdcb9
Revises: 998f27cb8c46
Create Date: 2026-03-10 16:27:40.340018

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '88f7bebcdcb9'
down_revision = '998f27cb8c46'
branch_labels = None
depends_on = None


def upgrade():
op.create_table(
'user_option',
sa.Column('id', sa.String(36), nullable=False),
sa.Column('created_at', sa.Integer(), nullable=False),
sa.Column('deleted_at', sa.Integer(), nullable=False),
sa.Column('user_id', sa.String(36), nullable=False),
sa.Column('name', sa.String(256), nullable=False),
sa.Column('value', sa.TEXT(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.ForeignKeyConstraint(['user_id'], ['user.id']),
sa.UniqueConstraint('user_id', 'name', 'deleted_at',
name='uc_user_id_name_deleted_at')
)
op.create_index(op.f('ix_user_option_user_name'),
'user_option', ['user_id', 'name'],
unique=False)


def downgrade():
op.drop_table('user_option')
5 changes: 4 additions & 1 deletion auth/auth_server/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ class Urls:
r"%s/users/(?P<user_id>[^/]+)/action_resources",
'bulk_action_resources': r"%s/bulk_action_resources",
'signin': r"%s/signin",
'verification_codes': r"%s/verification_codes"
'verification_codes': r"%s/verification_codes",
'user_options_collection': r"%s/users/(?P<user_id>[^/]+)/options",
'user_options':
r"%s/users/(?P<user_id>[^/]+)/options/(?P<option_name>[^/]+)"
}

def __init__(self):
Expand Down
93 changes: 93 additions & 0 deletions auth/auth_server/controllers/user_option.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import logging

from auth.auth_server.controllers.base import BaseController
from auth.auth_server.controllers.base_async import BaseAsyncControllerWrapper
from auth.auth_server.exceptions import Err
from auth.auth_server.models.models import UserOption, User
from auth.auth_server.utils import (strtobool, check_string_attribute,
check_valid_json)
from tools.optscale_exceptions.common_exc import (
WrongArgumentsException, ForbiddenException, NotFoundException)

LOG = logging.getLogger(__name__)


class UserOptionsController(BaseController):
def _get_model_type(self):
return UserOption

def check_user_access(self, user_id, token):
req_user = self.get_user_by_id(user_id)
if not req_user:
raise NotFoundException(Err.OA0003, [User.__name__, user_id])
if token:
token_user = self.get_user(token)
if token_user.id != user_id:
raise ForbiddenException(Err.OA0012, [])
return req_user

def get_by_name(self, user_id, option_name, **kwargs):
token = kwargs.get('token', None)
self.check_user_access(user_id, token)
user_options = super().list(user_id=user_id, name=option_name)
if len(user_options) > 1:
raise WrongArgumentsException(Err.OA0029, [])
elif len(user_options) == 0:
return '{}'
else:
return user_options[0].value

def list(self, user_id, **kwargs):
token = kwargs.get('token', None)
self.check_user_access(user_id, token)
with_values = kwargs.get('with_values', False)
try:
if not isinstance(with_values, bool):
with_values = strtobool(with_values)
except ValueError:
raise WrongArgumentsException(Err.OA0063, ['with_values'])
base_list = super().list(user_id=user_id)
result = [
{
'name': obj.name, 'value': obj.value
} if with_values else obj.name for obj in base_list
]
return result

def patch(self, user_id, option_name, value_data, **kwargs):
token = kwargs.get('token', None)
self.check_user_access(user_id, token)

options = super().list(user_id=user_id, name=option_name)
check_valid_json(value_data, 'value')
if len(options) > 1:
raise WrongArgumentsException(Err.OA0029, [])
elif len(options) == 0:
check_string_attribute('option_name', option_name,
max_length=256)
res = super().create(user_id=user_id, name=option_name,
deleted_at=0, value=value_data)
return res.value
else:
option = options[0]
res = super().edit(option.id, value=value_data)
return res.value

def delete(self, user_id, option_name, **kwargs):
token = kwargs.get('token', None)
self.check_user_access(user_id, token)

options = super().list(user_id=user_id, name=option_name)
if len(options) > 1:
raise WrongArgumentsException(Err.OA0029, [])
elif len(options) == 0:
raise NotFoundException(
Err.OA0003, [UserOption.__name__, option_name])
else:
option = options[0]
super().delete(option.id)


class UserOptionsAsyncController(BaseAsyncControllerWrapper):
def _get_controller_class(self):
return UserOptionsController
2 changes: 1 addition & 1 deletion auth/auth_server/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class Err(enum.Enum):
"Invalid model type: %s",
]
OA0046 = [
"Payload is not a valid json",
"%s is not a valid json",
]
OA0047 = [
"Payload is malformed",
Expand Down
1 change: 1 addition & 0 deletions auth/auth_server/handlers/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
import auth.auth_server.handlers.v2.types
import auth.auth_server.handlers.v2.signin
import auth.auth_server.handlers.v2.verification_codes
import auth.auth_server.handlers.v2.user_options
6 changes: 3 additions & 3 deletions auth/auth_server/handlers/v2/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json

from tools.optscale_exceptions.common_exc import WrongArgumentsException
from tools.optscale_exceptions.http_exc import OptHTTPError

from auth.auth_server.exceptions import Err
from auth.auth_server.handlers.v1.base import BaseHandler as BaseHandler_v1
Expand All @@ -17,13 +17,13 @@ def get_arg(self, name, type_, default=None, repeated=False):
if type_ == bool and isinstance(arg, str):
lowered = arg.lower()
if lowered not in ['true', 'false']:
raise WrongArgumentsException(Err.OA0063, [name])
raise OptHTTPError(400, Err.OA0063, [name])
return lowered == 'true'
return type_(arg)
else:
return arg
except ValueError:
raise WrongArgumentsException(Err.OA0060, [name])
raise OptHTTPError(400, Err.OA0060, [name])

def parse_url_params_into_payload(self, payload_map_params):
data = {}
Expand Down
Loading
Loading