commit 600e641560424e2d2c0a5fe1444e07b74ef7a29b Author: Daniel Sy Date: Tue Mar 25 17:53:35 2025 +0100 ✨ Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9986946 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Go workspace file +go.work + +# Go modules +go.mod +go.sum + +# Dependency directories +vendor/ + +# IDE and editor files +.idea/ +.vscode/ +*.swp + +# Logs +*.log + +# OS generated files +.DS_Store +Thumbs.db + +# Custom files +loic \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..69db6da --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "os" +) + +var rootCmd = &cobra.Command{ + Use: "loic", + Short: "LOIC-Anwendung für Lasttests", + Long: `Eine Anwendung für skalierbare Lasttests im Kubernetes-Cluster.`, +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/cmd/start.go b/cmd/start.go new file mode 100644 index 0000000..39cd680 --- /dev/null +++ b/cmd/start.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "fmt" + "forgejo.edf-bootstrap.cx.fg1.ffm.osc.live/Daniel.Sy/loic-go/pkg/loic" + "github.com/spf13/cobra" + "time" +) + +var startCmd = &cobra.Command{ + Use: "start", + Short: "Startet einen Lasttest", + Run: func(cmd *cobra.Command, args []string) { + targetURL, _ := cmd.Flags().GetString("target") + concurrency, _ := cmd.Flags().GetInt("concurrency") + duration, _ := cmd.Flags().GetDuration("duration") + rampUp, _ := cmd.Flags().GetDuration("ramp-up") + fmt.Printf("Starting test with target %s, concurrency %d, duration %v, ramp-up %v\n", targetURL, concurrency, duration, rampUp) + loic.StartTest(targetURL, concurrency, duration, rampUp) + }, +} + +func init() { + startCmd.Flags().StringP("target", "t", "", "Ziel-URL für den Test (z. B. http://service.cluster.local oder http://example.com)") + startCmd.Flags().IntP("concurrency", "c", 10, "Maximale Anzahl paralleler Anfragen") + startCmd.Flags().DurationP("duration", "d", time.Minute, "Dauer des Tests") + startCmd.Flags().DurationP("ramp-up", "r", 30*time.Second, "Zeit für schrittweise Erhöhung der Last") + rootCmd.AddCommand(startCmd) +} diff --git a/cmd/status.go b/cmd/status.go new file mode 100644 index 0000000..da37aa3 --- /dev/null +++ b/cmd/status.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "fmt" + "forgejo.edf-bootstrap.cx.fg1.ffm.osc.live/Daniel.Sy/loic-go/pkg/loic" + "github.com/spf13/cobra" +) + +var statusCmd = &cobra.Command{ + Use: "status", + Short: "Displays test status", + Long: "Display test status of the application", + Run: func(cmd *cobra.Command, args []string) { + if loic.IsTestRunning() { + fmt.Println("Status: running") + return + } else { + fmt.Println("Status: stopped") + } + }, +} + +func init() { + rootCmd.AddCommand(statusCmd) +} diff --git a/cmd/stop.go b/cmd/stop.go new file mode 100644 index 0000000..b96ed10 --- /dev/null +++ b/cmd/stop.go @@ -0,0 +1,18 @@ +package cmd + +import ( + "forgejo.edf-bootstrap.cx.fg1.ffm.osc.live/Daniel.Sy/loic-go/pkg/loic" + "github.com/spf13/cobra" +) + +var stopCmd = &cobra.Command{ + Use: "stop", + Short: "Stoppt den aktuellen Lasttest", + Run: func(cmd *cobra.Command, args []string) { + loic.StopTest() + }, +} + +func init() { + rootCmd.AddCommand(stopCmd) +} diff --git a/cmd/web.go b/cmd/web.go new file mode 100644 index 0000000..21b8d92 --- /dev/null +++ b/cmd/web.go @@ -0,0 +1,19 @@ +package cmd + +import ( + "forgejo.edf-bootstrap.cx.fg1.ffm.osc.live/Daniel.Sy/loic-go/pkg/web" + "github.com/spf13/cobra" +) + +var webCmd = &cobra.Command{ + Use: "web", + Short: "Startet den Web-Server", + Long: "Startet den Web-Server für myapp", + Run: func(cmd *cobra.Command, args []string) { + web.Start() + }, +} + +func init() { + rootCmd.AddCommand(webCmd) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..59409c9 --- /dev/null +++ b/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "context" + "forgejo.edf-bootstrap.cx.fg1.ffm.osc.live/Daniel.Sy/loic-go/cmd" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" + "log" + "time" +) + +func main() { + // Ressource definieren + res, err := resource.New(context.Background(), + resource.WithAttributes( + semconv.ServiceNameKey.String("loic"), + semconv.ServiceVersionKey.String("0.1.0"), + ), + ) + if err != nil { + log.Fatalf("failed to create resource: %v", err) + } + + // Exporter erstellen (stdout für Tests) + exporter, err := stdoutmetric.New() + if err != nil { + log.Fatalf("failed to create stdout exporter: %v", err) + } + + // Periodic Reader erstellen + reader := metric.NewPeriodicReader(exporter, metric.WithInterval(10*time.Second)) + + // MeterProvider initialisieren + meterProvider := metric.NewMeterProvider( + metric.WithResource(res), + metric.WithReader(reader), + ) + defer meterProvider.Shutdown(context.Background()) + + // Globalen MeterProvider setzen + otel.SetMeterProvider(meterProvider) + + // CLI ausführen + cmd.Execute() +} diff --git a/pkg/loic/loic.go b/pkg/loic/loic.go new file mode 100644 index 0000000..ece29f8 --- /dev/null +++ b/pkg/loic/loic.go @@ -0,0 +1,108 @@ +package loic + +import ( + "context" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/metric" + "log" + "net/http" + "sync" + "time" +) + +var meter = otel.Meter("loic") +var requestsSent metric.Int64Counter +var currentTest *Test +var isRunning bool + +func init() { + var err error + requestsSent, err = meter.Int64Counter("requests_sent", + metric.WithDescription("Number of requests sent"), + metric.WithUnit("1"), + ) + if err != nil { + log.Fatalf("failed to create counter: %v", err) + } +} + +type Test struct { + TargetURL string + Concurrency int + Duration time.Duration + RampUpTime time.Duration + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup +} + +func StartTest(targetURL string, concurrency int, duration time.Duration, rampUp time.Duration) { + ctx, cancel := context.WithCancel(context.Background()) + currentTest := &Test{ + TargetURL: targetURL, + Concurrency: concurrency, + Duration: duration, + RampUpTime: rampUp, + ctx: ctx, + cancel: cancel, + } + + go currentTest.run() +} + +func StopTest() { + if currentTest != nil && isRunning { + currentTest.cancel() + currentTest.wg.Wait() + currentTest = nil + isRunning = false + log.Println("Test stopped") + } +} + +func IsTestRunning() bool { + return isRunning +} + +func (t *Test) run() { + defer func() { isRunning = false }() + ticker := time.NewTicker(t.RampUpTime / time.Duration(t.Concurrency)) + defer ticker.Stop() + currentConcurrency := 1 + for i := 0; i < t.Concurrency; i++ { + select { + case <-ticker.C: + if currentConcurrency < t.Concurrency { + currentConcurrency++ + t.startWorker() + } + case <-time.After(t.Duration): + t.cancel() + case <-t.ctx.Done(): + return + } + } + t.wg.Wait() +} + +func (t *Test) startWorker() { + t.wg.Add(1) + go func() { + defer t.wg.Done() + for { + select { + case <-t.ctx.Done(): + return + default: + resp, err := http.Get(t.TargetURL) + if err != nil { + log.Println("Error:", err) + } else { + resp.Body.Close() + requestsSent.Add(t.ctx, 1) + } + time.Sleep(time.Second / time.Duration(t.Concurrency)) + } + } + }() +} diff --git a/pkg/web/templates/index.html b/pkg/web/templates/index.html new file mode 100644 index 0000000..35f2406 --- /dev/null +++ b/pkg/web/templates/index.html @@ -0,0 +1,52 @@ + + + + + LOIC Web UI + + + +
+

