Skip to content

Commit

Permalink
CSV export should respect --fileoutput flag
Browse files Browse the repository at this point in the history
We now treat CSV output the same as JSON and YAML.
A --fileoutput-dynamic flag was added to support the previous behavior for users who desire it
  • Loading branch information
aantn committed Jun 15, 2024
1 parent 39e6298 commit 896cc22
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 60 deletions.
24 changes: 9 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,15 @@ Read more about [how KRR works](#how-krr-works)
| Resource Recommendations 💡 | ✅ CPU/Memory requests and limits | ✅ CPU/Memory requests and limits |
| Installation Location 🌍 | ✅ Not required to be installed inside the cluster, can be used on your own device, connected to a cluster | ❌ Must be installed inside the cluster |
| Workload Configuration 🔧 | ✅ No need to configure a VPA object for each workload | ❌ Requires VPA object configuration for each workload |
| Immediate Results ⚡ | ✅ Gets results immediately (given Prometheus is running) | ❌ Requires time to gather data and provide recommendations |
| Reporting 📊 |Detailed CLI Report, web UI in [Robusta.dev](https://home.robusta.dev/) | ❌ Not supported |
| Immediate Results ⚡ | ✅ Gets results immediately (given Prometheus is running) | ❌ Requires time to gather data and provide recommendations |
| Reporting 📊 |Json, CSV, Markdown, [Web UI](#free-ui-for-krr-recommendations), and more! | ❌ Not supported |
| Extensibility 🔧 | ✅ Add your own strategies with few lines of Python | :warning: Limited extensibility |
| Explainability 📖 | ✅ See graphs explaining the recommendations | ❌ Not supported |
| Custom Metrics 📏 | 🔄 Support in future versions | ❌ Not supported |
| Custom Resources 🎛️ | 🔄 Support in future versions (e.g., GPU) | ❌ Not supported |
| Autoscaling 🔀 | 🔄 Support in future versions | ✅ Automatic application of recommendations |
| Default History 🕒 | 14 days | 8 days |
| Supports HPA 🔥 | ✅ Enable using `--allow-hpa` flag | ❌ Not supported |
| Default History 🕒 | 14 days | 8 days |
| Supports HPA 🔥 | ✅ Enable using `--allow-hpa` flag | ❌ Not supported |


<!-- GETTING STARTED -->
Expand Down Expand Up @@ -328,7 +328,7 @@ krr simple -c my-cluster-1 -c my-cluster-2
</details>

<details>
<summary>Customize output (JSON, YAML, and more</summary>
<summary>Output formats for reporting (JSON, YAML, CSV, and more)</summary>

Currently KRR ships with a few formatters to represent the scan data:

Expand All @@ -338,22 +338,16 @@ Currently KRR ships with a few formatters to represent the scan data:
- `pprint` - data representation from python's pprint library
- `csv` - export data to a csv file in the current directory

To run a strategy with a selected formatter, add a `-f` flag:
To run a strategy with a selected formatter, add a `-f` flag. Usually this should be combined with `--fileoutput <filename>` to write clean output to file without logs:

```sh
krr simple -f json
krr simple -f json --fileoutput krr-report.json
```

For JSON output, add --logtostderr so no logs go to the result file:
If you prefer, you can also use `--logtostderr` to get clean formatted output in one file and error logs in another:

```sh
krr simple --logtostderr -f json > result.json
```

For YAML output, do the same:

```sh
krr simple --logtostderr -f yaml > result.yaml
krr simple --logtostderr -f json > result.json 2> logs-and-errors.log
```
</details>

Expand Down
3 changes: 2 additions & 1 deletion robusta_krr/core/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ class Config(pd.BaseSettings):
log_to_stderr: bool
width: Optional[int] = pd.Field(None, ge=1)

# Outputs Settings
# Output Settings
file_output: Optional[str] = pd.Field(None)
file_output_dynamic = bool = pd.Field(False)
slack_output: Optional[str] = pd.Field(None)

other_args: dict[str, Any]
Expand Down
19 changes: 14 additions & 5 deletions robusta_krr/core/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import warnings
from concurrent.futures import ThreadPoolExecutor
from typing import Optional, Union
from datetime import timedelta
from datetime import timedelta, datetime
from prometrix import PrometheusNotFound
from rich.console import Console
from slack_sdk import WebClient
Expand Down Expand Up @@ -108,14 +108,23 @@ def _process_result(self, result: Result) -> None:

custom_print(formatted, rich=rich, force=True)

if settings.file_output or settings.slack_output:
if settings.file_output:
if settings.file_output_dynamic or settings.file_output or settings.slack_output:
if settings.file_output_dynamic:
current_datetime = datetime.now().strftime("%Y%m%d%H%M%S")
file_name = f"krr-{current_datetime}.{settings.format}"
logger.info(f"Writing output to file: {file_name}")
elif settings.file_output:
file_name = settings.file_output
elif settings.slack_output:
file_name = settings.slack_output

with open(file_name, "w") as target_file:
console = Console(file=target_file, width=settings.width)
console.print(formatted)
# don't use rich when writing a csv to avoid line wrapping etc
if settings.format == "csv":
target_file.write(formatted)
else:
console = Console(file=target_file, width=settings.width)
console.print(formatted)
if settings.slack_output:
client = WebClient(os.environ["SLACK_BOT_TOKEN"])
warnings.filterwarnings("ignore", category=UserWarning)
Expand Down
72 changes: 33 additions & 39 deletions robusta_krr/formatters/csv.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import itertools
import csv

import logging

import io

from robusta_krr.core.abstract import formatters
from robusta_krr.core.models.allocations import RecommendationValue, format_recommendation_value, format_diff, NONE_LITERAL, NAN_LITERAL
Expand All @@ -12,7 +11,6 @@

logger = logging.getLogger("krr")


def _format_request_str(item: ResourceScan, resource: ResourceType, selector: str) -> str:
allocated = getattr(item.object.allocations, selector)[resource]
recommended = getattr(item.recommended, selector)[resource]
Expand All @@ -38,11 +36,9 @@ def _format_total_diff(item: ResourceScan, resource: ResourceType, pods_current:

return format_diff(allocated, recommended, selector, pods_current)


@formatters.register()
def csv(result: Result) -> str:
@formatters.register("csv")
def csv_exporter(result: Result) -> str:
current_datetime = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
file_path = f"krr-{current_datetime}.csv"

# We need to order the resource columns so that they are in the format of Namespace,Name,Pods,Old Pods,Type,Container,CPU Diff,CPU Requests,CPU Limits,Memory Diff,Memory Requests,Memory Limits
resource_columns = []
Expand All @@ -51,36 +47,34 @@ def csv(result: Result) -> str:
resource_columns.append(f"{resource.name} Requests")
resource_columns.append(f"{resource.name} Limits")

with open(file_path, 'w+', newline='') as csvfile:
csv_writer = csv.writer(csvfile)
csv_writer.writerow([
"Namespace", "Name", "Pods", "Old Pods", "Type", "Container",
*resource_columns

])

for _, group in itertools.groupby(
enumerate(result.scans), key=lambda x: (x[1].object.cluster, x[1].object.namespace, x[1].object.name)
):
group_items = list(group)

for j, (i, item) in enumerate(group_items):
full_info_row = j == 0

row = [
item.object.namespace if full_info_row else "",
item.object.name if full_info_row else "",
f"{item.object.current_pods_count}" if full_info_row else "",
f"{item.object.deleted_pods_count}" if full_info_row else "",
item.object.kind if full_info_row else "",
item.object.container,
]

for resource in ResourceType:
row.append(_format_total_diff(item, resource, item.object.current_pods_count))
row += [_format_request_str(item, resource, selector) for selector in ["requests", "limits"]]

csv_writer.writerow(row)
output = io.StringIO()
csv_writer = csv.writer(output)
csv_writer.writerow([
"Namespace", "Name", "Pods", "Old Pods", "Type", "Container",
*resource_columns
])

for _, group in itertools.groupby(
enumerate(result.scans), key=lambda x: (x[1].object.cluster, x[1].object.namespace, x[1].object.name)
):
group_items = list(group)

for j, (i, item) in enumerate(group_items):
full_info_row = j == 0

row = [
item.object.namespace if full_info_row else "",
item.object.name if full_info_row else "",
f"{item.object.current_pods_count}" if full_info_row else "",
f"{item.object.deleted_pods_count}" if full_info_row else "",
item.object.kind if full_info_row else "",
item.object.container,
]

for resource in ResourceType:
row.append(_format_total_diff(item, resource, item.object.current_pods_count))
row += [_format_request_str(item, resource, selector) for selector in ["requests", "limits"]]

csv_writer.writerow(row)

logger.info("CSV File: %s", file_path)
return ""
return output.getvalue()
4 changes: 4 additions & 0 deletions robusta_krr/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ def run_strategy(
file_output: Optional[str] = typer.Option(
None, "--fileoutput", help="Filename to write output to (if not specified, file output is disabled)", rich_help_panel="Output Settings"
),
file_output_dynamic: bool = typer.Option(
False, "--fileoutput-dynamic", help="Ignore --fileoutput and write files to the current directory in the format krr-{datetime}.{format} (e.g. krr-20240518223924.csv)", rich_help_panel="Output Settings"
),
slack_output: Optional[str] = typer.Option(
None,
"--slackoutput",
Expand Down Expand Up @@ -279,6 +282,7 @@ def run_strategy(
log_to_stderr=log_to_stderr,
width=width,
file_output=file_output,
file_output_dynamic=file_output_dynamic,
slack_output=slack_output,
strategy=_strategy_name,
other_args=strategy_args,
Expand Down

0 comments on commit 896cc22

Please sign in to comment.