Skip to content

feat/aws/inactive cloudwatch log group#19

Open
gabrielecalafange wants to merge 23 commits intointegrationfrom
feat/aws/inactive_cloudwatch_log_group
Open

feat/aws/inactive cloudwatch log group#19
gabrielecalafange wants to merge 23 commits intointegrationfrom
feat/aws/inactive_cloudwatch_log_group

Conversation

@gabrielecalafange
Copy link

No description provided.

@anavirginianery
Copy link

Pull Request Description

Hello team, I hope this message finds you well.

I’m a Computer Science student at the Federal University of Campina Grande (Brazil), and part of a research team working on FinOps initiatives. At our Distributed Systems Laboratory (LSD), we’ve been contributing to the evolution of OptScale.

This pull request introduces a new AWS cost-optimization recommendation for identifying Inactive CloudWatch Log Groups.

CloudWatch Logs can silently accumulate storage costs over time. When log groups have no retention policy configured and exhibit no recent activity, they effectively become unused storage. This recommendation helps maintain observability hygiene and reduce unnecessary CloudWatch expenses by detecting log groups that are safe cleanup candidates.

Recommendation Behavior

A CloudWatch Log Group is considered inactive when all of the following conditions are met:

  • No retention policy is configured (infinite retention, indicating lack of lifecycle management)

  • No recent ingestion activity within the evaluation window (7 days)

  • No recent query activity in CloudWatch Logs Insights within the same period

Implementation details

  • Added a new recommendation rule under the AWS Log Group scope

  • Enhanced the discovery process to collect additional metadata required for evaluation

  • Implemented detection logic based on retention settings and recent ingestion and query activity

  • Added static cost-saving estimation based on a single AWS region's pricing

  • Added metadata consistent with existing OptScale AWS recommendations

UI Update

Adjusted cost-savings formatting: values below USD $1 are now displayed as "<$1" instead of $0, to avoid misleading perception of zero impact and improve clarity in cost-impact reporting.

Known Limitations

Pricing values are hard-coded and limited to one AWS region

Next Steps

  • Integrate dynamic AWS pricing using API calls or external configuration

  • Enhance the heuristic with configurable evaluation periods and activity thresholds

  • Extend pricing support for multiple AWS regions

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds support for CloudWatch Log Groups as a new discoverable resource type in AWS, including metrics collection, cost estimation, and recommendations for identifying inactive log groups.

  • Added LogGroupResource model with CloudWatch metrics support (ingestion, storage, query)
  • Implemented AWS discovery methods for log groups with parallel metrics fetching
  • Created recommendation module to identify inactive log groups and estimate potential savings
  • Added UI components and translations for the new recommendation type

