From 974b2ea205b15366fac15bb146551bcb54689a31 Mon Sep 17 00:00:00 2001 From: Anton Sukhov Date: Wed, 3 Jun 2026 16:58:05 +0400 Subject: [PATCH 1/4] OSN-1492. Add OptScale.ai promo banner ## Description promo banner added image --- .../TopAlertWrapper/TopAlert.styles.ts | 7 +++ .../TopAlertWrapper/TopAlertWrapper.tsx | 46 +++++++++++++++++-- .../components/TopAlertWrapper/constants.ts | 1 + ngui/ui/src/migrations.ts | 17 ++++++- ngui/ui/src/translations/en-US/app.json | 1 + ngui/ui/src/urls.ts | 3 ++ 6 files changed, 71 insertions(+), 4 deletions(-) diff --git a/ngui/ui/src/components/TopAlertWrapper/TopAlert.styles.ts b/ngui/ui/src/components/TopAlertWrapper/TopAlert.styles.ts index e76e02f4d..928a90aca 100644 --- a/ngui/ui/src/components/TopAlertWrapper/TopAlert.styles.ts +++ b/ngui/ui/src/components/TopAlertWrapper/TopAlert.styles.ts @@ -33,6 +33,13 @@ const useStyles = makeStyles()((theme) => ({ color: theme.palette.common.white, }, }, + promo: { + backgroundColor: "#2c67ce", + color: theme.palette.common.white, + ".close-alert-button": { + color: theme.palette.common.white, + }, + }, })); export default useStyles; diff --git a/ngui/ui/src/components/TopAlertWrapper/TopAlertWrapper.tsx b/ngui/ui/src/components/TopAlertWrapper/TopAlertWrapper.tsx index 2cb67eb76..e1aaeee16 100644 --- a/ngui/ui/src/components/TopAlertWrapper/TopAlertWrapper.tsx +++ b/ngui/ui/src/components/TopAlertWrapper/TopAlertWrapper.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo } from "react"; -import { Box } from "@mui/material"; +import { Box, Link } from "@mui/material"; import { render as renderGithubButton } from "github-buttons"; import { FormattedMessage, useIntl } from "react-intl"; import { useDispatch } from "react-redux"; @@ -7,7 +7,7 @@ import { useAllDataSources } from "hooks/coreData/useAllDataSources"; import { useGetToken } from "hooks/useGetToken"; import { useOrganizationInfo } from "hooks/useOrganizationInfo"; import { useRootData } from "hooks/useRootData"; -import { GITHUB_HYSTAX_OPTSCALE_REPO } from "urls"; +import { GITHUB_HYSTAX_OPTSCALE_REPO, OPTSCALE_AI } from "urls"; import { AZURE_TENANT, ENVIRONMENT } from "utils/constants"; import { SPACING_1 } from "utils/layouts"; import { updateOrganizationTopAlert as updateOrganizationTopAlertActionCreator } from "./actionCreators"; @@ -132,12 +132,14 @@ const TopAlertWrapper = ({ blacklistIds = [] }: TopAlertWrapperProps) => { }, { id: ALERT_TYPES.OPEN_SOURCE_ANNOUNCEMENT, + // Temporarily disabled — replaced by OPTSCALE_AI_PROMO_ANNOUNCEMENT below. + // To restore, revert condition back to: !isExistingUser && (!userId || organizationId) // isExistingUser — true only if user was logged in/visited optscale before. Set in migrations. // organizationId — wont be presented on initial load (so storedAlerts will be empty, so even if banner was closed, we would not know that, // so we need to wait for organizationId. But if user is not logged in — there also wont be organizationId, so we use next flag) // userId — presented after login // this check means "condition: not logged in new user (!isExistingUser && !userId) OR new user and we know organization id (!isExistingUser && organizationId)" - condition: !isExistingUser && (!userId || organizationId), + condition: false, getContent: () => ( { }, dataTestId: "top_alert_open_source_announcement", }, + { + id: ALERT_TYPES.OPTSCALE_AI_PROMO_ANNOUNCEMENT, + // isExistingUser — true only if user was logged in/visited optscale before. Set in migrations. + // organizationId — wont be presented on initial load (so storedAlerts will be empty, so even if banner was closed, we would not know that, + // so we need to wait for organizationId. But if user is not logged in — there also wont be organizationId, so we use next flag) + // userId — presented after login + // this check means "condition: not logged in new user (!isExistingUser && !userId) OR new user and we know organization id (!isExistingUser && organizationId)" + condition: !isExistingUser && (!userId || organizationId), + getContent: () => ( + + {chunks}, + br: () =>
, + link: (chunks) => ( + + {chunks} + + ), + }} + /> +
+ ), + type: "promo", + triggered: isTriggered(ALERT_TYPES.OPTSCALE_AI_PROMO_ANNOUNCEMENT), + onClose: () => { + updateOrganizationTopAlert({ id: ALERT_TYPES.OPTSCALE_AI_PROMO_ANNOUNCEMENT, closed: true }); + }, + dataTestId: "top_alert_optscale_ai_promo_announcement", + }, ]; }, [ storedAlerts, diff --git a/ngui/ui/src/components/TopAlertWrapper/constants.ts b/ngui/ui/src/components/TopAlertWrapper/constants.ts index 8c1deed34..d0fd228be 100644 --- a/ngui/ui/src/components/TopAlertWrapper/constants.ts +++ b/ngui/ui/src/components/TopAlertWrapper/constants.ts @@ -3,6 +3,7 @@ export const ALERT_TYPES = Object.freeze({ DATA_SOURCES_PROCEEDED: 3, OPEN_SOURCE_ANNOUNCEMENT: 4, INACTIVE_ORGANIZATION: 5, + OPTSCALE_AI_PROMO_ANNOUNCEMENT: 7, }); export const IS_EXISTING_USER = "isExistingUser"; diff --git a/ngui/ui/src/migrations.ts b/ngui/ui/src/migrations.ts index d2a5190b7..4ec4b64f6 100644 --- a/ngui/ui/src/migrations.ts +++ b/ngui/ui/src/migrations.ts @@ -4,7 +4,7 @@ import { RANGE_DATES } from "containers/RangePickerFormContainer/reducer"; import { millisecondsToSeconds } from "utils/datetime"; import { objectMap } from "utils/objects"; -export const CURRENT_VERSION = 15; +export const CURRENT_VERSION = 16; // When we modify storage structure, we will need to properly use migrations: // https://github.com/rt2zz/redux-persist/blob/master/docs/migrations.md @@ -133,6 +133,21 @@ const migrations = { ]) ); + return { + ...state, + alerts: newAlerts, + }; + }, + 16: (state) => { + // OPEN_SOURCE_ANNOUNCEMENT (github stars) is temporarily replaced by the OptScale AI promo banner. + const newAlerts = Object.fromEntries( + Object.entries(state.alerts).map(([orgId, payload]) => [ + orgId, + // OPEN_SOURCE_ANNOUNCEMENT = 4 + payload.filter(({ id }) => id !== 4), + ]) + ); + return { ...state, alerts: newAlerts, diff --git a/ngui/ui/src/translations/en-US/app.json b/ngui/ui/src/translations/en-US/app.json index b7890b55b..df9271395 100644 --- a/ngui/ui/src/translations/en-US/app.json +++ b/ngui/ui/src/translations/en-US/app.json @@ -1619,6 +1619,7 @@ "openPorts": "Open ports", "openShareSettings": "Open expenses export settings", "openSourceAnnouncement": "Please consider giving OptScale a Star on GitHub, it is 100% open-source. It would increase its visibility to others and expedite product development. Thank you!", + "optScaleAiPromoAnnouncement": "⭐️ NEW ⭐️ Cloud costs under control? Time to go beyond – into AI.

OptScale AI covers it all – cost control, security and guardrails, and full visibility over every AI prompt, model, and agent. Try free at optscale.ai", "optScalePrivacyPolicy": "OptScale Privacy Policy", "optScaleSlackIntegrationTitle": "OptScale Slack Integration", "optimal": "Optimal", diff --git a/ngui/ui/src/urls.ts b/ngui/ui/src/urls.ts index da3db28e6..d1efb34ed 100644 --- a/ngui/ui/src/urls.ts +++ b/ngui/ui/src/urls.ts @@ -604,6 +604,9 @@ export const GITHUB_HYSTAX_EXTRACT_LINKED_REPORTS = "https://github.com/hystax/o export const GITHUB_HYSTAX_OPTSCALE_REPO = "https://github.com/hystax/optscale"; export const PYPI_OPTSCALE_ARCEE = "https://pypi.org/project/optscale-arcee"; +// OptScale AI promo +export const OPTSCALE_AI = "https://optscale.ai/"; + // Nebius documentation export const NEBIUS_CREATE_SERVICE_ACCOUNT = "https://nebius.com/il/docs/iam/quickstart-sa#create-sa"; export const NEBIUS_CREATING_AUTHORIZED_KEYS = "https://nebius.com/il/docs/iam/operations/authorized-key/create"; From 368aeeb4982d49d34bd7679fea8d6f5f22960ee0 Mon Sep 17 00:00:00 2001 From: mirlena777 <142885068+mirlena777@users.noreply.github.com> Date: Mon, 15 Jun 2026 12:42:10 +0100 Subject: [PATCH 2/4] Add files via upload --- .../OptScale-AI-description-ReadMe-GitHub.png | Bin 0 -> 33524 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 documentation/images/OptScale-AI-description-ReadMe-GitHub.png diff --git a/documentation/images/OptScale-AI-description-ReadMe-GitHub.png b/documentation/images/OptScale-AI-description-ReadMe-GitHub.png new file mode 100644 index 0000000000000000000000000000000000000000..867ce40f0fdf58d354f006ba01ad48a15503790b GIT binary patch literal 33524 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 literal 0 HcmV?d00001 From ede93a6cfc1a25cf1e78e450aad595895a8c9b53 Mon Sep 17 00:00:00 2001 From: mirlena777 <142885068+mirlena777@users.noreply.github.com> Date: Mon, 15 Jun 2026 12:47:16 +0100 Subject: [PATCH 3/4] Update README.md --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 26519a296..448d2c1b3 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,21 @@ It provides deep visibility into infrastructure costs, automated optimization re +
+
+ +
+
+ + + 🔖 NEW 🔖
Cloud costs under control? Time to go beyond – into AI. +
+
OptScale AI covers it all – cost control, security and guardrails, and full visibility over every AI prompt, model, and agent. + + Try free at [optscale.ai](https://optscale.ai) +
+ +

From 8b5b897d251e3b0105f1d2c15ececb5ab892c66c Mon Sep 17 00:00:00 2001 From: sd-hystax <110374605+sd-hystax@users.noreply.github.com> Date: Tue, 23 Jun 2026 08:38:25 +0300 Subject: [PATCH 4/4] OSN-1461. Reduced Insider worker memory usage ## Description Reduced Insider worker memory usage ## Related issue number OSN-1461 ## Special notes ## Checklist * [ ] The pull request title is a good summary of the changes * [ ] Unit tests for the changes exist * [ ] New and existing unit tests pass locally --- insider/insider_worker/http_client/client.py | 7 +- insider/insider_worker/main.py | 20 +++- ...20260618150755_last_seen_currency_index.py | 53 +++++++++ insider/insider_worker/processors/azure.py | 112 +++++++++--------- insider/insider_worker/processors/base.py | 5 +- 5 files changed, 131 insertions(+), 66 deletions(-) create mode 100644 insider/insider_worker/migrations/20260618150755_last_seen_currency_index.py diff --git a/insider/insider_worker/http_client/client.py b/insider/insider_worker/http_client/client.py index a6d64842b..27576df10 100644 --- a/insider/insider_worker/http_client/client.py +++ b/insider/insider_worker/http_client/client.py @@ -35,12 +35,13 @@ def request(self, url, method): response_body = None # pylint: disable=E1101 if response.status_code != requests.codes.no_content: - if 'application/json' in response.headers['Content-Type']: + content_type = response.headers.get('Content-Type', '') + if 'application/json' in content_type: response_body = json.loads( response.content.decode('utf-8')) - if 'text/plain' in response.headers['Content-Type']: + elif 'text/plain' in content_type: response_body = response.content.decode() - if 'application/octet-stream' in response.headers['Content-Type']: + else: response_body = response.content return response.status_code, response_body diff --git a/insider/insider_worker/main.py b/insider/insider_worker/main.py index 260f8c4ce..e144fe683 100644 --- a/insider/insider_worker/main.py +++ b/insider/insider_worker/main.py @@ -21,6 +21,7 @@ LOG = get_logger(__name__) TASK_EXCHANGE = Exchange(EXCHANGE_NAME, type='direct') TASK_QUEUE = Queue(QUEUE_NAME, TASK_EXCHANGE, routing_key=QUEUE_NAME) +DISCOVERIES_THRESHOLD = 43200 # 12 hours in seconds class InsiderWorker(ConsumerMixin): @@ -42,13 +43,28 @@ def discoveries(self): def get_consumers(self, consumer, channel): return [consumer(queues=[TASK_QUEUE], accept=['json'], - callbacks=[self.process_task], prefetch_count=10)] + callbacks=[self.process_task], prefetch_count=1)] + + def get_last_discovery_ts(self, cloud_type): + discoveries = self.discoveries.find( + {'cloud_type': cloud_type, 'completed_at': {'$ne': 0}} + ).sort( + [('completed_at', -1)]).limit(1) + try: + discovery = next(discoveries) + return discovery.get('started_at', 0) + except StopIteration: + return 0 def _process_task(self, task): start_process_time = int(datetime.now(tz=timezone.utc).timestamp()) cloud_type = task.get('cloud_type') if not cloud_type: raise Exception('Invalid task received: {}'.format(task)) + last_discovery_ts = self.get_last_discovery_ts(cloud_type) + if last_discovery_ts + DISCOVERIES_THRESHOLD >= start_process_time: + LOG.info('Skipping task for %s by threshold', cloud_type) + return discovery_id = self.discoveries.insert_one({ 'cloud_type': cloud_type, 'started_at': start_process_time, @@ -56,7 +72,7 @@ def _process_task(self, task): }).inserted_id get_processor_class(cloud_type)( - self.mongo_client, self.config_cl).process_prices() + self.mongo_client, self.config_cl).process_prices(last_discovery_ts) end_process_time = int(datetime.now(tz=timezone.utc).timestamp()) self.discoveries.update_one( diff --git a/insider/insider_worker/migrations/20260618150755_last_seen_currency_index.py b/insider/insider_worker/migrations/20260618150755_last_seen_currency_index.py new file mode 100644 index 000000000..58786e92d --- /dev/null +++ b/insider/insider_worker/migrations/20260618150755_last_seen_currency_index.py @@ -0,0 +1,53 @@ +import logging +from insider.insider_worker.migrations.base import BaseMigration + +NEW_INDEXES = { + 'CurrencyLastSeen': ['currencyCode', 'last_seen'] +} +OLD_INDEXES = { + 'LastSeen': ['last_seen'] +} +LOG = logging.getLogger(__name__) + + +class Migration(BaseMigration): + def get_indexes(self): + return [x['name'] for x in self.azure_prices.list_indexes()] + + def upgrade(self): + existing_indexes = self.get_indexes() + for index_name, index_fields in NEW_INDEXES.items(): + if index_name in existing_indexes: + LOG.info(f'Index {index_name} already exists') + continue + LOG.info(f'Creating index {index_name}') + self.azure_prices.create_index( + [(f, 1) for f in index_fields], + name=index_name, + background=True + ) + for index_name, index_fields in OLD_INDEXES.items(): + if index_name in existing_indexes: + LOG.info(f'Dropping index {index_name}') + self.azure_prices.drop_index(index_name) + else: + LOG.info(f'Index {index_name} doesn\'t exist') + + def downgrade(self): + existing_indexes = self.get_indexes() + for index_name, index_fields in OLD_INDEXES.items(): + if index_name in existing_indexes: + LOG.info(f'Index {index_name} already exists') + continue + LOG.info(f'Creating index {index_name}') + self.azure_prices.create_index( + [(f, 1) for f in index_fields], + name=index_name, + background=True + ) + for index_name, index_fields in NEW_INDEXES.items(): + if index_name in existing_indexes: + LOG.info(f'Dropping index {index_name}') + self.azure_prices.drop_index(index_name) + else: + LOG.info(f'Index {index_name} doesn\'t exist') diff --git a/insider/insider_worker/processors/azure.py b/insider/insider_worker/processors/azure.py index 5b02dc75f..cf0f5a54d 100644 --- a/insider/insider_worker/processors/azure.py +++ b/insider/insider_worker/processors/azure.py @@ -15,8 +15,8 @@ ACTIVITIES_EXCHANGE_NAME = 'activities-tasks' ACTIVITIES_EXCHANGE = Exchange(ACTIVITIES_EXCHANGE_NAME, type='topic') LOG = get_logger(__name__) -PRICES_PER_REQUEST = 100 PRICES_COUNT_TO_LOG = 1000 +CHINA_CURRENCY_CODE = 'CNY' class AzurePriceProcessor(BasePriceProcessor): @@ -35,16 +35,6 @@ def discoveries(self): def prices(self): return self.mongo_client.insider.azure_prices - def get_last_discovery(self): - discoveries = self.discoveries.find( - {'cloud_type': self.CLOUD_TYPE, 'completed_at': {'$ne': 0}} - ).sort( - [('completed_at', -1)]).limit(1) - try: - return next(discoveries) - except StopIteration: - return {} - @staticmethod def unique_values(price): return tuple(price.get(p) for p in AzurePriceProcessor.UNIQUE_FIELDS) @@ -82,42 +72,39 @@ def _get_currencies_list(self): currencies = set(map(lambda x: x['currency'], orgs['organizations'])) return list(currencies) - def _process_global_prices(self, http_client, old_prices_map): - LOG.info('Start processing Azure Global prices') - for currency in self._get_currencies_list(): - LOG.info('Processing Azure prices for currency: %s', currency) - processed_keys = {} - prices_counter = 0 - - next_page = 'https://prices.azure.com/api/retail/prices' - next_page += '?currencyCode=%s' % currency - while True: - if prices_counter % PRICES_COUNT_TO_LOG == 0: - LOG.info('Total number of prices got from ' - 'cloud: %s', prices_counter) - try: - code, response = http_client.get(next_page) - except SSLError: - LOG.error('Getting Azure prices failed with SSL ' - 'verification error. Will try to get prices' - 'without SSL verification') - self.send_sslerror_service_email() - http_client = Client(verify=False) - code, response = http_client.get(next_page) - items = response.get('Items', []) - new_prices_map = {self.unique_values(p): p for p in items} - self.update_price_records(new_prices_map, old_prices_map, - processed_keys) - new_url = response.get('NextPageLink') - if not new_url or new_url == next_page: - LOG.info('Total number of prices got from ' - 'cloud: %s', prices_counter) - break - next_page = new_url - prices_counter += response.get('Count', 0) - - def _process_china_prices(self, http_client, old_prices_map): - LOG.info('Start processing Azure China prices') + def _process_global_prices(self, http_client, old_prices_map, currency): + LOG.info('Processing Azure prices for currency: %s', currency) + processed_keys = {} + prices_counter = 0 + next_page = 'https://prices.azure.com/api/retail/prices' + next_page += '?currencyCode=%s' % currency + while True: + if prices_counter % PRICES_COUNT_TO_LOG == 0: + LOG.info('Total number of prices got from ' + 'cloud: %s', prices_counter) + try: + code, response = http_client.get(next_page) + except SSLError: + LOG.error('Getting Azure prices failed with SSL ' + 'verification error. Will try to get prices' + 'without SSL verification') + self.send_sslerror_service_email() + http_client = Client(verify=False) + code, response = http_client.get(next_page) + items = response.get('Items', []) + new_prices_map = {self.unique_values(p): p for p in items} + self.update_price_records(new_prices_map, old_prices_map, + processed_keys) + new_url = response.get('NextPageLink') + if not new_url or new_url == next_page: + LOG.info('Total number of prices got from ' + 'cloud: %s', prices_counter) + break + next_page = new_url + prices_counter += response.get('Count', 0) + + def _process_china_prices(self, http_client, old_prices_map, currency): + LOG.info('Start processing Azure China prices (%s)', currency) url = 'https://prices.azure.cn/api/retail/pricesheet/download?' \ 'api-version=2023-06-01-preview' _, response = http_client.get(url) @@ -130,17 +117,28 @@ def _process_china_prices(self, http_client, old_prices_map): LOG.info('Total number of prices got from cloud: %s', len(new_prices_map)) - def process_prices(self): - last_discovery = self.get_last_discovery() - old_prices = self.prices.find( - {'last_seen': {'$gte': last_discovery.get('started_at', 0)}}, - {k: 1 for k in self.UNIQUE_FIELDS + self.CHANGE_FIELDS + ['last_seen']} - ) - old_prices_map = {self.unique_values(p): p for p in old_prices} - + def process_prices(self, last_discovery_ts): http_client = Client() - self._process_global_prices(http_client, old_prices_map) - self._process_china_prices(http_client, old_prices_map) + process_func_map = { + CHINA_CURRENCY_CODE: self._process_china_prices + } + for currency in self._get_currencies_list(): + old_prices = self.prices.find( + { + 'last_seen': { + '$gte': last_discovery_ts + }, + 'currencyCode': currency + }, + { + k: 1 for k in + self.UNIQUE_FIELDS + self.CHANGE_FIELDS + ['last_seen'] + } + ) + old_prices_map = {self.unique_values(p): p for p in old_prices} + process_func = process_func_map.get( + currency, self._process_global_prices) + process_func(http_client, old_prices_map, currency) def update_price_records(self, new_prices_map, old_prices_map, processed_keys): diff --git a/insider/insider_worker/processors/base.py b/insider/insider_worker/processors/base.py index 468258cad..d962faaca 100644 --- a/insider/insider_worker/processors/base.py +++ b/insider/insider_worker/processors/base.py @@ -11,8 +11,5 @@ def discoveries(self): def prices(self): raise NotImplementedError() - def get_last_discovery(self): - raise NotImplementedError() - - def process_prices(self): + def process_prices(self, last_discovery_ts): raise NotImplementedError()