LOIC Control Panel

+
+
+
Start Test
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+
+
Stop Test
+
+ +
+
+
+
+
+
Status
+

Current Status: {{ .Status }}

+
+
+
+ + + \ No newline at end of file diff --git a/pkg/web/web.go b/pkg/web/web.go new file mode 100644 index 0000000..88b1296 --- /dev/null +++ b/pkg/web/web.go @@ -0,0 +1,41 @@ +package web + +import ( + "forgejo.edf-bootstrap.cx.fg1.ffm.osc.live/Daniel.Sy/loic-go/pkg/loic" + "github.com/gin-gonic/gin" + "net/http" + "strconv" + "time" +) + +func Start() { + r := gin.Default() + r.LoadHTMLGlob("pkg/web/templates/*") + + r.GET("/", func(c *gin.Context) { + status := "stopped" + if loic.IsTestRunning() { + status = "running" + } + c.HTML(http.StatusOK, "index.html", gin.H{ + "Status": status, + }) + }) + + r.POST("/start", func(c *gin.Context) { + targetURL := c.PostForm("target_url") + concurrency, _ := strconv.Atoi(c.PostForm("concurrency")) + duration, _ := time.ParseDuration(c.PostForm("duration")) + rampUp, _ := time.ParseDuration(c.PostForm("ramp_up")) + + go loic.StartTest(targetURL, concurrency, duration, rampUp) + c.Redirect(http.StatusSeeOther, "/") + }) + + r.POST("/stop", func(c *gin.Context) { + loic.StopTest() + c.Redirect(http.StatusSeeOther, "/") + }) + + r.Run(":8080") +}