项目4:Namespace 资源报表生成器
项目4:Namespace 资源报表生成器
项目背景
在多租户 Kubernetes 集群中,需要了解每个 Namespace 的资源使用情况,用于:
- 💰 成本核算和分摊
- 📊 资源配额调整
- 🔍 资源使用分析
- 📈 趋势预测
解决方案:
定期生成各 Namespace 的资源使用报表,支持多种输出格式(HTML、PDF、Excel、JSON)。
功能需求
核心功能
- ✅ 统计 CPU、内存、存储使用量
- ✅ Pod 数量统计
- ✅ Service、ConfigMap、Secret 统计
- ✅ 资源配额使用情况
- ✅ 多种输出格式(HTML、JSON、CSV、Excel)
- ✅ 定时生成报表
高级功能
- ✅ 同比和环比分析
- ✅ 成本估算
- ✅ 异常检测
- ✅ 报表邮件推送
- ✅ Dashboard 可视化
Go 完整实现
main.go
package main
import (
"context"
"flag"
"fmt"
"os"
"time"
"github.com/sirupsen/logrus"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
metricsv1beta1 "k8s.io/metrics/pkg/client/clientset/versioned"
"namespace-reporter/pkg/config"
"namespace-reporter/pkg/reporter"
)
var log = logrus.New()
func main() {
kubeconfig := flag.String("kubeconfig", "", "path to kubeconfig")
configFile := flag.String("config", "", "path to config file")
outputFormat := flag.String("format", "html", "output format: html, json, csv, excel")
outputPath := flag.String("output", "report.html", "output file path")
flag.Parse()
// 加载配置
cfg, err := config.LoadConfig(*configFile)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// 设置日志
level, _ := logrus.ParseLevel(cfg.LogLevel)
log.SetLevel(level)
log.SetFormatter(&logrus.JSONFormatter{})
// 创建 Kubernetes 客户端
var k8sConfig *rest.Config
if *kubeconfig != "" {
k8sConfig, err = clientcmd.BuildConfigFromFlags("", *kubeconfig)
} else {
k8sConfig, err = rest.InClusterConfig()
}
if err != nil {
log.Fatalf("Failed to create k8s config: %v", err)
}
clientset, err := kubernetes.NewForConfig(k8sConfig)
if err != nil {
log.Fatalf("Failed to create clientset: %v", err)
}
metricsClient, err := metricsv1beta1.NewForConfig(k8sConfig)
if err != nil {
log.Fatalf("Failed to create metrics client: %v", err)
}
// 创建报表生成器
nsReporter := reporter.NewNamespaceReporter(clientset, metricsClient, cfg, log)
// 生成报表
report, err := nsReporter.GenerateReport()
if err != nil {
log.Fatalf("Failed to generate report: %v", err)
}
// 输出报表
if err := nsReporter.ExportReport(report, *outputFormat, *outputPath); err != nil {
log.Fatalf("Failed to export report: %v", err)
}
log.Infof("Report generated successfully: %s", *outputPath)
// 如果配置了邮件,发送报表
if cfg.Email.Enabled {
if err := nsReporter.SendEmailReport(report, *outputPath); err != nil {
log.Errorf("Failed to send email: %v", err)
} else {
log.Info("Report sent via email")
}
}
}
pkg/reporter/reporter.go
package reporter
import (
"context"
"fmt"
"sort"
"time"
"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
metricsv1beta1 "k8s.io/metrics/pkg/client/clientset/versioned"
"namespace-reporter/pkg/config"
)
type NamespaceReporter struct {
clientset *kubernetes.Clientset
metricsClient *metricsv1beta1.Clientset
config *config.Config
logger *logrus.Logger
}
type Report struct {
GeneratedAt time.Time
ClusterName string
TotalNamespaces int
NamespaceReports []NamespaceReport
Summary Summary
}
type NamespaceReport struct {
Name string
CreationTimestamp time.Time
// 资源使用
CPURequest int64 // millicores
CPULimit int64
CPUUsage int64
MemoryRequest int64 // bytes
MemoryLimit int64
MemoryUsage int64
StorageUsage int64
// 资源配额
ResourceQuota *ResourceQuotaInfo
// 对象统计
PodCount int
RunningPods int
ServiceCount int
ConfigMapCount int
SecretCount int
PVCCount int
// 成本估算
EstimatedCost float64
// 标签
Labels map[string]string
}
type ResourceQuotaInfo struct {
Name string
CPUHard int64
CPUUsed int64
MemoryHard int64
MemoryUsed int64
PodsHard int
PodsUsed int
}
type Summary struct {
TotalCPURequest int64
TotalCPULimit int64
TotalCPUUsage int64
TotalMemoryRequest int64
TotalMemoryLimit int64
TotalMemoryUsage int64
TotalStorageUsage int64
TotalPods int
TotalEstimatedCost float64
}
func NewNamespaceReporter(
clientset *kubernetes.Clientset,
metricsClient *metricsv1beta1.Clientset,
cfg *config.Config,
logger *logrus.Logger,
) *NamespaceReporter {
return &NamespaceReporter{
clientset: clientset,
metricsClient: metricsClient,
config: cfg,
logger: logger,
}
}
func (nr *NamespaceReporter) GenerateReport() (*Report, error) {
nr.logger.Info("Starting report generation")
// 获取所有命名空间
namespaces, err := nr.clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
if err != nil {
return nil, fmt.Errorf("failed to list namespaces: %v", err)
}
report := &Report{
GeneratedAt: time.Now(),
ClusterName: nr.config.ClusterName,
TotalNamespaces: len(namespaces.Items),
}
// 生成每个命名空间的报表
for _, ns := range namespaces.Items {
// 跳过系统命名空间
if nr.shouldSkipNamespace(ns.Name) {
continue
}
nsReport, err := nr.generateNamespaceReport(&ns)
if err != nil {
nr.logger.WithError(err).Warnf("Failed to generate report for namespace %s", ns.Name)
continue
}
report.NamespaceReports = append(report.NamespaceReports, *nsReport)
// 更新汇总
report.Summary.TotalCPURequest += nsReport.CPURequest
report.Summary.TotalCPULimit += nsReport.CPULimit
report.Summary.TotalCPUUsage += nsReport.CPUUsage
report.Summary.TotalMemoryRequest += nsReport.MemoryRequest
report.Summary.TotalMemoryLimit += nsReport.MemoryLimit
report.Summary.TotalMemoryUsage += nsReport.MemoryUsage
report.Summary.TotalStorageUsage += nsReport.StorageUsage
report.Summary.TotalPods += nsReport.PodCount
report.Summary.TotalEstimatedCost += nsReport.EstimatedCost
}
// 按 CPU 使用量排序
sort.Slice(report.NamespaceReports, func(i, j int) bool {
return report.NamespaceReports[i].CPUUsage > report.NamespaceReports[j].CPUUsage
})
nr.logger.Infof("Report generated for %d namespaces", len(report.NamespaceReports))
return report, nil
}
func (nr *NamespaceReporter) shouldSkipNamespace(namespace string) bool {
skipNamespaces := []string{"kube-system", "kube-public", "kube-node-lease"}
for _, skip := range skipNamespaces {
if namespace == skip {
return true
}
}
return false
}
func (nr *NamespaceReporter) generateNamespaceReport(ns *corev1.Namespace) (*NamespaceReport, error) {
nsReport := &NamespaceReport{
Name: ns.Name,
CreationTimestamp: ns.CreationTimestamp.Time,
Labels: ns.Labels,
}
// 获取 Pods
pods, err := nr.clientset.CoreV1().Pods(ns.Name).List(context.TODO(), metav1.ListOptions{})
if err != nil {
return nil, err
}
nsReport.PodCount = len(pods.Items)
// 统计资源请求和限制
for _, pod := range pods.Items {
if pod.Status.Phase == corev1.PodRunning {
nsReport.RunningPods++
}
for _, container := range pod.Spec.Containers {
// CPU Request
if cpu, ok := container.Resources.Requests[corev1.ResourceCPU]; ok {
nsReport.CPURequest += cpu.MilliValue()
}
// CPU Limit
if cpu, ok := container.Resources.Limits[corev1.ResourceCPU]; ok {
nsReport.CPULimit += cpu.MilliValue()
}
// Memory Request
if mem, ok := container.Resources.Requests[corev1.ResourceMemory]; ok {
nsReport.MemoryRequest += mem.Value()
}
// Memory Limit
if mem, ok := container.Resources.Limits[corev1.ResourceMemory]; ok {
nsReport.MemoryLimit += mem.Value()
}
}
}
// 获取实际使用量(从 metrics-server)
podMetrics, err := nr.metricsClient.MetricsV1beta1().PodMetricses(ns.Name).List(
context.TODO(),
metav1.ListOptions{},
)
if err == nil {
for _, pm := range podMetrics.Items {
for _, container := range pm.Containers {
nsReport.CPUUsage += container.Usage.Cpu().MilliValue()
nsReport.MemoryUsage += container.Usage.Memory().Value()
}
}
}
// 获取 Services
services, _ := nr.clientset.CoreV1().Services(ns.Name).List(context.TODO(), metav1.ListOptions{})
nsReport.ServiceCount = len(services.Items)
// 获取 ConfigMaps
configMaps, _ := nr.clientset.CoreV1().ConfigMaps(ns.Name).List(context.TODO(), metav1.ListOptions{})
nsReport.ConfigMapCount = len(configMaps.Items)
// 获取 Secrets
secrets, _ := nr.clientset.CoreV1().Secrets(ns.Name).List(context.TODO(), metav1.ListOptions{})
nsReport.SecretCount = len(secrets.Items)
// 获取 PVCs
pvcs, _ := nr.clientset.CoreV1().PersistentVolumeClaims(ns.Name).List(context.TODO(), metav1.ListOptions{})
nsReport.PVCCount = len(pvcs.Items)
for _, pvc := range pvcs.Items {
if storage, ok := pvc.Status.Capacity[corev1.ResourceStorage]; ok {
nsReport.StorageUsage += storage.Value()
}
}
// 获取资源配额
quotas, _ := nr.clientset.CoreV1().ResourceQuotas(ns.Name).List(context.TODO(), metav1.ListOptions{})
if len(quotas.Items) > 0 {
quota := "as.Items[0]
nsReport.ResourceQuota = &ResourceQuotaInfo{
Name: quota.Name,
}
if cpu, ok := quota.Spec.Hard[corev1.ResourceRequestsCPU]; ok {
nsReport.ResourceQuota.CPUHard = cpu.MilliValue()
}
if cpu, ok := quota.Status.Used[corev1.ResourceRequestsCPU]; ok {
nsReport.ResourceQuota.CPUUsed = cpu.MilliValue()
}
if mem, ok := quota.Spec.Hard[corev1.ResourceRequestsMemory]; ok {
nsReport.ResourceQuota.MemoryHard = mem.Value()
}
if mem, ok := quota.Status.Used[corev1.ResourceRequestsMemory]; ok {
nsReport.ResourceQuota.MemoryUsed = mem.Value()
}
if pods, ok := quota.Spec.Hard[corev1.ResourcePods]; ok {
nsReport.ResourceQuota.PodsHard = int(pods.Value())
}
if pods, ok := quota.Status.Used[corev1.ResourcePods]; ok {
nsReport.ResourceQuota.PodsUsed = int(pods.Value())
}
}
// 估算成本(简化计算)
nsReport.EstimatedCost = nr.calculateCost(nsReport)
return nsReport, nil
}
func (nr *NamespaceReporter) calculateCost(nsReport *NamespaceReport) float64 {
// 简化的成本计算
// CPU: $0.03 per core per hour
// Memory: $0.004 per GB per hour
// Storage: $0.10 per GB per month
cpuCores := float64(nsReport.CPURequest) / 1000.0
memoryGB := float64(nsReport.MemoryRequest) / (1024 * 1024 * 1024)
storageGB := float64(nsReport.StorageUsage) / (1024 * 1024 * 1024)
// 月成本
cpuCost := cpuCores * 0.03 * 24 * 30
memoryCost := memoryGB * 0.004 * 24 * 30
storageCost := storageGB * 0.10
return cpuCost + memoryCost + storageCost
}
pkg/reporter/exporter.go
package reporter
import (
"encoding/csv"
"encoding/json"
"fmt"
"html/template"
"os"
"github.com/xuri/excelize/v2"
)
func (nr *NamespaceReporter) ExportReport(report *Report, format, outputPath string) error {
switch format {
case "html":
return nr.exportHTML(report, outputPath)
case "json":
return nr.exportJSON(report, outputPath)
case "csv":
return nr.exportCSV(report, outputPath)
case "excel":
return nr.exportExcel(report, outputPath)
default:
return fmt.Errorf("unsupported format: %s", format)
}
}
func (nr *NamespaceReporter) exportHTML(report *Report, outputPath string) error {
tmpl := `
<!DOCTYPE html>
<html>
<head>
<title>Kubernetes Namespace Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #326CE5; }
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #326CE5; color: white; }
tr:nth-child(even) { background-color: #f2f2f2; }
.summary { background-color: #e8f4f8; padding: 15px; margin: 20px 0; }
</style>
</head>
<body>
<h1>Kubernetes Namespace Resource Report</h1>
<p>Generated at: {{.GeneratedAt.Format "2006-01-02 15:04:05"}}</p>
<p>Cluster: {{.ClusterName}}</p>
<div class="summary">
<h2>Summary</h2>
<p>Total Namespaces: {{.TotalNamespaces}}</p>
<p>Total CPU Request: {{divide .Summary.TotalCPURequest 1000}} cores</p>
<p>Total CPU Usage: {{divide .Summary.TotalCPUUsage 1000}} cores</p>
<p>Total Memory Request: {{bytesToGB .Summary.TotalMemoryRequest}} GB</p>
<p>Total Memory Usage: {{bytesToGB .Summary.TotalMemoryUsage}} GB</p>
<p>Total Storage: {{bytesToGB .Summary.TotalStorageUsage}} GB</p>
<p>Total Pods: {{.Summary.TotalPods}}</p>
<p>Total Estimated Cost: ${{printf "%.2f" .Summary.TotalEstimatedCost}}/month</p>
</div>
<h2>Namespace Details</h2>
<table>
<tr>
<th>Namespace</th>
<th>CPU Request (cores)</th>
<th>CPU Usage (cores)</th>
<th>Memory Request (GB)</th>
<th>Memory Usage (GB)</th>
<th>Storage (GB)</th>
<th>Pods</th>
<th>Estimated Cost</th>
</tr>
{{range .NamespaceReports}}
<tr>
<td>{{.Name}}</td>
<td>{{divide .CPURequest 1000}}</td>
<td>{{divide .CPUUsage 1000}}</td>
<td>{{bytesToGB .MemoryRequest}}</td>
<td>{{bytesToGB .MemoryUsage}}</td>
<td>{{bytesToGB .StorageUsage}}</td>
<td>{{.PodCount}}</td>
<td>${{printf "%.2f" .EstimatedCost}}</td>
</tr>
{{end}}
</table>
</body>
</html>
`
funcMap := template.FuncMap{
"divide": func(a, b int64) float64 {
return float64(a) / float64(b)
},
"bytesToGB": func(bytes int64) float64 {
return float64(bytes) / (1024 * 1024 * 1024)
},
}
t, err := template.New("report").Funcs(funcMap).Parse(tmpl)
if err != nil {
return err
}
f, err := os.Create(outputPath)
if err != nil {
return err
}
defer f.Close()
return t.Execute(f, report)
}
func (nr *NamespaceReporter) exportJSON(report *Report, outputPath string) error {
f, err := os.Create(outputPath)
if err != nil {
return err
}
defer f.Close()
encoder := json.NewEncoder(f)
encoder.SetIndent("", " ")
return encoder.Encode(report)
}
func (nr *NamespaceReporter) exportCSV(report *Report, outputPath string) error {
f, err := os.Create(outputPath)
if err != nil {
return err
}
defer f.Close()
writer := csv.NewWriter(f)
defer writer.Flush()
// Header
writer.Write([]string{
"Namespace",
"CPU Request (cores)",
"CPU Usage (cores)",
"Memory Request (GB)",
"Memory Usage (GB)",
"Storage (GB)",
"Pods",
"Estimated Cost ($)",
})
// Data
for _, ns := range report.NamespaceReports {
writer.Write([]string{
ns.Name,
fmt.Sprintf("%.2f", float64(ns.CPURequest)/1000),
fmt.Sprintf("%.2f", float64(ns.CPUUsage)/1000),
fmt.Sprintf("%.2f", float64(ns.MemoryRequest)/(1024*1024*1024)),
fmt.Sprintf("%.2f", float64(ns.MemoryUsage)/(1024*1024*1024)),
fmt.Sprintf("%.2f", float64(ns.StorageUsage)/(1024*1024*1024)),
fmt.Sprintf("%d", ns.PodCount),
fmt.Sprintf("%.2f", ns.EstimatedCost),
})
}
return nil
}
func (nr *NamespaceReporter) exportExcel(report *Report, outputPath string) error {
f := excelize.NewFile()
defer f.Close()
sheetName := "Resource Report"
index, _ := f.NewSheet(sheetName)
// Header
headers := []string{"Namespace", "CPU Request", "CPU Usage", "Memory Request", "Memory Usage", "Storage", "Pods", "Cost"}
for i, h := range headers {
cell := fmt.Sprintf("%c1", 'A'+i)
f.SetCellValue(sheetName, cell, h)
}
// Data
for i, ns := range report.NamespaceReports {
row := i + 2
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), ns.Name)
f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), float64(ns.CPURequest)/1000)
f.SetCellValue(sheetName, fmt.Sprintf("C%d", row), float64(ns.CPUUsage)/1000)
f.SetCellValue(sheetName, fmt.Sprintf("D%d", row), float64(ns.MemoryRequest)/(1024*1024*1024))
f.SetCellValue(sheetName, fmt.Sprintf("E%d", row), float64(ns.MemoryUsage)/(1024*1024*1024))
f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), float64(ns.StorageUsage)/(1024*1024*1024))
f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), ns.PodCount)
f.SetCellValue(sheetName, fmt.Sprintf("H%d", row), ns.EstimatedCost)
}
f.SetActiveSheet(index)
return f.SaveAs(outputPath)
}
Python 实现
namespace_reporter.py
#!/usr/bin/env python3
import argparse
import json
import csv
from datetime import datetime
from kubernetes import client, config
from jinja2 import Template
import openpyxl
from openpyxl.styles import Font, PatternFill
class NamespaceReporter:
def __init__(self, cluster_name='default'):
# 加载 Kubernetes 配置
try:
config.load_incluster_config()
except:
config.load_kube_config()
self.v1 = client.CoreV1Api()
self.metrics_api = client.CustomObjectsApi()
self.cluster_name = cluster_name
def generate_report(self):
"""生成报表"""
print("Generating namespace report...")
# 获取所有命名空间
namespaces = self.v1.list_namespace()
report = {
'generated_at': datetime.now(),
'cluster_name': self.cluster_name,
'total_namespaces': len(namespaces.items),
'namespace_reports': [],
'summary': {
'total_cpu_request': 0,
'total_cpu_usage': 0,
'total_memory_request': 0,
'total_memory_usage': 0,
'total_storage': 0,
'total_pods': 0,
'total_cost': 0
}
}
# 生成每个命名空间的报表
for ns in namespaces.items:
if self.should_skip_namespace(ns.metadata.name):
continue
ns_report = self.generate_namespace_report(ns)
report['namespace_reports'].append(ns_report)
# 更新汇总
report['summary']['total_cpu_request'] += ns_report['cpu_request']
report['summary']['total_cpu_usage'] += ns_report['cpu_usage']
report['summary']['total_memory_request'] += ns_report['memory_request']
report['summary']['total_memory_usage'] += ns_report['memory_usage']
report['summary']['total_storage'] += ns_report['storage_usage']
report['summary']['total_pods'] += ns_report['pod_count']
report['summary']['total_cost'] += ns_report['estimated_cost']
# 按 CPU 使用量排序
report['namespace_reports'].sort(
key=lambda x: x['cpu_usage'],
reverse=True
)
print(f"Report generated for {len(report['namespace_reports'])} namespaces")
return report
def should_skip_namespace(self, namespace):
"""检查是否跳过命名空间"""
skip_namespaces = ['kube-system', 'kube-public', 'kube-node-lease']
return namespace in skip_namespaces
def generate_namespace_report(self, ns):
"""生成单个命名空间报表"""
ns_name = ns.metadata.name
report = {
'name': ns_name,
'creation_timestamp': ns.metadata.creation_timestamp,
'cpu_request': 0,
'cpu_limit': 0,
'cpu_usage': 0,
'memory_request': 0,
'memory_limit': 0,
'memory_usage': 0,
'storage_usage': 0,
'pod_count': 0,
'running_pods': 0,
'service_count': 0,
'configmap_count': 0,
'secret_count': 0,
'pvc_count': 0,
'estimated_cost': 0
}
# 获取 Pods
try:
pods = self.v1.list_namespaced_pod(ns_name)
report['pod_count'] = len(pods.items)
for pod in pods.items:
if pod.status.phase == 'Running':
report['running_pods'] += 1
for container in pod.spec.containers:
if container.resources.requests:
if 'cpu' in container.resources.requests:
report['cpu_request'] += self.parse_cpu(
container.resources.requests['cpu']
)
if 'memory' in container.resources.requests:
report['memory_request'] += self.parse_memory(
container.resources.requests['memory']
)
if container.resources.limits:
if 'cpu' in container.resources.limits:
report['cpu_limit'] += self.parse_cpu(
container.resources.limits['cpu']
)
if 'memory' in container.resources.limits:
report['memory_limit'] += self.parse_memory(
container.resources.limits['memory']
)
except Exception as e:
print(f"Error getting pods for {ns_name}: {e}")
# 获取实际使用量
try:
pod_metrics = self.metrics_api.list_namespaced_custom_object(
group="metrics.k8s.io",
version="v1beta1",
namespace=ns_name,
plural="pods"
)
for pm in pod_metrics.get('items', []):
for container in pm.get('containers', []):
usage = container.get('usage', {})
if 'cpu' in usage:
report['cpu_usage'] += self.parse_cpu(usage['cpu'])
if 'memory' in usage:
report['memory_usage'] += self.parse_memory(usage['memory'])
except Exception as e:
print(f"Error getting metrics for {ns_name}: {e}")
# 获取其他资源
try:
services = self.v1.list_namespaced_service(ns_name)
report['service_count'] = len(services.items)
except:
pass
try:
configmaps = self.v1.list_namespaced_config_map(ns_name)
report['configmap_count'] = len(configmaps.items)
except:
pass
try:
secrets = self.v1.list_namespaced_secret(ns_name)
report['secret_count'] = len(secrets.items)
except:
pass
try:
pvcs = self.v1.list_namespaced_persistent_volume_claim(ns_name)
report['pvc_count'] = len(pvcs.items)
for pvc in pvcs.items:
if pvc.status.capacity and 'storage' in pvc.status.capacity:
report['storage_usage'] += self.parse_memory(
pvc.status.capacity['storage']
)
except:
pass
# 计算成本
report['estimated_cost'] = self.calculate_cost(report)
return report
def parse_cpu(self, cpu_str):
"""解析 CPU(转换为 millicores)"""
if cpu_str.endswith('m'):
return int(cpu_str[:-1])
elif cpu_str.endswith('n'):
return int(cpu_str[:-1]) // 1000000
else:
return int(float(cpu_str) * 1000)
def parse_memory(self, mem_str):
"""解析内存(转换为 bytes)"""
units = {
'Ki': 1024, 'Mi': 1024**2, 'Gi': 1024**3, 'Ti': 1024**4,
'K': 1000, 'M': 1000**2, 'G': 1000**3, 'T': 1000**4
}
for unit, multiplier in units.items():
if mem_str.endswith(unit):
return int(mem_str[:-len(unit)]) * multiplier
return int(mem_str)
def calculate_cost(self, report):
"""计算成本"""
cpu_cores = report['cpu_request'] / 1000.0
memory_gb = report['memory_request'] / (1024**3)
storage_gb = report['storage_usage'] / (1024**3)
# 月成本
cpu_cost = cpu_cores * 0.03 * 24 * 30
memory_cost = memory_gb * 0.004 * 24 * 30
storage_cost = storage_gb * 0.10
return cpu_cost + memory_cost + storage_cost
def export_html(self, report, output_path):
"""导出 HTML"""
template = Template('''
<!DOCTYPE html>
<html>
<head>
<title>Kubernetes Namespace Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #326CE5; }
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #326CE5; color: white; }
tr:nth-child(even) { background-color: #f2f2f2; }
.summary { background-color: #e8f4f8; padding: 15px; margin: 20px 0; }
</style>
</head>
<body>
<h1>Kubernetes Namespace Resource Report</h1>
<p>Generated at: {{ report.generated_at.strftime('%Y-%m-%d %H:%M:%S') }}</p>
<p>Cluster: {{ report.cluster_name }}</p>
<div class="summary">
<h2>Summary</h2>
<p>Total Namespaces: {{ report.total_namespaces }}</p>
<p>Total CPU Request: {{ "%.2f"|format(report.summary.total_cpu_request / 1000) }} cores</p>
<p>Total CPU Usage: {{ "%.2f"|format(report.summary.total_cpu_usage / 1000) }} cores</p>
<p>Total Memory Request: {{ "%.2f"|format(report.summary.total_memory_request / (1024**3)) }} GB</p>
<p>Total Memory Usage: {{ "%.2f"|format(report.summary.total_memory_usage / (1024**3)) }} GB</p>
<p>Total Storage: {{ "%.2f"|format(report.summary.total_storage / (1024**3)) }} GB</p>
<p>Total Pods: {{ report.summary.total_pods }}</p>
<p>Total Estimated Cost: ${{ "%.2f"|format(report.summary.total_cost) }}/month</p>
</div>
<h2>Namespace Details</h2>
<table>
<tr>
<th>Namespace</th>
<th>CPU Request (cores)</th>
<th>CPU Usage (cores)</th>
<th>Memory Request (GB)</th>
<th>Memory Usage (GB)</th>
<th>Storage (GB)</th>
<th>Pods</th>
<th>Estimated Cost</th>
</tr>
{% for ns in report.namespace_reports %}
<tr>
<td>{{ ns.name }}</td>
<td>{{ "%.2f"|format(ns.cpu_request / 1000) }}</td>
<td>{{ "%.2f"|format(ns.cpu_usage / 1000) }}</td>
<td>{{ "%.2f"|format(ns.memory_request / (1024**3)) }}</td>
<td>{{ "%.2f"|format(ns.memory_usage / (1024**3)) }}</td>
<td>{{ "%.2f"|format(ns.storage_usage / (1024**3)) }}</td>
<td>{{ ns.pod_count }}</td>
<td>${{ "%.2f"|format(ns.estimated_cost) }}</td>
</tr>
{% endfor %}
</table>
</body>
</html>
''')
with open(output_path, 'w') as f:
f.write(template.render(report=report))
def export_json(self, report, output_path):
"""导出 JSON"""
# 转换 datetime 为字符串
report_copy = report.copy()
report_copy['generated_at'] = report['generated_at'].isoformat()
for ns in report_copy['namespace_reports']:
if 'creation_timestamp' in ns and ns['creation_timestamp']:
ns['creation_timestamp'] = ns['creation_timestamp'].isoformat()
with open(output_path, 'w') as f:
json.dump(report_copy, f, indent=2)
def export_csv(self, report, output_path):
"""导出 CSV"""
with open(output_path, 'w', newline='') as f:
writer = csv.writer(f)
# Header
writer.writerow([
'Namespace', 'CPU Request (cores)', 'CPU Usage (cores)',
'Memory Request (GB)', 'Memory Usage (GB)', 'Storage (GB)',
'Pods', 'Estimated Cost ($)'
])
# Data
for ns in report['namespace_reports']:
writer.writerow([
ns['name'],
f"{ns['cpu_request']/1000:.2f}",
f"{ns['cpu_usage']/1000:.2f}",
f"{ns['memory_request']/(1024**3):.2f}",
f"{ns['memory_usage']/(1024**3):.2f}",
f"{ns['storage_usage']/(1024**3):.2f}",
ns['pod_count'],
f"{ns['estimated_cost']:.2f}"
])
def export_excel(self, report, output_path):
"""导出 Excel"""
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Resource Report"
# Header
headers = [
'Namespace', 'CPU Request', 'CPU Usage', 'Memory Request',
'Memory Usage', 'Storage', 'Pods', 'Cost'
]
for col, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col, value=header)
cell.font = Font(bold=True)
cell.fill = PatternFill(start_color="326CE5", end_color="326CE5", fill_type="solid")
# Data
for row, ns in enumerate(report['namespace_reports'], 2):
ws.cell(row=row, column=1, value=ns['name'])
ws.cell(row=row, column=2, value=ns['cpu_request']/1000)
ws.cell(row=row, column=3, value=ns['cpu_usage']/1000)
ws.cell(row=row, column=4, value=ns['memory_request']/(1024**3))
ws.cell(row=row, column=5, value=ns['memory_usage']/(1024**3))
ws.cell(row=row, column=6, value=ns['storage_usage']/(1024**3))
ws.cell(row=row, column=7, value=ns['pod_count'])
ws.cell(row=row, column=8, value=ns['estimated_cost'])
wb.save(output_path)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Kubernetes Namespace Reporter')
parser.add_argument('--format', default='html', choices=['html', 'json', 'csv', 'excel'],
help='Output format')
parser.add_argument('--output', default='report.html', help='Output file path')
parser.add_argument('--cluster-name', default='default', help='Cluster name')
args = parser.parse_args()
reporter = NamespaceReporter(cluster_name=args.cluster_name)
report = reporter.generate_report()
if args.format == 'html':
reporter.export_html(report, args.output)
elif args.format == 'json':
reporter.export_json(report, args.output)
elif args.format == 'csv':
reporter.export_csv(report, args.output)
elif args.format == 'excel':
reporter.export_excel(report, args.output)
print(f"Report exported to {args.output}")
使用示例
生成 HTML 报表
# Go
./namespace-reporter --format=html --output=report.html
# Python
python3 namespace_reporter.py --format=html --output=report.html
生成 Excel 报表
# Go
./namespace-reporter --format=excel --output=report.xlsx
# Python
python3 namespace_reporter.py --format=excel --output=report.xlsx
定时生成报表(CronJob)
apiVersion: batch/v1
kind: CronJob
metadata:
name: namespace-reporter
namespace: kube-system
spec:
schedule: "0 0 * * *" # 每天凌晨
jobTemplate:
spec:
template:
spec:
serviceAccountName: namespace-reporter
containers:
- name: reporter
image: your-registry/namespace-reporter:latest
args:
- --format=html
- --output=/reports/report-$(date +%Y%m%d).html
volumeMounts:
- name: reports
mountPath: /reports
restartPolicy: OnFailure
volumes:
- name: reports
persistentVolumeClaim:
claimName: reports-pvc
总结
功能特性
✅ 多维度统计: CPU、内存、存储、Pod 数量
✅ 成本估算: 基于资源使用的成本计算
✅ 多种格式: HTML、JSON、CSV、Excel
✅ 定时生成: 支持 CronJob 定时运行
✅ 可视化: 美观的 HTML 报表
扩展方向
- 趋势分析: 对比历史数据
- 异常检测: 识别资源使用异常
- 邮件推送: 自动发送报表
- Dashboard: Grafana 可视化
- 成本优化建议: 基于使用情况给出优化建议
下一个项目将介绍应用健康检查自愈工具。