Reviewed Changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
tools/cloud_adapter/model.py Added LogGroupResource class and log_group enum value; changed string quotes to double quotes
tools/cloud_adapter/clouds/aws.py Implemented log group discovery, metrics collection, and helper methods for AWS CloudWatch
rest_api/rest_api_server/tests/unittests/test_discovery_infos_api.py Updated test to expect 7 resource types instead of 6
rest_api/rest_api_server/controllers/base.py Added logic to populate resource name from meta.name if missing
rest_api/rest_api_server/alembic/versions/*.py Added log_group to database enum types for discovery_info table
ngui/ui/src/translations/en-US/app.json Added translation keys for inactive CloudWatch log groups UI
ngui/ui/src/hooks/useOptscaleRecommendations.ts Registered new recommendation component
ngui/ui/src/containers/RecommendationsOverviewContainer/recommendations/*.tsx Added InactiveCloudWatchLogGroup recommendation component
ngui/ui/src/components/FormattedMoney/useMoneyFormatter.ts Refactored money formatter with TypeScript types; changed < $0.01 to < $1 threshold
docker_images/resource_discovery/worker.py Enhanced error handling for discovery calls with exception logging
diworker/diworker/migrations/20250417123000_cloudwatch_metrics.py Created ClickHouse table for CloudWatch metrics storage
diworker/diworker/importers/base.py Added methods to save CloudWatch metrics to ClickHouse
diworker/diworker/importers/aws.py Implemented log group metrics processing and persistence
bumiworker/bumiworker/modules/recommendations/inactive_cloud_watch_log_group.py Core recommendation logic for detecting inactive log groups and calculating savings
bumiworker/bumiworker/modules/pricing/aws_cloudwatch.py AWS CloudWatch Logs pricing constants
bumiworker/bumiworker/modules/module.py Minor string formatting updates
bumiworker/bumiworker/modules/base.py Renamed time_measure to TimeMeasure (PEP8 compliance)
bumiworker/bumiworker/modules/archive/inactive_cloud_watch_log_group.py Archive logic for inactive log group recommendations
Comments suppressed due to low confidence (2)

bumiworker/bumiworker/modules/base.py:109

    def _get(self):

bumiworker/bumiworker/modules/base.py:149

    def _get(self):

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

return ca.get("id") or ca.get("_id")
return ""

def _get(self):
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overridden method signature does not match call, where it is passed too many arguments. Overriding method method InactiveCloudWatchLogGroup._get matches the call.

Suggested change
def _get(self):
def _get(self, *args, **kwargs):

Copilot uses AI. Check for mistakes.
return pool_id
return optimization.get("pool_id")

def _get(self, previous_options, optimizations, cloud_accounts_map, **kwargs):
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method requires 4 positional arguments, whereas overridden ServiceBase._get requires 1. This call correctly calls the base method, but does not match the signature of the overriding method.
This method requires 4 positional arguments, whereas overridden ModuleBase._get requires 1. This call correctly calls the base method, but does not match the signature of the overriding method.
This method requires 4 positional arguments, whereas overridden InactiveCloudWatchLogGroup._get requires 1. This call correctly calls the base method, but does not match the signature of the overriding method.
This method requires 4 positional arguments, whereas overridden ArchiveBase._get may be called with 1. This call correctly calls the base method, but does not match the signature of the overriding method.
This method requires 4 positional arguments, whereas overridden ArchiveBase._get may be called with arbitrarily many. This call correctly calls the base method, but does not match the signature of the overriding method.

Suggested change
def _get(self, previous_options, optimizations, cloud_accounts_map, **kwargs):
def _get(self, previous_options, **kwargs):
optimizations = kwargs.get("optimizations")
cloud_accounts_map = kwargs.get("cloud_accounts_map")
if optimizations is None or cloud_accounts_map is None:
raise ValueError("Missing required keyword arguments: 'optimizations' and/or 'cloud_accounts_map'")

Copilot uses AI. Check for mistakes.
@anavirginianery anavirginianery self-assigned this Nov 10, 2025
@anavirginianery anavirginianery force-pushed the feat/aws/inactive_cloudwatch_log_group branch from 1ca70dc to cadde8d Compare November 12, 2025 21:52
nexusriot and others added 23 commits December 4, 2025 19:58
## Description

Change available filters API behavior

## Related issue number

OSN-1135

## Special notes

<!-- Please provide additional information if required. -->

## 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
- Fix incorrect redirect filters in table data
- Now we get available meta keys based on applied filters using the available_filters api
- Added toggle to apply selected categorization as an additional meta filter
- Show null meta as is, without converting to "(not set)" label
- removed multiplying on node cpu and ram gb numbers (as hourly_cost is a cost for ALL cpu and ram)
- added currency support
- changed default cost_model costs
fix: linter errors

fix: linter errors

fix: linter errors

fix: linter errors
fix: bumiworker tests

fix: bumiworker tests

ngui lint

fix: imports lint

fix: lint errors bumiworker and ngui

fix: ordering app.json

revert file

revert files before lint
@anavirginianery anavirginianery force-pushed the feat/aws/inactive_cloudwatch_log_group branch from 19c1d72 to 13cf5e2 Compare December 4, 2025 23:02
"""
cutoff_recent_window = self._utc_now() - timedelta(days=days_threshold)
total = 0
for m in series or []:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change these letter variables for complete and descriptive words

"""
cutoff_recent_window = self._utc_now() - timedelta(days=RECENT_WINDOW_DAYS)
total = 0
for m in series or []:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change these letter variables for complete and descriptive words

Copy link
Collaborator

@marianezei marianezei left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would carefully review Copilot's recommendations. Perhaps it would be good to accept them.

nice work everyone!!

Comment on lines +303 to +322
item = {
'cloud_resource_id': r.get('resource_id'),
'resource_id': r.get('resource_id'),
'resource_name': r.get('name'),
'log_group_name': r.get('log_group_name'),
'cloud_account_id': r.get('cloud_account_id'),
'cloud_type': ca_info.get('type'),
'cloud_account_name': ca_info.get('name'),
'region': r.get('region'),
'owner': {"id": None, "name": None},
'pool': {"id": r.get("pool_id"), "name": None, "purpose": None},
'is_excluded': is_excluded,
'retention_in_days': r.get('retention_in_days'),
'stored_bytes': int(r.get('stored_bytes', 0) or 0),
'detected_at': self.created_at,
'storage': int(r.get('stored_bytes', 0) or 0),
'ingestion': int(ingestion_occurrences),
'query': int(query_occurrences),
'saving': saving,
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can import this from a file where contains these kind of info. in fact...other recommendations may/might need this.
what do you think?

filter_with_tags = response.get('filter_values', {}).get('tag')
filter_without_tags = response.get('filter_values', {}).get('without_tag')
self.assertListEqual(filter_with_tags, [])
self.assertListEqual(filter_with_tags, ['hello'])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unit-test?

cloud_accounts_map)
self._archive_data(res, previous['created_at'])
except Exception as ex:
except Exception as ex: # pylint: disable=broad-exception-caught
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment needs to stay in the file?

continue

response = self._aggregate_resources(ca_id)
for r in response:
Copy link
Collaborator

@jhenriwue jhenriwue Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one letter doesn't explain well what is this variable, I think we can change to something better, right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One case that one letter can be understandable is in a list comprehension

x.get('cloud_account_id') is not None and
# RIFee is created once a month and is updated every day
# RIFee is created once a month and is updated every
# day
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why another line to write one single word? lint problems?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.