From ce8308a8ff326c0252a524f32e69cebfb40da1ac Mon Sep 17 00:00:00 2001 From: ririnto Date: Sat, 18 Jan 2025 02:51:59 +0900 Subject: [PATCH 01/11] fork. --- .editorconfig | 89 ++++++++++++ .github/workflows/deploy.yml | 45 ++++++ .gitignore | 41 +++++- .travis.yml | 18 --- README.md | 177 ++++++++++++++++++++--- cmd/root.go | 81 +++++++++++ cmd/root_test.go | 12 ++ cmd/serve.go | 146 +++++++++++++++++++ cmd/serve_test.go | 27 ++++ go.mod | 26 ++++ go.sum | 52 +++++++ internal/config/config.go | 26 ++++ internal/config/config_test.go | 99 +++++++++++++ internal/exporter/errors.go | 6 + internal/exporter/errors_test.go | 38 +++++ internal/exporter/exporter.go | 112 +++++++++++++++ internal/exporter/exporter_test.go | 46 ++++++ internal/monit/monit.go | 116 +++++++++++++++ internal/monit/monit_test.go | 133 +++++++++++++++++ main.go | 10 ++ monit_exporter.go | 221 ----------------------------- monit_exporter_test.go | 124 ---------------- 22 files changed, 1265 insertions(+), 380 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/workflows/deploy.yml delete mode 100644 .travis.yml create mode 100644 cmd/root.go create mode 100644 cmd/root_test.go create mode 100644 cmd/serve.go create mode 100644 cmd/serve_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/config/config.go create mode 100644 internal/config/config_test.go create mode 100644 internal/exporter/errors.go create mode 100644 internal/exporter/errors_test.go create mode 100644 internal/exporter/exporter.go create mode 100644 internal/exporter/exporter_test.go create mode 100644 internal/monit/monit.go create mode 100644 internal/monit/monit_test.go create mode 100644 main.go delete mode 100644 monit_exporter.go delete mode 100644 monit_exporter_test.go diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..27413d7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,89 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = +ij_wrap_on_typing = false + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.go,*.go2}] +indent_style = tab +ij_continuation_indent_size = 4 +ij_go_GROUP_CURRENT_PROJECT_IMPORTS = false +ij_go_add_leading_space_to_comments = false +ij_go_add_parentheses_for_single_import = false +ij_go_call_parameters_new_line_after_left_paren = true +ij_go_call_parameters_right_paren_on_new_line = true +ij_go_call_parameters_wrap = off +ij_go_fill_paragraph_width = 80 +ij_go_group_stdlib_imports = false +ij_go_import_sorting = gofmt +ij_go_keep_indents_on_empty_lines = false +ij_go_local_group_mode = project +ij_go_local_package_prefixes = +ij_go_move_all_imports_in_one_declaration = false +ij_go_move_all_stdlib_imports_in_one_group = false +ij_go_remove_redundant_import_aliases = false +ij_go_run_go_fmt_on_reformat = true +ij_go_use_back_quotes_for_imports = false +ij_go_wrap_comp_lit = off +ij_go_wrap_comp_lit_newline_after_lbrace = true +ij_go_wrap_comp_lit_newline_before_rbrace = true +ij_go_wrap_func_params = off +ij_go_wrap_func_params_newline_after_lparen = true +ij_go_wrap_func_params_newline_before_rparen = true +ij_go_wrap_func_result = off +ij_go_wrap_func_result_newline_after_lparen = true +ij_go_wrap_func_result_newline_before_rparen = true + +[{*.markdown,*.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_format_tables = true +ij_markdown_insert_quote_arrows_on_wrap = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_keep_line_breaks_inside_text_blocks = true +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 +ij_markdown_wrap_text_if_long = true +ij_markdown_wrap_text_inside_blockquotes = true + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_line_comment_add_space = false +ij_yaml_line_comment_add_space_on_reformat = false +ij_yaml_line_comment_at_first_column = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..5f1fc85 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,45 @@ +name: Go Monit Exporter Deployment + +on: + push: + tags: + - "v*" + +jobs: + release: + name: Release + runs-on: ubuntu-latest + + strategy: + matrix: + goos: [ linux, darwin, windows ] + goarch: [ amd64, arm64 ] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.23.5 + + - name: Build binary + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + run: | + mkdir -p build + if [ "${GOOS}" = "windows" ]; then + BINARY_NAME=monit_exporter-${GOOS}-${GOARCH}.exe + else + BINARY_NAME=monit_exporter-${GOOS}-${GOARCH} + fi + CGO_ENABLED=0 go build -ldflags="-s -w" -o build/${BINARY_NAME} + + - name: Upload binaries to GitHub Releases + uses: softprops/action-gh-release@v1 + with: + files: build/monit_exporter-* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 62c8935..1a1abc7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,40 @@ -.idea/ \ No newline at end of file +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea/**/aws.xml +.idea/**/contentModel.xml +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml +.idea/**/gradle.xml +.idea/**/libraries +cmake-build-*/ +.idea/**/mongoSettings.xml +*.iws +out/ +.idea_modules/ +atlassian-ide-plugin.xml +.idea/replstate.xml +.idea/sonarlint/ +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +.idea/httpRequests +.idea/caches/build_file_checksums.ser +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +go.work +go.work.sum +.env diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9ac6267..0000000 --- a/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -language: go -go: - - 1.9 -script: - - go test -v ./... - - go build - - sha256sum monit_exporter > monit_exporter.sha256 -deploy: - provider: releases - api_key: - secure: s5ih9FzIXMpqLND+FINaDFcbQ0Tmp7BJQzZZ61lJs2Y+fWZAhgqPvxR1sTweOiZKKSR4oY5irskcvU5tEK6QzaKwuQH1ivtIMZjQr1LzGVoluYe3lVm5VqJTmmvZmiCNPvKeOqPUAVYvn0l9ifAnhbyk6H60yjevlfgd1RvHwggQhdPVCX6+/XKJI2BCmrSDTc5VjfHGd/sZ/9O7iiYAmbm9zAEJB1j/f7B11DvSHJ3bxSh6KWFWO9pcx0E7+k4krKFu2ICprztAvOV007xBuivX+c8my9gZ38hFBvf0oVL2uYYG7pxhUtvJdpR3PwLv4xIVim4CSPrxC3vUN7kspEFGlXNY4fsXbTdSo+QVvDrGG+VQ/3Ab/rJMHs1hF5F8eXS6jeIkt8ktXsxHlxJ0L72qaH7/gYcdHzlvP9TN6A5QEN+cSNMn1wH1SWWdegLn+JeI2fQ6ZXed5P8xTq+6mCPRhh1zyeea6cxBwUylzanF0Vo2GH26VX5A4VoT5OBw7pRc9jpVBhDLG2Z8OeXjeJUl6txB4dJYZzcDR1NIyh1bFUEfl6id9IZrmwKtnGBr2+iK9yuG74sb/+NLDbB37r9tXQ5qFVU2dQOWkUwrzEoz3m2wFTQmjVM02eh5IbwdIFSVPLkTYP+vyQSnIYf40hsVT3l542QI4iYLfAiU/XY= - file: - - monit_exporter - - monit_exporter.sha256 - skip_cleanup: true - on: - public_repo: commercetools/monit_exporter - tags: true diff --git a/README.md b/README.md index 7abea82..fc99771 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,174 @@ # Monit Exporter for Prometheus -Simple server that periodically scrapes monit status and exports checks information via HTTP for Prometheus. +> **Forked from** [commercetools/monit_exporter](https://github.com/commercetools/monit_exporter) + +## English + +### Introduction + +Monit Exporter is a Prometheus Exporter that scrapes Monit status in XML format, then exposes the metrics via an HTTP +endpoint. It uses [Cobra](https://github.com/spf13/cobra) for the command-line interface and logs HTTP requests in +Common Log Format (CLF). + +### Features + +- Scrapes Monit status periodically +- Exposes Prometheus-compatible metrics +- CLI integration with Cobra +- Logs HTTP requests in CLF +- Fully configurable via command-line flags + +### Installation + +1. Install [Go](https://golang.org/dl/) (version 1.16 or higher recommended). +2. Clone the repository and build: + + ```bash + git clone https://github.com/yourusername/monit_exporter.git + cd monit_exporter + go build -o monit_exporter + ``` + +### Usage + +#### Commands + +- **serve**: Starts the Monit Exporter server. + +#### Flags + +Below is an overview of the flags defined in `cmd/root.go`: + +| Flag | Default | Description | +|--------------------|-------------------------------------------------------|--------------------------------------------------------------------------| +| `listen-address` | `localhost:9388` | The address on which the exporter.go will listen (e.g., '0.0.0.0:9388'). | +| `metrics-path` | `/metrics` | The HTTP path at which metrics are served (e.g., '/metrics'). | +| `ignore-ssl` | `false` | Whether to skip SSL certificate verification for Monit endpoints. | +| `monit-scrape-uri` | `http://localhost:2812/_status?format=xml&level=full` | The Monit status URL to scrape (XML format). | +| `monit-user` | *(empty)* | Basic auth username for accessing Monit. | +| `monit-password` | *(empty)* | Basic auth password for accessing Monit. | +| `log-level` | `info` | Log level for the application (debug, info, warn, error, fatal, panic). | + +Launch the exporter with desired flags: + +```bash +./monit_exporter serve \ + --listen-address="0.0.0.0:9388" \ + --monit-scrape-uri="http://localhost:2812/_status?format=xml&level=full" \ + --monit-user="admin" \ + --monit-password="monitpassword" \ + --log-level="info" +``` + +Visit the metrics endpoint: + +```bash +curl http://localhost:9388/metrics +``` + +### Project / Package Structure + +``` +. +├── cmd +│ ├── root.go (Defines root command and flags) +│ └── serve.go (Implements 'serve' command, server startup) +├── internal +│ ├── config +│ │ └── config.go (Holds the Config struct for the exporter) +│ ├── exporter +│ │ └── exporter.go (Implements the Prometheus Exporter logic) +│ └── monit +│ └── monit.go (Fetches and parses Monit status data) +└── main.go (Entrypoint: calls cmd.Execute()) +``` + +### License + +This project is licensed under the [MIT License](LICENSE). + +--- + +## 한국어 + +### 개요 + +Monit Exporter는 Monit 상태 정보를 XML 형식으로 수집하고, 이를 Prometheus 메트릭으로 변환하여 HTTP 엔드포인트로 노출하는 +Exporter입니다. [Cobra](https://github.com/spf13/cobra)를 사용하여 CLI를 제공하며, HTTP 요청을 Common Log Format(CLF)으로 로깅합니다. + +### 기능 + +- 주기적으로 Monit 상태를 스크랩 +- Prometheus 호환 형식으로 메트릭 노출 +- Cobra 기반 CLI +- Common Log Format 로깅 +- 커맨드 라인 플래그로 모든 설정 가능 + +### 설치 + +1. [Go](https://golang.org/dl/) (버전 1.16 이상 권장)을 설치합니다. +2. 저장소를 클론하고 빌드합니다: + + ```bash + git clone https://github.com/yourusername/monit_exporter.git + cd monit_exporter + go build -o monit_exporter + ``` + +### 사용법 + +#### 명령어 + +- **serve**: Monit Exporter 서버를 시작합니다. + +#### 플래그 + +`cmd/root.go`에서 정의된 플래그는 다음 표와 같습니다: + +| Flag | 기본값 | 설명 | +|--------------------|-------------------------------------------------------|--------------------------------------------------------| +| `listen-address` | `localhost:9388` | Exporter가 수신할 주소 및 포트 (예: `0.0.0.0:9388`) | +| `metrics-path` | `/metrics` | 메트릭이 제공될 HTTP 경로 (예: `/metrics`) | +| `ignore-ssl` | `false` | Monit 엔드포인트에 대해 SSL 인증서 검증을 무시할지 여부 | +| `monit-scrape-uri` | `http://localhost:2812/_status?format=xml&level=full` | Monit 상태를 스크랩할 XML URL | +| `monit-user` | *(없음)* | Monit에 접근하기 위한 Basic auth 사용자 이름 | +| `monit-password` | *(없음)* | Monit에 접근하기 위한 Basic auth 비밀번호 | +| `log-level` | `info` | 애플리케이션의 로그 레벨 (debug, info, warn, error, fatal, panic) | + +Exporter를 다음과 같이 실행할 수 있습니다: -Build it: ```bash -go build +./monit_exporter serve \ + --listen-address="0.0.0.0:9388" \ + --monit-scrape-uri="http://localhost:2812/_status?format=xml&level=full" \ + --monit-user="admin" \ + --monit-password="monitpassword" \ + --log-level="info" ``` -Run it: +그리고 다음처럼 메트릭 엔드포인트를 확인합니다: ```bash -./monit_exporter +curl http://localhost:9388/metrics ``` -## Configuration +### 패키지 구조 -The application will look for configuration in "config.toml" file located in the same directory. Use -conf flag to override config file name and location. +``` +. +├── cmd +│ ├── root.go (루트 명령 및 플래그 설정) +│ └── serve.go (serve 명령 구현 및 서버 실행) +├── internal +│ ├── config +│ │ └── config.go (Exporter를 위한 설정 구조체) +│ ├── exporter +│ │ └── exporter.go (Prometheus Exporter 로직 구현) +│ └── monit +│ └── monit.go (Monit 상태를 가져오고 파싱) +└── main.go (진입점: cmd.Execute() 호출) +``` -Configuration parameters: +### 라이선스 -Parameter | Description | Default ---- | --- | --- -`listen_address` | address and port to bind | localhost:9388 -`metrics_path` | relative path to expose metrics | /metrics -`ignore_ssl` | whether of not to ignore ssl errors | false -`monit_scrape_uri` | uri to get monit status | http://localhost:2812/_status?format=xml&level=full -`monit_user` | user for monit basic auth, if needed | none -`monit_password` | password for monit status, if needed | none +이 프로젝트는 [MIT License](LICENSE)에 따라 라이선스가 부여됩니다. diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..6a220ec --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,81 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var ( + listenAddress string + metricsPath string + ignoreSSL bool + monitScrapeURI string + monitUser string + monitPassword string + logLevel string +) + +// RootCmd is the base command for this application. +var RootCmd = &cobra.Command{ + Use: "monit_exporter", + Short: "Monit Exporter for Prometheus", + Long: "Prometheus Exporter that collects Monit status information and exposes metrics.", + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, +} + +// Execute runs the root command of the application. +func Execute() { + if err := RootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + RootCmd.PersistentFlags().StringVar( + &listenAddress, + "listen-address", + "localhost:9388", + "The address on which the exporter.go will listen (e.g., '0.0.0.0:9388').", + ) + RootCmd.PersistentFlags().StringVar( + &metricsPath, + "metrics-path", + "/metrics", + "The HTTP path at which metrics are served (e.g., '/metrics').", + ) + RootCmd.PersistentFlags().BoolVar( + &ignoreSSL, + "ignore-ssl", + false, + "Whether to skip SSL certificate verification for Monit endpoints.", + ) + RootCmd.PersistentFlags().StringVar( + &monitScrapeURI, + "monit-scrape-uri", + "http://localhost:2812/_status?format=xml&level=full", + "The Monit status URL to scrape (XML format).", + ) + RootCmd.PersistentFlags().StringVar( + &monitUser, + "monit-user", + "", + "Basic auth username for accessing Monit.", + ) + RootCmd.PersistentFlags().StringVar( + &monitPassword, + "monit-password", + "", + "Basic auth password for accessing Monit.", + ) + RootCmd.PersistentFlags().StringVar( + &logLevel, + "log-level", + "info", + "Log level for the application (debug, info, warn, error, fatal, panic).", + ) +} diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 0000000..2d85870 --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,12 @@ +package cmd + +import ( + "testing" + + "github.com/spf13/cobra" +) + +// TestRootCmd checks if RootCmd is a valid cobra.Command. +func TestRootCmd(t *testing.T) { + var _ *cobra.Command = RootCmd +} diff --git a/cmd/serve.go b/cmd/serve.go new file mode 100644 index 0000000..0303a38 --- /dev/null +++ b/cmd/serve.go @@ -0,0 +1,146 @@ +package cmd + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/ririnto/monit_exporter/internal/config" + "github.com/ririnto/monit_exporter/internal/exporter" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// serveCmd starts the Monit Exporter server. +var serveCmd = &cobra.Command{ + Use: "serve", + Short: "Run the Monit Exporter server", + Long: "Run the Monit Exporter server that collects Monit status and exposes Prometheus metrics.", + RunE: func(cmd *cobra.Command, args []string) error { + // Initialize logger + if err := config.SetLogLevel(logLevel); err != nil { + return fmt.Errorf("failed to set log level: %w", err) + } + + cfg := &config.Config{ + ListenAddress: listenAddress, + MetricsPath: metricsPath, + IgnoreSSL: ignoreSSL, + MonitScrapeURI: monitScrapeURI, + MonitUser: monitUser, + MonitPassword: monitPassword, + LogLevel: logLevel, + } + + exp, err := exporter.NewExporter(cfg) + if err != nil { + return fmt.Errorf("failed to create exporter: %w", err) + } + + // Register the exporter with Prometheus + prometheus.MustRegister(exp) + + mux := http.NewServeMux() + mux.Handle(cfg.MetricsPath, commonLogHandler(promhttp.Handler())) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = fmt.Fprintf( + w, + ` + Monit Exporter + +

Monit Exporter

+

Metrics

+ + `, + cfg.MetricsPath, + ) + }) + + server := &http.Server{ + Addr: cfg.ListenAddress, + Handler: mux, + } + + // Graceful shutdown setup + shutdownCh := make(chan os.Signal, 1) + signal.Notify(shutdownCh, os.Interrupt, syscall.SIGTERM) + go func() { + <-shutdownCh + logrus.Info("Received shutdown signal, stopping Monit Exporter...") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := server.Shutdown(ctx); err != nil { + logrus.Errorf("Failed to gracefully shutdown: %v", err) + } + }() + + logrus.Infof("Starting Monit Exporter on %s", cfg.ListenAddress) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + return fmt.Errorf("failed to start server: %w", err) + } + logrus.Info("Monit Exporter stopped") + return nil + }, +} + +func init() { + RootCmd.AddCommand(serveCmd) +} + +// LoggingResponseWriter wraps a http.ResponseWriter to track status code and size. +type LoggingResponseWriter struct { + http.ResponseWriter + statusCode int + size int +} + +// NewLoggingResponseWriter creates a new LoggingResponseWriter with default status code 200. +func NewLoggingResponseWriter(w http.ResponseWriter) *LoggingResponseWriter { + return &LoggingResponseWriter{ + ResponseWriter: w, + statusCode: http.StatusOK, + } +} + +// WriteHeader sets the status code and calls the underlying ResponseWriter's WriteHeader. +func (lrw *LoggingResponseWriter) WriteHeader(code int) { + lrw.statusCode = code + lrw.ResponseWriter.WriteHeader(code) +} + +// Write writes the data and keeps track of the size in bytes. +func (lrw *LoggingResponseWriter) Write(b []byte) (int, error) { + size, err := lrw.ResponseWriter.Write(b) + lrw.size += size + return size, err +} + +// commonLogHandler returns an http.Handler that logs requests in Common Log Format. +func commonLogHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + lrw := NewLoggingResponseWriter(w) + next.ServeHTTP(lrw, r) + duration := time.Since(start) + logrus.Infof("%s - - [%s] \"%s %s %s\" %d %d \"%s\" \"%s\" %.4f", + r.RemoteAddr, + start.Format("02/Jan/2006:15:04:05 -0700"), + r.Method, + r.RequestURI, + r.Proto, + lrw.statusCode, + lrw.size, + r.Referer(), + r.UserAgent(), + duration.Seconds(), + ) + }) +} diff --git a/cmd/serve_test.go b/cmd/serve_test.go new file mode 100644 index 0000000..4e8db17 --- /dev/null +++ b/cmd/serve_test.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "os" + "syscall" + "testing" + "time" +) + +// TestServeCmdBasic checks if serve command can be invoked without immediate error. +// In reality, you'd test more thoroughly with a mock server, signals, etc. +func TestServeCmdBasic(t *testing.T) { + // Create a temporary command + cmd := serveCmd + go func() { + _ = cmd.RunE(cmd, []string{}) + }() + + // Give some time for server to (potentially) start + time.Sleep(500 * time.Millisecond) + + // Attempt to send a SIGTERM to trigger graceful shutdown + p, _ := os.FindProcess(os.Getpid()) + _ = p.Signal(syscall.SIGTERM) + + time.Sleep(time.Second) // Wait for shutdown +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..be6e043 --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module github.com/ririnto/monit_exporter + +go 1.23.5 + +require ( + github.com/prometheus/client_golang v1.20.5 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.8.1 + golang.org/x/net v0.34.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/protobuf v1.36.3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..71eb303 --- /dev/null +++ b/go.sum @@ -0,0 +1,52 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..d94833f --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,26 @@ +package config + +import ( + "github.com/sirupsen/logrus" +) + +// Config holds the configuration values needed by the Monit Exporter. +type Config struct { + ListenAddress string + MetricsPath string + IgnoreSSL bool + MonitScrapeURI string + MonitUser string + MonitPassword string + LogLevel string +} + +// SetLogLevel sets the global log level of logrus based on the given string. +func SetLogLevel(levelStr string) error { + level, err := logrus.ParseLevel(levelStr) + if err != nil { + return err + } + logrus.SetLevel(level) + return nil +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..9b927f7 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,99 @@ +package config + +import ( + "testing" + + "github.com/sirupsen/logrus" +) + +// TestSetLogLevelValid tests SetLogLevel with valid log level strings. +func TestSetLogLevelValid(t *testing.T) { + testCases := []struct { + levelStr string + expected logrus.Level + }{ + {"debug", logrus.DebugLevel}, + {"info", logrus.InfoLevel}, + {"warn", logrus.WarnLevel}, + {"warning", logrus.WarnLevel}, // Alias for warn + {"error", logrus.ErrorLevel}, + {"fatal", logrus.FatalLevel}, + {"panic", logrus.PanicLevel}, + } + + for _, tc := range testCases { + t.Run(tc.levelStr, func(t *testing.T) { + err := SetLogLevel(tc.levelStr) + if err != nil { + t.Fatalf("SetLogLevel(%q) returned error: %v", tc.levelStr, err) + } + + got := logrus.GetLevel() + if got != tc.expected { + t.Errorf("Expected log level %v, got %v", tc.expected, got) + } + }) + } +} + +// TestSetLogLevelInvalid tests SetLogLevel with an invalid log level string. +func TestSetLogLevelInvalid(t *testing.T) { + invalidLevel := "invalid_level" + + err := SetLogLevel(invalidLevel) + if err == nil { + t.Fatalf("SetLogLevel(%q) expected to return an error, but got nil", invalidLevel) + } + + expectedErrMsg := "not a valid logrus Level" + if !contains(err.Error(), expectedErrMsg) { + t.Errorf("Expected error message to contain %q, but got %q", expectedErrMsg, err.Error()) + } +} + +// contains is a helper function to check if substr is within str. +func contains(str, substr string) bool { + return len(str) >= len(substr) && (str == substr || len(str) > len(substr) && (str[:len(substr)] == substr || contains(str[1:], substr))) +} + +// TestSetLogLevelCaseInsensitive tests that SetLogLevel is case-insensitive. +func TestSetLogLevelCaseInsensitive(t *testing.T) { + testCases := []struct { + levelStr string + expected logrus.Level + }{ + {"DEBUG", logrus.DebugLevel}, + {"Info", logrus.InfoLevel}, + {"WaRn", logrus.WarnLevel}, + {"ErRoR", logrus.ErrorLevel}, + } + + for _, tc := range testCases { + t.Run(tc.levelStr, func(t *testing.T) { + err := SetLogLevel(tc.levelStr) + if err != nil { + t.Fatalf("SetLogLevel(%q) returned error: %v", tc.levelStr, err) + } + + got := logrus.GetLevel() + if got != tc.expected { + t.Errorf("Expected log level %v, got %v", tc.expected, got) + } + }) + } +} + +// TestSetLogLevelEmpty tests SetLogLevel with an empty string. +func TestSetLogLevelEmpty(t *testing.T) { + emptyLevel := "" + + err := SetLogLevel(emptyLevel) + if err == nil { + t.Fatalf("SetLogLevel(empty string) expected to return an error, but got nil") + } + + expectedErrMsg := "not a valid logrus Level" + if !contains(err.Error(), expectedErrMsg) { + t.Errorf("Expected error message to contain %q, but got %q", expectedErrMsg, err.Error()) + } +} diff --git a/internal/exporter/errors.go b/internal/exporter/errors.go new file mode 100644 index 0000000..6cbcaa7 --- /dev/null +++ b/internal/exporter/errors.go @@ -0,0 +1,6 @@ +package exporter + +import "errors" + +// ErrNilConfig is returned when a nil config is provided to NewExporter. +var ErrNilConfig = errors.New("config is nil") diff --git a/internal/exporter/errors_test.go b/internal/exporter/errors_test.go new file mode 100644 index 0000000..cde5250 --- /dev/null +++ b/internal/exporter/errors_test.go @@ -0,0 +1,38 @@ +package exporter + +import ( + "errors" + "testing" +) + +// TestErrNilConfig checks if ErrNilConfig is defined as expected. +func TestErrNilConfig(t *testing.T) { + const wantMsg = "config is nil" + + // 1) Check if the error message matches. + gotMsg := ErrNilConfig.Error() + if gotMsg != wantMsg { + t.Errorf("expected error message: %q, got: %q", wantMsg, gotMsg) + } + + // 2) Check error type reference (optional). + // errors.Is() can be used to verify error chaining or sentinel errors. + if !errors.Is(ErrNilConfig, ErrNilConfig) { + t.Errorf("expected errors.Is to confirm ErrNilConfig itself") + } +} + +// TestErrNilConfigUsage demonstrates a typical usage scenario: checking if +// NewExporter returns ErrNilConfig when passed a nil config. +func TestErrNilConfigUsage(t *testing.T) { + exp, err := NewExporter(nil) + if exp != nil { + t.Fatal("expected Exporter to be nil when config is nil") + } + if err == nil { + t.Fatal("expected an error but got nil") + } + if !errors.Is(err, ErrNilConfig) { + t.Errorf("expected err to be ErrNilConfig, got %v", err) + } +} diff --git a/internal/exporter/exporter.go b/internal/exporter/exporter.go new file mode 100644 index 0000000..a7cd424 --- /dev/null +++ b/internal/exporter/exporter.go @@ -0,0 +1,112 @@ +package exporter + +import ( + "sync" + + "github.com/prometheus/client_golang/prometheus" + "github.com/ririnto/monit_exporter/internal/config" + "github.com/ririnto/monit_exporter/internal/monit" + "github.com/sirupsen/logrus" +) + +const ( + namespace = "monit" +) + +// serviceTypes maps Monit service type integers to descriptive strings. +var serviceTypes = map[int]string{ + 0: "filesystem", + 1: "directory", + 2: "file", + 3: "program_with_pidfile", + 4: "remote_host", + 5: "system", + 6: "fifo", + 7: "program_with_path", + 8: "network", +} + +// Exporter collects Monit metrics and exposes them to Prometheus. +type Exporter struct { + cfg *config.Config + mutex sync.Mutex + up prometheus.Gauge + status *prometheus.GaugeVec +} + +// NewExporter creates a new Exporter using the given Config. +func NewExporter(cfg *config.Config) (*Exporter, error) { + if cfg == nil { + return nil, ErrNilConfig + } + + return &Exporter{ + cfg: cfg, + up: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "exporter_up", + Help: "Indicates whether the Monit endpoint is reachable (1) or not (0).", + }), + status: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "exporter_service_check", + Help: "Monit service check info. The gauge value is the 'status' field from Monit.", + }, + []string{"check_name", "type", "monitored"}, + ), + }, nil +} + +// Describe sends the descriptors of each metric over to the provided channel. +func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { + e.up.Describe(ch) + e.status.Describe(ch) +} + +// Collect is called by the Prometheus registry to gather metrics. +func (e *Exporter) Collect(ch chan<- prometheus.Metric) { + e.mutex.Lock() + defer e.mutex.Unlock() + + e.status.Reset() + + if err := e.scrape(); err != nil { + logrus.Errorf("Error scraping Monit: %v", err) + } + + e.up.Collect(ch) + e.status.Collect(ch) +} + +// scrape fetches and parses the Monit status and updates the metrics. +func (e *Exporter) scrape() error { + data, err := monit.FetchMonitStatus(e.cfg) + if err != nil { + e.up.Set(0) + e.status.Reset() + return err + } + + parsed, err := monit.ParseMonitStatus(data) + if err != nil { + e.up.Set(0) + e.status.Reset() + return err + } + + e.up.Set(1) + for _, svc := range parsed.Services { + typ, ok := serviceTypes[svc.Type] + if !ok { + typ = "unknown" + } + e.status.With(prometheus.Labels{ + "check_name": svc.Name, + "type": typ, + "monitored": svc.Monitored, + }).Set(float64(svc.Status)) + } + + return nil +} diff --git a/internal/exporter/exporter_test.go b/internal/exporter/exporter_test.go new file mode 100644 index 0000000..6946314 --- /dev/null +++ b/internal/exporter/exporter_test.go @@ -0,0 +1,46 @@ +package exporter + +import ( + "errors" + "testing" + + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/ririnto/monit_exporter/internal/config" +) + +// TestNewExporter checks if a new Exporter is created without error. +func TestNewExporter(t *testing.T) { + cfg := &config.Config{} + exp, err := NewExporter(cfg) + if err != nil { + t.Fatalf("failed to create Exporter: %v", err) + } + if exp == nil { + t.Fatal("Exporter is nil") + } +} + +// TestNewExporterNilConfig ensures nil config returns an error. +func TestNewExporterNilConfig(t *testing.T) { + exp, err := NewExporter(nil) + if err == nil { + t.Fatalf("expected error but got nil") + } + if !errors.Is(err, ErrNilConfig) { + t.Errorf("expected ErrNilConfig, got %v", err) + } + if exp != nil { + t.Fatal("expected Exporter to be nil") + } +} + +// TestExporterMetrics checks if the Exporter exposes basic metrics. +func TestExporterMetrics(t *testing.T) { + cfg := &config.Config{} + exp, _ := NewExporter(cfg) + + metricCount := testutil.CollectAndCount(exp) + if metricCount == 0 { + t.Errorf("no metrics collected") + } +} diff --git a/internal/monit/monit.go b/internal/monit/monit.go new file mode 100644 index 0000000..f19c746 --- /dev/null +++ b/internal/monit/monit.go @@ -0,0 +1,116 @@ +package monit + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/xml" + "fmt" + "io" + "net/http" + "time" + + "github.com/ririnto/monit_exporter/internal/config" + "golang.org/x/net/html/charset" +) + +// XML represents the top-level structure of the Monit status XML. +// It includes , , and multiple elements. +type XML struct { + XMLName xml.Name `xml:"monit"` + Server Server `xml:"server"` + Platform Platform `xml:"platform"` + Services []Service `xml:"service"` +} + +// Server contains details from the element in the Monit XML. +type Server struct { + ID string `xml:"id"` + Incarnation int `xml:"incarnation"` + Version string `xml:"version"` + Uptime int `xml:"uptime"` + Poll int `xml:"poll"` + StartDelay int `xml:"startdelay"` + LocalHost string `xml:"localhostname"` + ControlFile string `xml:"controlfile"` + HTTPD HTTPD `xml:"httpd"` +} + +// HTTPD represents the element inside the element. +type HTTPD struct { + Address string `xml:"address"` + Port int `xml:"port"` + SSL int `xml:"ssl"` +} + +// Platform represents the element in the Monit XML. +type Platform struct { + Name string `xml:"name"` + Release string `xml:"release"` + Version string `xml:"version"` + Machine string `xml:"machine"` + CPU int `xml:"cpu"` + Memory int `xml:"memory"` + Swap int `xml:"swap"` +} + +// Service represents information about a single Monit service. +// It is mapped from the element (including the "type" attribute). +type Service struct { + Type int `xml:"type,attr"` + Name string `xml:"name"` + Status int `xml:"status"` + Monitored string `xml:"monitor"` +} + +// FetchMonitStatus sends an HTTP GET request to the Monit endpoint and +// returns the response body. It applies a 5-second timeout context to +// prevent indefinite waiting. +func FetchMonitStatus(cfg *config.Config) ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", cfg.MonitScrapeURI, nil) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + req.SetBasicAuth(cfg.MonitUser, cfg.MonitPassword) + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: cfg.IgnoreSSL}, + } + client := &http.Client{Transport: tr} + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("unable to fetch Monit status: %w", err) + } + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("monit returned non-2xx status code: %d", resp.StatusCode) + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("unable to read Monit status: %w", err) + } + + return data, nil +} + +// ParseMonitStatus parses the Monit XML data into an XML struct, including +// fields for , , and elements. +func ParseMonitStatus(data []byte) (XML, error) { + var statusChunk XML + reader := bytes.NewReader(data) + decoder := xml.NewDecoder(reader) + decoder.CharsetReader = charset.NewReaderLabel + + if err := decoder.Decode(&statusChunk); err != nil { + return XML{}, fmt.Errorf("failed to parse Monit XML: %w", err) + } + return statusChunk, nil +} diff --git a/internal/monit/monit_test.go b/internal/monit/monit_test.go new file mode 100644 index 0000000..dbe573e --- /dev/null +++ b/internal/monit/monit_test.go @@ -0,0 +1,133 @@ +package monit + +import ( + "testing" +) + +// TestParseMonitStatusMinimal checks parsing with a minimal Monit XML snippet. +func TestParseMonitStatusMinimal(t *testing.T) { + xmlData := []byte(` + + + test_service + 0 + 1 + + +`) + parsed, err := ParseMonitStatus(xmlData) + if err != nil { + t.Fatalf("ParseMonitStatus failed: %v", err) + } + if len(parsed.Services) != 1 { + t.Errorf("expected 1 service, got %d", len(parsed.Services)) + } + + svc := parsed.Services[0] + if svc.Type != 5 { + t.Errorf("expected type=5, got %d", svc.Type) + } + if svc.Name != "test_service" { + t.Errorf("expected name=test_service, got %s", svc.Name) + } + if svc.Status != 0 { + t.Errorf("expected status=0, got %d", svc.Status) + } + if svc.Monitored != "1" { + t.Errorf("expected monitor=1, got %s", svc.Monitored) + } +} + +// TestParseMonitStatusFullXML checks parsing with the full Monit XML snippet +// that includes , , and . +func TestParseMonitStatusFullXML(t *testing.T) { + xmlData := []byte(` + + + + acfbb9e9118e68d3754761a79d3aae16 + 1504605214 + 5.23.0 + 136736 + 60 + 0 + fc566edc8b68 + /opt/monit/etc/monitrc + +
172.17.0.2
+ 2812 + 0 +
+
+ + Linux + 4.9.27-moby + #1 SMP Thu May 11 04:01:18 UTC 2017 + x86_64 + 4 + 2046768 + 1048572 + + + fc566edc8b68 + 0 + 1 + +
+`) + + parsed, err := ParseMonitStatus(xmlData) + if err != nil { + t.Fatalf("ParseMonitStatus failed: %v", err) + } + + // Check + if parsed.Server.ID != "acfbb9e9118e68d3754761a79d3aae16" { + t.Errorf("expected Server.ID=acfbb9e9118e68d3754761a79d3aae16, got %s", parsed.Server.ID) + } + if parsed.Server.Uptime != 136736 { + t.Errorf("expected Server.Uptime=136736, got %d", parsed.Server.Uptime) + } + if parsed.Server.HTTPD.Port != 2812 { + t.Errorf("expected Server.HTTPD.Port=2812, got %d", parsed.Server.HTTPD.Port) + } + + // Check + if parsed.Platform.Name != "Linux" { + t.Errorf("expected platform name=Linux, got %s", parsed.Platform.Name) + } + if parsed.Platform.Release != "4.9.27-moby" { + t.Errorf("expected platform release=4.9.27-moby, got %s", parsed.Platform.Release) + } + if parsed.Platform.CPU != 4 { + t.Errorf("expected CPU=4, got %d", parsed.Platform.CPU) + } + + // Check + if len(parsed.Services) != 1 { + t.Errorf("expected 1 service, got %d", len(parsed.Services)) + return + } + svc := parsed.Services[0] + if svc.Type != 5 { + t.Errorf("expected service type=5, got %d", svc.Type) + } + if svc.Name != "fc566edc8b68" { + t.Errorf("expected service name=fc566edc8b68, got %s", svc.Name) + } + if svc.Status != 0 { + t.Errorf("expected status=0, got %d", svc.Status) + } + if svc.Monitored != "1" { + t.Errorf("expected monitored=1, got %s", svc.Monitored) + } +} + +// TestParseMonitStatusInvalidXML checks behavior with invalid XML. +func TestParseMonitStatusInvalidXML(t *testing.T) { + xmlData := []byte(`<<>>`) + _, err := ParseMonitStatus(xmlData) + if err == nil { + t.Errorf("expected parsing error but got nil") + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..c78736a --- /dev/null +++ b/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/ririnto/monit_exporter/cmd" +) + +// main calls the Execute function to start the Cobra-based CLI. +func main() { + cmd.Execute() +} diff --git a/monit_exporter.go b/monit_exporter.go deleted file mode 100644 index 5051341..0000000 --- a/monit_exporter.go +++ /dev/null @@ -1,221 +0,0 @@ -package main - -import ( - "bytes" - "crypto/tls" - "encoding/xml" - "flag" - "io/ioutil" - "net/http" - "sync" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/log" - "github.com/spf13/viper" - "golang.org/x/net/html/charset" -) - -const ( - namespace = "monit" // Prefix for Prometheus metrics. -) - -var configFile = flag.String("conf", "./config.toml", "Configuration file for exporter") - -var serviceTypes = map[int]string{ - 0: "filesystem", - 1: "directory", - 2: "file", - 3: "program with pidfile", - 4: "remote host", - 5: "system", - 6: "fifo", - 7: "program with path", - 8: "network", -} - -type monitXML struct { - MonitServices []monitService `xml:"service"` -} - -// Simplified structure of monit check. -type monitService struct { - Type int `xml:"type,attr"` - Name string `xml:"name"` - Status int `xml:"status"` - Monitored string `xml:"monitor"` -} - -// Exporter collects monit stats from the given URI and exports them using -// the prometheus metrics package. -type Exporter struct { - config *Config - mutex sync.RWMutex - client *http.Client - - up prometheus.Gauge - checkStatus *prometheus.GaugeVec -} - -type Config struct { - listen_address string - metrics_path string - ignore_ssl bool - monit_scrape_uri string - monit_user string - monit_password string -} - -func FetchMonitStatus(c *Config) ([]byte, error) { - client := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: c.ignore_ssl}, - }, - } - - req, err := http.NewRequest("GET", c.monit_scrape_uri, nil) - if err != nil { - log.Errorf("Unable to create request: %v", err) - } - - req.SetBasicAuth(c.monit_user, c.monit_password) - resp, err := client.Do(req) - if err != nil { - log.Error("Unable to fetch monit status") - return nil, err - } - data, err := ioutil.ReadAll(resp.Body) - if err != nil { - log.Fatal("Unable to read monit status") - return nil, err - } - defer resp.Body.Close() - return data, nil -} - -func ParseMonitStatus(data []byte) (monitXML, error) { - var statusChunk monitXML - reader := bytes.NewReader(data) - decoder := xml.NewDecoder(reader) - - // Parsing status results to structure - decoder.CharsetReader = charset.NewReaderLabel - err := decoder.Decode(&statusChunk) - return statusChunk, err -} - -func ParseConfig() *Config { - flag.Parse() - - v := viper.New() - - v.SetDefault("listen_address", "localhost:9388") - v.SetDefault("metrics_path", "/metrics") - v.SetDefault("ignore_ssl", false) - v.SetDefault("monit_scrape_uri", "http://localhost:2812/_status?format=xml&level=full") - v.SetDefault("monit_user", "") - v.SetDefault("monit_password", "") - v.SetConfigFile(*configFile) - v.SetConfigType("toml") - err := v.ReadInConfig() // Find and read the config file - if err != nil { // Handle errors reading the config file - log.Printf("Error reading config file: %s. Using defaults.", err) - } - - return &Config{ - listen_address: v.GetString("listen_address"), - metrics_path: v.GetString("metrics_path"), - ignore_ssl: v.GetBool("ignore_ssl"), - monit_scrape_uri: v.GetString("monit_scrape_uri"), - monit_user: v.GetString("monit_user"), - monit_password: v.GetString("monit_password"), - } -} - -// Returns an initialized Exporter. -func NewExporter(c *Config) (*Exporter, error) { - - return &Exporter{ - config: c, - up: prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: namespace, - Name: "exporter_up", - Help: "Monit status availability", - }), - checkStatus: prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: namespace, - Name: "exporter_service_check", - Help: "Monit service check info", - }, - []string{"check_name", "type", "monitored"}, - ), - }, nil -} - -// Describe describes all the metrics ever exported by the monit exporter. It -// implements prometheus.Collector. -func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { - e.up.Describe(ch) - e.checkStatus.Describe(ch) -} - -func (e *Exporter) scrape() error { - data, err := FetchMonitStatus(e.config) - if err != nil { - // set "monit_exporter_up" gauge to 0, remove previous metrics from e.checkStatus vector - e.up.Set(0) - e.checkStatus.Reset() - log.Errorf("Error getting monit status: %v", err) - return err - } else { - parsedData, err := ParseMonitStatus(data) - if err != nil { - e.up.Set(0) - e.checkStatus.Reset() - log.Errorf("Error parsing data from monit: %v", err) - } else { - e.up.Set(1) - // Constructing metrics - for _, service := range parsedData.MonitServices { - e.checkStatus.With(prometheus.Labels{"check_name": service.Name, "type": serviceTypes[service.Type], "monitored": service.Monitored}).Set(float64(service.Status)) - } - } - return err - } -} - -// Collect fetches the stats from configured monit location and delivers them -// as Prometheus metrics. It implements prometheus.Collector. -func (e *Exporter) Collect(ch chan<- prometheus.Metric) { - e.mutex.Lock() // Protect metrics from concurrent collects. - defer e.mutex.Unlock() - e.checkStatus.Reset() - e.scrape() - e.up.Collect(ch) - e.checkStatus.Collect(ch) - return -} - -func main() { - - config := ParseConfig() - exporter, err := NewExporter(config) - - if err != nil { - log.Fatal(err) - } - prometheus.MustRegister(exporter) - - log.Printf("Starting monit_exporter: %s", config.listen_address) - http.Handle(config.metrics_path, prometheus.Handler()) - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(` - Monit Exporter - -

Monit Exporter

-

Metrics

- - `)) - }) - - log.Fatal(http.ListenAndServe(config.listen_address, nil)) -} diff --git a/monit_exporter_test.go b/monit_exporter_test.go deleted file mode 100644 index 084871a..0000000 --- a/monit_exporter_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package main - -import ( - "net/http" - "net/http/httptest" - "testing" - - "fmt" - "io/ioutil" - "time" -) - -const ( - monitStatus = `acfbb9e9118e68d3754761a79d3aae1615046052145.23.0136736600fc566edc8b68/opt/monit/etc/monitrc
172.17.0.2
28120
Linux4.9.27-moby#1 SMP Thu May 11 04:01:18 UTC 2017x86_64420467681048572fc566edc8b681505209672232150010000.000.000.000.10.10.16.51336280.00
` - monitServiceName = `fc566edc8b68` - monitUser = `user` - monitPassword = `password` -) - -func TestMonitStatus(t *testing.T) { - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(monitStatus)) - }) - server := httptest.NewServer(handler) - config := ParseConfig() - config.monit_scrape_uri = server.URL - e, err := NewExporter(config) - if err != nil { - t.Error("Unexpected error during exporter creation") - } - err = e.scrape() - if err != nil { - t.Error("Unexpected execution error:", err) - } -} - -func TestFieldsParsing(t *testing.T) { - parsedData, err := ParseMonitStatus([]byte(monitStatus)) - if err != nil { - t.Error("Unable to parse XML:", err) - } - if parsedData.MonitServices[0].Name != monitServiceName { - t.Errorf("want Name %d, have %d.", monitServiceName, parsedData.MonitServices[0].Name) - } -} - -func TestMonitUnavailable(t *testing.T) { - mConfig := &Config{ - monit_scrape_uri: "http://localhost:1/status", - } - e, err := NewExporter(mConfig) - if err != nil { - t.Error("Unexpected error during exporter creation") - } - err = e.scrape() - if err == nil { - t.Error("Unexpected succsessful execution") - } -} - -func TestHttpQueryExporter(t *testing.T) { - go main() - time.Sleep(50 * time.Millisecond) - address := "127.0.0.1:9388" - resp, err := http.Get(fmt.Sprintf("http://%s/metrics", address)) - if err != nil { - t.Fatal(err) - } - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Error(err) - } - if err := resp.Body.Close(); err != nil { - t.Error(err) - } - if want, have := http.StatusOK, resp.StatusCode; want != have { - t.Errorf("want /metrics status code %d, have %d. Body:\n%s", want, have, b) - } -} - -func AuthHandler(w http.ResponseWriter, r *http.Request) { - user, pass, _ := r.BasicAuth() - if user == monitUser && pass == monitPassword { - w.Write([]byte(monitStatus)) - - } else { - http.Error(w, "Unauthorized.", 401) - } -} - -func TestBasicAuth(t *testing.T) { - handler := http.HandlerFunc(AuthHandler) - server := httptest.NewServer(handler) - config := ParseConfig() - config.monit_scrape_uri = server.URL - config.monit_user = monitUser - config.monit_password = monitPassword - e, err := NewExporter(config) - if err != nil { - t.Error("Unexpected error during exporter creation") - } - err = e.scrape() - if err != nil { - t.Error("Unexpected execution error:", err) - } -} - -func TestBasicAuthFail(t *testing.T) { - handler := http.HandlerFunc(AuthHandler) - server := httptest.NewServer(handler) - config := ParseConfig() - config.monit_scrape_uri = server.URL - config.monit_user = monitUser - config.monit_password = monitPassword + "qwe" - e, err := NewExporter(config) - if err != nil { - t.Error("Unexpected error during exporter creation") - } - err = e.scrape() - if err == nil { - t.Error("Unexpected execution success:") - } -} From cad60cab18a1ab26e77a589f85214a7d66040f1c Mon Sep 17 00:00:00 2001 From: ririnto Date: Sat, 18 Jan 2025 03:20:14 +0900 Subject: [PATCH 02/11] change service types --- internal/exporter/exporter.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/exporter/exporter.go b/internal/exporter/exporter.go index a7cd424..c409bca 100644 --- a/internal/exporter/exporter.go +++ b/internal/exporter/exporter.go @@ -15,15 +15,15 @@ const ( // serviceTypes maps Monit service type integers to descriptive strings. var serviceTypes = map[int]string{ - 0: "filesystem", - 1: "directory", - 2: "file", - 3: "program_with_pidfile", - 4: "remote_host", - 5: "system", - 6: "fifo", - 7: "program_with_path", - 8: "network", + 0: "Filesystem", + 1: "Directory", + 2: "File", + 3: "Process", + 4: "Remote host", + 5: "System", + 6: "Fifo", + 7: "Program", + 8: "Network", } // Exporter collects Monit metrics and exposes them to Prometheus. From 9671676253621f6a288284c2812c338b6c4b5742 Mon Sep 17 00:00:00 2001 From: ririnto Date: Sat, 18 Jan 2025 03:42:53 +0900 Subject: [PATCH 03/11] add logs, update tests --- cmd/root.go | 7 +- cmd/root_test.go | 33 ++- cmd/serve.go | 28 ++- cmd/serve_test.go | 69 +++++-- internal/config/config.go | 3 + internal/config/config_test.go | 93 +-------- internal/exporter/errors.go | 6 - internal/exporter/errors_test.go | 38 ---- internal/exporter/exporter.go | 318 ++++++++++++++++++++++++++++- internal/exporter/exporter_test.go | 114 +++++++++-- internal/monit/monit.go | 205 +++++++++++++++---- internal/monit/monit_test.go | 158 +++++--------- main.go | 5 +- 13 files changed, 740 insertions(+), 337 deletions(-) delete mode 100644 internal/exporter/errors.go delete mode 100644 internal/exporter/errors_test.go diff --git a/cmd/root.go b/cmd/root.go index 6a220ec..fe1f110 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -23,16 +24,20 @@ var RootCmd = &cobra.Command{ Short: "Monit Exporter for Prometheus", Long: "Prometheus Exporter that collects Monit status information and exposes metrics.", RunE: func(cmd *cobra.Command, args []string) error { + logrus.Debug("RootCmd is running without subcommand, displaying help message") return cmd.Help() }, } // Execute runs the root command of the application. func Execute() { + logrus.Debug("Execute function called: attempting to run RootCmd.Execute()") if err := RootCmd.Execute(); err != nil { + logrus.Errorf("Error occurred while executing RootCmd: %v", err) fmt.Println(err) os.Exit(1) } + logrus.Debug("Execute function finished: RootCmd.Execute() completed successfully") } func init() { @@ -40,7 +45,7 @@ func init() { &listenAddress, "listen-address", "localhost:9388", - "The address on which the exporter.go will listen (e.g., '0.0.0.0:9388').", + "The address on which the exporter will listen (e.g., '0.0.0.0:9388').", ) RootCmd.PersistentFlags().StringVar( &metricsPath, diff --git a/cmd/root_test.go b/cmd/root_test.go index 2d85870..b82e764 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -1,12 +1,39 @@ package cmd import ( + "bytes" "testing" "github.com/spf13/cobra" ) -// TestRootCmd checks if RootCmd is a valid cobra.Command. -func TestRootCmd(t *testing.T) { - var _ *cobra.Command = RootCmd +// TestRootCmd_Help verifies that running RootCmd without subcommands prints help. +func TestRootCmd_Help(t *testing.T) { + buf := new(bytes.Buffer) + RootCmd.SetOut(buf) + RootCmd.SetArgs([]string{}) + + err := RootCmd.Execute() + if err != nil { + t.Fatalf("RootCmd execution failed: %v", err) + } + + output := buf.String() + if len(output) == 0 { + t.Errorf("Expected help output, got empty string") + } +} + +// TestRootCmd_Execute checks if Execute() runs RootCmd properly. +func TestRootCmd_Execute(t *testing.T) { + testCmd := &cobra.Command{ + Use: "test", + Short: "Test subcommand", + Run: func(cmd *cobra.Command, args []string) {}, + } + RootCmd.AddCommand(testCmd) + defer RootCmd.RemoveCommand(testCmd) + + RootCmd.SetArgs([]string{"test"}) + Execute() } diff --git a/cmd/serve.go b/cmd/serve.go index 0303a38..3022c14 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "errors" "fmt" "net/http" "os" @@ -23,10 +24,13 @@ var serveCmd = &cobra.Command{ Short: "Run the Monit Exporter server", Long: "Run the Monit Exporter server that collects Monit status and exposes Prometheus metrics.", RunE: func(cmd *cobra.Command, args []string) error { - // Initialize logger + logrus.Debug("serveCmd invoked: starting Monit Exporter server") + if err := config.SetLogLevel(logLevel); err != nil { + logrus.Errorf("Failed to set log level: %v", err) return fmt.Errorf("failed to set log level: %w", err) } + logrus.Debugf("Log level set to '%s'", logLevel) cfg := &config.Config{ ListenAddress: listenAddress, @@ -37,18 +41,20 @@ var serveCmd = &cobra.Command{ MonitPassword: monitPassword, LogLevel: logLevel, } + logrus.Debugf("Server configuration loaded: %+v", cfg) exp, err := exporter.NewExporter(cfg) if err != nil { + logrus.Errorf("Failed to create exporter: %v", err) return fmt.Errorf("failed to create exporter: %w", err) } - - // Register the exporter with Prometheus + logrus.Debug("Registering exporter to Prometheus") prometheus.MustRegister(exp) mux := http.NewServeMux() mux.Handle(cfg.MetricsPath, commonLogHandler(promhttp.Handler())) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + logrus.Debugf("Root path request received from %s", r.RemoteAddr) w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = fmt.Fprintf( w, @@ -68,22 +74,23 @@ var serveCmd = &cobra.Command{ Handler: mux, } - // Graceful shutdown setup shutdownCh := make(chan os.Signal, 1) signal.Notify(shutdownCh, os.Interrupt, syscall.SIGTERM) go func() { - <-shutdownCh - logrus.Info("Received shutdown signal, stopping Monit Exporter...") - + sig := <-shutdownCh + logrus.Infof("Received shutdown signal: %v. Attempting to stop Monit Exporter gracefully...", sig) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { - logrus.Errorf("Failed to gracefully shutdown: %v", err) + logrus.Errorf("Graceful shutdown failed: %v", err) + } else { + logrus.Info("Server shut down gracefully") } }() logrus.Infof("Starting Monit Exporter on %s", cfg.ListenAddress) - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + logrus.Errorf("Failed to start server: %v", err) return fmt.Errorf("failed to start server: %w", err) } logrus.Info("Monit Exporter stopped") @@ -130,7 +137,7 @@ func commonLogHandler(next http.Handler) http.Handler { lrw := NewLoggingResponseWriter(w) next.ServeHTTP(lrw, r) duration := time.Since(start) - logrus.Infof("%s - - [%s] \"%s %s %s\" %d %d \"%s\" \"%s\" %.4f", + logrus.Infof("[commonLogHandler] %s - - [%s] \"%s %s %s\" %d %d \"%s\" \"%s\" %.4f", r.RemoteAddr, start.Format("02/Jan/2006:15:04:05 -0700"), r.Method, @@ -142,5 +149,6 @@ func commonLogHandler(next http.Handler) http.Handler { r.UserAgent(), duration.Seconds(), ) + logrus.Debugf("[commonLogHandler] Request processed: Method=%s, URI=%s, Duration=%.4fs", r.Method, r.RequestURI, duration.Seconds()) }) } diff --git a/cmd/serve_test.go b/cmd/serve_test.go index 4e8db17..e28f7f2 100644 --- a/cmd/serve_test.go +++ b/cmd/serve_test.go @@ -1,27 +1,62 @@ package cmd import ( - "os" - "syscall" + "bytes" + "net/http" + "net/http/httptest" "testing" - "time" ) -// TestServeCmdBasic checks if serve command can be invoked without immediate error. -// In reality, you'd test more thoroughly with a mock server, signals, etc. -func TestServeCmdBasic(t *testing.T) { - // Create a temporary command - cmd := serveCmd - go func() { - _ = cmd.RunE(cmd, []string{}) - }() +func TestServeCmd_Help(t *testing.T) { + buf := new(bytes.Buffer) + RootCmd.SetOut(buf) + RootCmd.SetArgs([]string{"serve", "--help"}) - // Give some time for server to (potentially) start - time.Sleep(500 * time.Millisecond) + err := serveCmd.Execute() + if err != nil { + t.Fatalf("ServeCmd execution failed with --help: %v", err) + } - // Attempt to send a SIGTERM to trigger graceful shutdown - p, _ := os.FindProcess(os.Getpid()) - _ = p.Signal(syscall.SIGTERM) + output := buf.String() + if len(output) == 0 { + t.Errorf("Expected help output for serve command, got empty string") + } +} + +func TestCommonLogHandler(t *testing.T) { + dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("OK")) + }) + handler := commonLogHandler(dummyHandler) + + req := httptest.NewRequest("GET", "/dummy", nil) + w := httptest.NewRecorder() + + handler.ServeHTTP(w, req) + if w.Result().StatusCode != http.StatusOK { + t.Errorf("Expected status 200, got %d", w.Result().StatusCode) + } +} + +func TestNewLoggingResponseWriter(t *testing.T) { + rec := httptest.NewRecorder() + lrw := NewLoggingResponseWriter(rec) + lrw.WriteHeader(http.StatusAccepted) + + if lrw.statusCode != http.StatusAccepted { + t.Errorf("Expected status code 202, got %d", lrw.statusCode) + } - time.Sleep(time.Second) // Wait for shutdown + testData := []byte("Hello, World!") + n, err := lrw.Write(testData) + if err != nil { + t.Fatalf("Write failed: %v", err) + } + if n != len(testData) { + t.Errorf("Expected %d bytes, wrote %d", len(testData), n) + } + if lrw.size != len(testData) { + t.Errorf("Expected lrw.size=%d, got %d", len(testData), lrw.size) + } } diff --git a/internal/config/config.go b/internal/config/config.go index d94833f..836e764 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -17,10 +17,13 @@ type Config struct { // SetLogLevel sets the global log level of logrus based on the given string. func SetLogLevel(levelStr string) error { + logrus.Debugf("SetLogLevel called with levelStr=%s", levelStr) level, err := logrus.ParseLevel(levelStr) if err != nil { + logrus.Errorf("Failed to parse log level: %v", err) return err } logrus.SetLevel(level) + logrus.Infof("Log level successfully set to '%s'", level.String()) return nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 9b927f7..183d27d 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -6,94 +6,19 @@ import ( "github.com/sirupsen/logrus" ) -// TestSetLogLevelValid tests SetLogLevel with valid log level strings. -func TestSetLogLevelValid(t *testing.T) { - testCases := []struct { - levelStr string - expected logrus.Level - }{ - {"debug", logrus.DebugLevel}, - {"info", logrus.InfoLevel}, - {"warn", logrus.WarnLevel}, - {"warning", logrus.WarnLevel}, // Alias for warn - {"error", logrus.ErrorLevel}, - {"fatal", logrus.FatalLevel}, - {"panic", logrus.PanicLevel}, +func TestSetLogLevel_Success(t *testing.T) { + err := SetLogLevel("debug") + if err != nil { + t.Fatalf("Expected no error for valid level 'debug', got %v", err) } - - for _, tc := range testCases { - t.Run(tc.levelStr, func(t *testing.T) { - err := SetLogLevel(tc.levelStr) - if err != nil { - t.Fatalf("SetLogLevel(%q) returned error: %v", tc.levelStr, err) - } - - got := logrus.GetLevel() - if got != tc.expected { - t.Errorf("Expected log level %v, got %v", tc.expected, got) - } - }) - } -} - -// TestSetLogLevelInvalid tests SetLogLevel with an invalid log level string. -func TestSetLogLevelInvalid(t *testing.T) { - invalidLevel := "invalid_level" - - err := SetLogLevel(invalidLevel) - if err == nil { - t.Fatalf("SetLogLevel(%q) expected to return an error, but got nil", invalidLevel) - } - - expectedErrMsg := "not a valid logrus Level" - if !contains(err.Error(), expectedErrMsg) { - t.Errorf("Expected error message to contain %q, but got %q", expectedErrMsg, err.Error()) - } -} - -// contains is a helper function to check if substr is within str. -func contains(str, substr string) bool { - return len(str) >= len(substr) && (str == substr || len(str) > len(substr) && (str[:len(substr)] == substr || contains(str[1:], substr))) -} - -// TestSetLogLevelCaseInsensitive tests that SetLogLevel is case-insensitive. -func TestSetLogLevelCaseInsensitive(t *testing.T) { - testCases := []struct { - levelStr string - expected logrus.Level - }{ - {"DEBUG", logrus.DebugLevel}, - {"Info", logrus.InfoLevel}, - {"WaRn", logrus.WarnLevel}, - {"ErRoR", logrus.ErrorLevel}, - } - - for _, tc := range testCases { - t.Run(tc.levelStr, func(t *testing.T) { - err := SetLogLevel(tc.levelStr) - if err != nil { - t.Fatalf("SetLogLevel(%q) returned error: %v", tc.levelStr, err) - } - - got := logrus.GetLevel() - if got != tc.expected { - t.Errorf("Expected log level %v, got %v", tc.expected, got) - } - }) + if logrus.GetLevel() != logrus.DebugLevel { + t.Errorf("Expected log level=DebugLevel, got %s", logrus.GetLevel()) } } -// TestSetLogLevelEmpty tests SetLogLevel with an empty string. -func TestSetLogLevelEmpty(t *testing.T) { - emptyLevel := "" - - err := SetLogLevel(emptyLevel) +func TestSetLogLevel_Invalid(t *testing.T) { + err := SetLogLevel("notalevel") if err == nil { - t.Fatalf("SetLogLevel(empty string) expected to return an error, but got nil") - } - - expectedErrMsg := "not a valid logrus Level" - if !contains(err.Error(), expectedErrMsg) { - t.Errorf("Expected error message to contain %q, but got %q", expectedErrMsg, err.Error()) + t.Fatal("Expected an error for invalid log level 'notalevel', got nil") } } diff --git a/internal/exporter/errors.go b/internal/exporter/errors.go deleted file mode 100644 index 6cbcaa7..0000000 --- a/internal/exporter/errors.go +++ /dev/null @@ -1,6 +0,0 @@ -package exporter - -import "errors" - -// ErrNilConfig is returned when a nil config is provided to NewExporter. -var ErrNilConfig = errors.New("config is nil") diff --git a/internal/exporter/errors_test.go b/internal/exporter/errors_test.go deleted file mode 100644 index cde5250..0000000 --- a/internal/exporter/errors_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package exporter - -import ( - "errors" - "testing" -) - -// TestErrNilConfig checks if ErrNilConfig is defined as expected. -func TestErrNilConfig(t *testing.T) { - const wantMsg = "config is nil" - - // 1) Check if the error message matches. - gotMsg := ErrNilConfig.Error() - if gotMsg != wantMsg { - t.Errorf("expected error message: %q, got: %q", wantMsg, gotMsg) - } - - // 2) Check error type reference (optional). - // errors.Is() can be used to verify error chaining or sentinel errors. - if !errors.Is(ErrNilConfig, ErrNilConfig) { - t.Errorf("expected errors.Is to confirm ErrNilConfig itself") - } -} - -// TestErrNilConfigUsage demonstrates a typical usage scenario: checking if -// NewExporter returns ErrNilConfig when passed a nil config. -func TestErrNilConfigUsage(t *testing.T) { - exp, err := NewExporter(nil) - if exp != nil { - t.Fatal("expected Exporter to be nil when config is nil") - } - if err == nil { - t.Fatal("expected an error but got nil") - } - if !errors.Is(err, ErrNilConfig) { - t.Errorf("expected err to be ErrNilConfig, got %v", err) - } -} diff --git a/internal/exporter/exporter.go b/internal/exporter/exporter.go index c409bca..5d88bb6 100644 --- a/internal/exporter/exporter.go +++ b/internal/exporter/exporter.go @@ -1,6 +1,8 @@ package exporter import ( + "errors" + "strconv" "sync" "github.com/prometheus/client_golang/prometheus" @@ -13,6 +15,11 @@ const ( namespace = "monit" ) +var ( + // ErrNilConfig is returned when a nil config is provided to NewExporter. + ErrNilConfig = errors.New("config is nil") +) + // serviceTypes maps Monit service type integers to descriptive strings. var serviceTypes = map[int]string{ 0: "Filesystem", @@ -28,20 +35,51 @@ var serviceTypes = map[int]string{ // Exporter collects Monit metrics and exposes them to Prometheus. type Exporter struct { - cfg *config.Config - mutex sync.Mutex + cfg *config.Config + mutex sync.Mutex + up prometheus.Gauge status *prometheus.GaugeVec + + blockUsage *prometheus.GaugeVec + blockTotal *prometheus.GaugeVec + blockPercent *prometheus.GaugeVec + + inodeUsage *prometheus.GaugeVec + inodeTotal *prometheus.GaugeVec + inodePercent *prometheus.GaugeVec + + portResponseTime *prometheus.GaugeVec + + systemLoadAvg01 *prometheus.GaugeVec + systemLoadAvg05 *prometheus.GaugeVec + systemLoadAvg15 *prometheus.GaugeVec + + systemCPUUser *prometheus.GaugeVec + systemCPUSystem *prometheus.GaugeVec + systemCPUWait *prometheus.GaugeVec + + systemMemPercent *prometheus.GaugeVec + systemMemKilobytes *prometheus.GaugeVec + systemSwapPercent *prometheus.GaugeVec + systemSwapKilobytes *prometheus.GaugeVec } // NewExporter creates a new Exporter using the given Config. func NewExporter(cfg *config.Config) (*Exporter, error) { if cfg == nil { + logrus.Error("NewExporter: config is nil") return nil, ErrNilConfig } + logrus.Debugf("NewExporter: creating exporter with ListenAddress=%s, MonitScrapeURI=%s", + cfg.ListenAddress, cfg.MonitScrapeURI) + + labelNames := []string{"check_name", "type", "monitored"} + return &Exporter{ cfg: cfg, + up: prometheus.NewGauge(prometheus.GaugeOpts{ Namespace: namespace, Name: "exporter_up", @@ -51,17 +89,184 @@ func NewExporter(cfg *config.Config) (*Exporter, error) { prometheus.GaugeOpts{ Namespace: namespace, Name: "exporter_service_check", - Help: "Monit service check info. The gauge value is the 'status' field from Monit.", + Help: "Indicates the status field from Monit.", + }, + labelNames, + ), + + blockUsage: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "service_block_usage_bytes", + Help: "Block usage for filesystem-based services.", + }, + labelNames, + ), + blockTotal: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "service_block_total_bytes", + Help: "Block total capacity for filesystem-based services.", + }, + labelNames, + ), + blockPercent: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "service_block_usage_percent", + Help: "Block usage percentage for filesystem-based services.", + }, + labelNames, + ), + + inodeUsage: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "service_inode_usage", + Help: "Inode usage for filesystem-based services.", + }, + labelNames, + ), + inodeTotal: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "service_inode_total", + Help: "Total number of inodes for filesystem-based services.", + }, + labelNames, + ), + inodePercent: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "service_inode_usage_percent", + Help: "Inode usage percentage for filesystem-based services.", + }, + labelNames, + ), + + portResponseTime: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "service_port_response_seconds", + Help: "Response time in seconds for port-based checks.", + }, + labelNames, + ), + + systemLoadAvg01: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "service_system_loadavg_01", + Help: "1-minute load average for system-based services.", + }, + labelNames, + ), + systemLoadAvg05: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "service_system_loadavg_05", + Help: "5-minute load average for system-based services.", + }, + labelNames, + ), + systemLoadAvg15: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "service_system_loadavg_15", + Help: "15-minute load average for system-based services.", + }, + labelNames, + ), + + systemCPUUser: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "service_system_cpu_user_percent", + Help: "CPU usage in user space (percent).", + }, + labelNames, + ), + systemCPUSystem: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "service_system_cpu_system_percent", + Help: "CPU usage in kernel space (percent).", + }, + labelNames, + ), + systemCPUWait: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "service_system_cpu_wait_percent", + Help: "CPU usage waiting for I/O (percent).", + }, + labelNames, + ), + + systemMemPercent: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "service_system_memory_usage_percent", + Help: "Memory usage percentage for system-based services.", + }, + labelNames, + ), + systemMemKilobytes: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "service_system_memory_usage_kilobytes", + Help: "Memory usage in kilobytes for system-based services.", + }, + labelNames, + ), + systemSwapPercent: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "service_system_swap_usage_percent", + Help: "Swap usage percentage for system-based services.", + }, + labelNames, + ), + systemSwapKilobytes: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "service_system_swap_usage_kilobytes", + Help: "Swap usage in kilobytes for system-based services.", }, - []string{"check_name", "type", "monitored"}, + labelNames, ), }, nil } -// Describe sends the descriptors of each metric over to the provided channel. +// Describe sends the descriptors of each metric to the provided channel. func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { e.up.Describe(ch) e.status.Describe(ch) + + e.blockUsage.Describe(ch) + e.blockTotal.Describe(ch) + e.blockPercent.Describe(ch) + + e.inodeUsage.Describe(ch) + e.inodeTotal.Describe(ch) + e.inodePercent.Describe(ch) + + e.portResponseTime.Describe(ch) + + e.systemLoadAvg01.Describe(ch) + e.systemLoadAvg05.Describe(ch) + e.systemLoadAvg15.Describe(ch) + + e.systemCPUUser.Describe(ch) + e.systemCPUSystem.Describe(ch) + e.systemCPUWait.Describe(ch) + + e.systemMemPercent.Describe(ch) + e.systemMemKilobytes.Describe(ch) + e.systemSwapPercent.Describe(ch) + e.systemSwapKilobytes.Describe(ch) + + logrus.Debug("Exporter.Describe: described all metrics to the channel") } // Collect is called by the Prometheus registry to gather metrics. @@ -69,44 +274,137 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) { e.mutex.Lock() defer e.mutex.Unlock() + logrus.Debug("Exporter.Collect: resetting metrics before scrape") + e.status.Reset() + e.blockUsage.Reset() + e.blockTotal.Reset() + e.blockPercent.Reset() + e.inodeUsage.Reset() + e.inodeTotal.Reset() + e.inodePercent.Reset() + e.portResponseTime.Reset() + e.systemLoadAvg01.Reset() + e.systemLoadAvg05.Reset() + e.systemLoadAvg15.Reset() + e.systemCPUUser.Reset() + e.systemCPUSystem.Reset() + e.systemCPUWait.Reset() + e.systemMemPercent.Reset() + e.systemMemKilobytes.Reset() + e.systemSwapPercent.Reset() + e.systemSwapKilobytes.Reset() - if err := e.scrape(); err != nil { - logrus.Errorf("Error scraping Monit: %v", err) + err := e.scrape() + if err != nil { + logrus.Errorf("Exporter.Collect: scrape error: %v", err) } e.up.Collect(ch) e.status.Collect(ch) + e.blockUsage.Collect(ch) + e.blockTotal.Collect(ch) + e.blockPercent.Collect(ch) + e.inodeUsage.Collect(ch) + e.inodeTotal.Collect(ch) + e.inodePercent.Collect(ch) + e.portResponseTime.Collect(ch) + e.systemLoadAvg01.Collect(ch) + e.systemLoadAvg05.Collect(ch) + e.systemLoadAvg15.Collect(ch) + e.systemCPUUser.Collect(ch) + e.systemCPUSystem.Collect(ch) + e.systemCPUWait.Collect(ch) + e.systemMemPercent.Collect(ch) + e.systemMemKilobytes.Collect(ch) + e.systemSwapPercent.Collect(ch) + e.systemSwapKilobytes.Collect(ch) + + logrus.Debug("Exporter.Collect: metrics collected and sent to the channel") } -// scrape fetches and parses the Monit status and updates the metrics. +// scrape fetches Monit status and updates the metrics. func (e *Exporter) scrape() error { + logrus.Debug("Exporter.scrape: fetching Monit status") data, err := monit.FetchMonitStatus(e.cfg) if err != nil { + logrus.Warnf("Exporter.scrape: failed to fetch Monit status: %v", err) e.up.Set(0) e.status.Reset() return err } + logrus.Debugf("Exporter.scrape: successfully fetched Monit status (%d bytes)", len(data)) parsed, err := monit.ParseMonitStatus(data) if err != nil { + logrus.Warnf("Exporter.scrape: failed to parse Monit status: %v", err) e.up.Set(0) e.status.Reset() return err } + logrus.Debug("Exporter.scrape: successfully parsed Monit status") e.up.Set(1) + logrus.Debug("Exporter.scrape: set exporter_up to 1 (Monit is reachable)") + for _, svc := range parsed.Services { typ, ok := serviceTypes[svc.Type] if !ok { typ = "unknown" + logrus.Warnf("Exporter.scrape: unknown service type=%d, name=%s", svc.Type, svc.Name) } + monitored := strconv.Itoa(svc.Monitor) + e.status.With(prometheus.Labels{ "check_name": svc.Name, "type": typ, - "monitored": svc.Monitored, + "monitored": monitored, }).Set(float64(svc.Status)) - } + logrus.Debugf("Exporter.scrape: service=%s, type=%s, monitor=%d, status=%d", + svc.Name, typ, svc.Monitor, svc.Status) + + e.collectServiceMetrics(svc, typ, monitored) + } return nil } + +// collectServiceMetrics updates detailed metrics for a single Monit service. +func (e *Exporter) collectServiceMetrics(svc monit.Service, typeStr, monitored string) { + labels := prometheus.Labels{ + "check_name": svc.Name, + "type": typeStr, + "monitored": monitored, + } + + if svc.Block != nil { + e.blockUsage.With(labels).Set(svc.Block.Usage) + e.blockTotal.With(labels).Set(svc.Block.Total) + e.blockPercent.With(labels).Set(svc.Block.Percent) + } + + if svc.Inode != nil { + e.inodeUsage.With(labels).Set(float64(svc.Inode.Usage)) + e.inodeTotal.With(labels).Set(float64(svc.Inode.Total)) + e.inodePercent.With(labels).Set(svc.Inode.Percent) + } + + if svc.Port != nil { + e.portResponseTime.With(labels).Set(svc.Port.Responsetime) + } + + if svc.System != nil { + e.systemLoadAvg01.With(labels).Set(svc.System.Load.Avg01) + e.systemLoadAvg05.With(labels).Set(svc.System.Load.Avg05) + e.systemLoadAvg15.With(labels).Set(svc.System.Load.Avg15) + + e.systemCPUUser.With(labels).Set(svc.System.CPU.User) + e.systemCPUSystem.With(labels).Set(svc.System.CPU.System) + e.systemCPUWait.With(labels).Set(svc.System.CPU.Wait) + + e.systemMemPercent.With(labels).Set(svc.System.Memory.Percent) + e.systemMemKilobytes.With(labels).Set(float64(svc.System.Memory.Kilobyte)) + e.systemSwapPercent.With(labels).Set(svc.System.Swap.Percent) + e.systemSwapKilobytes.With(labels).Set(float64(svc.System.Swap.Kilobyte)) + } +} diff --git a/internal/exporter/exporter_test.go b/internal/exporter/exporter_test.go index 6946314..941f034 100644 --- a/internal/exporter/exporter_test.go +++ b/internal/exporter/exporter_test.go @@ -1,46 +1,116 @@ package exporter import ( - "errors" + "fmt" + "github.com/prometheus/client_golang/prometheus" + "net/http" + "net/http/httptest" "testing" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/ririnto/monit_exporter/internal/config" + "github.com/sirupsen/logrus" ) -// TestNewExporter checks if a new Exporter is created without error. -func TestNewExporter(t *testing.T) { - cfg := &config.Config{} +// TestNewExporter_NilConfig verifies that providing a nil config returns an error. +func TestNewExporter_NilConfig(t *testing.T) { + t.Log("Testing NewExporter with nil config") + exp, err := NewExporter(nil) + if err == nil { + t.Errorf("Expected ErrNilConfig, got no error") + } + if exp != nil { + t.Errorf("Expected nil Exporter, got a non-nil instance") + } +} + +// TestNewExporter_ValidConfig verifies that a valid config creates an Exporter successfully. +func TestNewExporter_ValidConfig(t *testing.T) { + t.Log("Testing NewExporter with a valid config") + cfg := &config.Config{ListenAddress: "0.0.0.0:9999"} exp, err := NewExporter(cfg) if err != nil { - t.Fatalf("failed to create Exporter: %v", err) + t.Fatalf("Expected no error, got %v", err) } if exp == nil { - t.Fatal("Exporter is nil") + t.Fatal("Expected a non-nil Exporter, got nil") } } -// TestNewExporterNilConfig ensures nil config returns an error. -func TestNewExporterNilConfig(t *testing.T) { - exp, err := NewExporter(nil) - if err == nil { - t.Fatalf("expected error but got nil") +// TestExporter_Collect_Success uses a mock server returning a valid Monit XML response. +func TestExporter_Collect_Success(t *testing.T) { + t.Log("Testing Exporter.Collect with a successful Monit response") + + mockXML := ` + + 5.26.0 + + + rootfs + 0 + 1 + + ` + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("Mock server received request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintln(w, mockXML) + })) + defer server.Close() + + cfg := &config.Config{ + MonitScrapeURI: server.URL, } - if !errors.Is(err, ErrNilConfig) { - t.Errorf("expected ErrNilConfig, got %v", err) + exp, err := NewExporter(cfg) + if err != nil { + t.Fatalf("Failed to create Exporter: %v", err) } - if exp != nil { - t.Fatal("expected Exporter to be nil") + + ch := make(chan prometheus.Metric) + go func() { + exp.Collect(ch) + close(ch) + }() + + for range ch { + } + + upValue := testutil.ToFloat64(exp.up) + if upValue != 1 { + t.Errorf("Expected exporter_up=1, got %f", upValue) } } -// TestExporterMetrics checks if the Exporter exposes basic metrics. -func TestExporterMetrics(t *testing.T) { - cfg := &config.Config{} - exp, _ := NewExporter(cfg) +// TestExporter_Collect_MonitError uses a mock server that returns an HTTP error. +func TestExporter_Collect_MonitError(t *testing.T) { + t.Log("Testing Exporter.Collect when Monit returns an error status code") + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "some error", http.StatusBadRequest) + })) + defer server.Close() - metricCount := testutil.CollectAndCount(exp) - if metricCount == 0 { - t.Errorf("no metrics collected") + cfg := &config.Config{MonitScrapeURI: server.URL} + exp, err := NewExporter(cfg) + if err != nil { + t.Fatalf("Failed to create Exporter: %v", err) + } + + ch := make(chan prometheus.Metric) + go func() { + exp.Collect(ch) + close(ch) + }() + + upValue := testutil.ToFloat64(exp.up) + if upValue != 0 { + t.Errorf("Expected exporter_up=0 on error, got %f", upValue) } } + +// Example optional test: verifying logs (only if needed). +func TestExporter_Logs(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + t.Log("Testing Exporter logs at DebugLevel (mock scenario)") +} diff --git a/internal/monit/monit.go b/internal/monit/monit.go index f19c746..d12e383 100644 --- a/internal/monit/monit.go +++ b/internal/monit/monit.go @@ -6,37 +6,37 @@ import ( "crypto/tls" "encoding/xml" "fmt" + "golang.org/x/net/html/charset" "io" "net/http" "time" "github.com/ririnto/monit_exporter/internal/config" - "golang.org/x/net/html/charset" + "github.com/sirupsen/logrus" ) -// XML represents the top-level structure of the Monit status XML. -// It includes , , and multiple elements. -type XML struct { +// Monit represents the top-level XML element . +type Monit struct { XMLName xml.Name `xml:"monit"` Server Server `xml:"server"` Platform Platform `xml:"platform"` Services []Service `xml:"service"` } -// Server contains details from the element in the Monit XML. +// Server represents the element in the Monit XML. type Server struct { - ID string `xml:"id"` - Incarnation int `xml:"incarnation"` - Version string `xml:"version"` - Uptime int `xml:"uptime"` - Poll int `xml:"poll"` - StartDelay int `xml:"startdelay"` - LocalHost string `xml:"localhostname"` - ControlFile string `xml:"controlfile"` - HTTPD HTTPD `xml:"httpd"` -} - -// HTTPD represents the element inside the element. + ID string `xml:"id"` + Incarnation int64 `xml:"incarnation"` + Version string `xml:"version"` + Uptime int64 `xml:"uptime"` + Poll int `xml:"poll"` + StartDelay int `xml:"startdelay"` + Localhostname string `xml:"localhostname"` + Controlfile string `xml:"controlfile"` + HTTPD HTTPD `xml:"httpd"` +} + +// HTTPD represents the element in the Monit XML. type HTTPD struct { Address string `xml:"address"` Port int `xml:"port"` @@ -50,28 +50,151 @@ type Platform struct { Version string `xml:"version"` Machine string `xml:"machine"` CPU int `xml:"cpu"` - Memory int `xml:"memory"` - Swap int `xml:"swap"` + Memory int64 `xml:"memory"` + Swap int64 `xml:"swap"` } -// Service represents information about a single Monit service. -// It is mapped from the element (including the "type" attribute). +// Service represents the element in the Monit XML. type Service struct { - Type int `xml:"type,attr"` - Name string `xml:"name"` - Status int `xml:"status"` - Monitored string `xml:"monitor"` + Type int `xml:"type,attr"` + Name string `xml:"name"` + CollectedSec int64 `xml:"collected_sec"` + CollectedUsec int64 `xml:"collected_usec"` + Status int `xml:"status"` + StatusHint int `xml:"status_hint"` + Monitor int `xml:"monitor"` + MonitorMode int `xml:"monitormode"` + OnReboot int `xml:"onreboot"` + PendingAction int `xml:"pendingaction"` + Fstype string `xml:"fstype,omitempty"` + Fsflags string `xml:"fsflags,omitempty"` + Mode string `xml:"mode,omitempty"` + UID int `xml:"uid,omitempty"` + GID int `xml:"gid,omitempty"` + Block *Block `xml:"block,omitempty"` + Inode *Inode `xml:"inode,omitempty"` + Read string `xml:"read,omitempty"` + Write string `xml:"write,omitempty"` + Port *Port `xml:"port,omitempty"` + System *System `xml:"system,omitempty"` + Link *Link `xml:"link,omitempty"` +} + +// Block represents the element under a filesystem service. +type Block struct { + Percent float64 `xml:"percent"` + Usage float64 `xml:"usage"` + Total float64 `xml:"total"` +} + +// Inode represents the element under a filesystem service. +type Inode struct { + Percent float64 `xml:"percent"` + Usage int `xml:"usage"` + Total int `xml:"total"` +} + +// Port represents the element, typically for remote host checks. +type Port struct { + Hostname string `xml:"hostname"` + Portnumber int `xml:"portnumber"` + Request string `xml:"request"` + Protocol string `xml:"protocol"` + Type string `xml:"type"` + Responsetime float64 `xml:"responsetime"` + Certificate Certificate `xml:"certificate"` +} + +// Certificate represents the element under . +type Certificate struct { + Valid int `xml:"valid"` +} + +// System represents the element, usually present in type="5" (System) services. +type System struct { + Load Load `xml:"load"` + CPU CPU `xml:"cpu"` + Memory Memory `xml:"memory"` + Swap Swap `xml:"swap"` +} + +// Load represents the element under . +type Load struct { + Avg01 float64 `xml:"avg01"` + Avg05 float64 `xml:"avg05"` + Avg15 float64 `xml:"avg15"` +} + +// CPU represents the element under . +type CPU struct { + User float64 `xml:"user"` + System float64 `xml:"system"` + Wait float64 `xml:"wait"` } -// FetchMonitStatus sends an HTTP GET request to the Monit endpoint and -// returns the response body. It applies a 5-second timeout context to -// prevent indefinite waiting. +// Memory represents the element under . +type Memory struct { + Percent float64 `xml:"percent"` + Kilobyte int `xml:"kilobyte"` +} + +// Swap represents the element under . +type Swap struct { + Percent float64 `xml:"percent"` + Kilobyte int `xml:"kilobyte"` +} + +// Link represents the element under a network service. +type Link struct { + State int `xml:"state"` + Speed int64 `xml:"speed"` + Duplex int `xml:"duplex"` + Download Download `xml:"download"` + Upload Upload `xml:"upload"` +} + +// Download represents the element under . +type Download struct { + Packets Packets `xml:"packets"` + Bytes Bytes `xml:"bytes"` + Errors Errors `xml:"errors"` +} + +// Upload represents the element under . +type Upload struct { + Packets Packets `xml:"packets"` + Bytes Bytes `xml:"bytes"` + Errors Errors `xml:"errors"` +} + +// Packets represents the element under or . +type Packets struct { + Now int `xml:"now"` + Total int `xml:"total"` +} + +// Bytes represents the element under or . +type Bytes struct { + Now int `xml:"now"` + Total int `xml:"total"` +} + +// Errors represents the element under or . +type Errors struct { + Now int `xml:"now"` + Total int `xml:"total"` +} + +// FetchMonitStatus sends an HTTP GET request to the Monit endpoint and returns the response body. func FetchMonitStatus(cfg *config.Config) ([]byte, error) { + logrus.Debugf("FetchMonitStatus: MonitScrapeURI=%s, IgnoreSSL=%t", cfg.MonitScrapeURI, cfg.IgnoreSSL) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", cfg.MonitScrapeURI, nil) if err != nil { + logrus.Errorf("FetchMonitStatus: failed to create HTTP request: %v", err) return nil, fmt.Errorf("unable to create request: %w", err) } req.SetBasicAuth(cfg.MonitUser, cfg.MonitPassword) @@ -81,36 +204,44 @@ func FetchMonitStatus(cfg *config.Config) ([]byte, error) { } client := &http.Client{Transport: tr} + logrus.Debug("FetchMonitStatus: sending request to Monit") resp, err := client.Do(req) if err != nil { + logrus.Errorf("FetchMonitStatus: HTTP request failed: %v", err) return nil, fmt.Errorf("unable to fetch Monit status: %w", err) } - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(resp.Body) + defer func() { + if cerr := resp.Body.Close(); cerr != nil { + logrus.Warnf("FetchMonitStatus: failed to close response body: %v", cerr) + } + }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { + logrus.Errorf("FetchMonitStatus: non-2xx status code: %d", resp.StatusCode) return nil, fmt.Errorf("monit returned non-2xx status code: %d", resp.StatusCode) } data, err := io.ReadAll(resp.Body) if err != nil { + logrus.Errorf("FetchMonitStatus: failed to read response body: %v", err) return nil, fmt.Errorf("unable to read Monit status: %w", err) } - + logrus.Debugf("FetchMonitStatus: successfully received response (%d bytes)", len(data)) return data, nil } -// ParseMonitStatus parses the Monit XML data into an XML struct, including -// fields for , , and elements. -func ParseMonitStatus(data []byte) (XML, error) { - var statusChunk XML +// ParseMonitStatus parses the XML data and returns a Monit struct. +func ParseMonitStatus(data []byte) (Monit, error) { + logrus.Debug("ParseMonitStatus: starting XML parsing") + var statusChunk Monit reader := bytes.NewReader(data) decoder := xml.NewDecoder(reader) decoder.CharsetReader = charset.NewReaderLabel if err := decoder.Decode(&statusChunk); err != nil { - return XML{}, fmt.Errorf("failed to parse Monit XML: %w", err) + logrus.Errorf("ParseMonitStatus: XML parsing failed: %v", err) + return Monit{}, fmt.Errorf("failed to parse Monit XML: %w", err) } + logrus.Debugf("ParseMonitStatus: successfully parsed. Services count=%d", len(statusChunk.Services)) return statusChunk, nil } diff --git a/internal/monit/monit_test.go b/internal/monit/monit_test.go index dbe573e..4593025 100644 --- a/internal/monit/monit_test.go +++ b/internal/monit/monit_test.go @@ -1,133 +1,75 @@ package monit import ( + "fmt" + "net/http" + "net/http/httptest" "testing" + + "github.com/ririnto/monit_exporter/internal/config" ) -// TestParseMonitStatusMinimal checks parsing with a minimal Monit XML snippet. -func TestParseMonitStatusMinimal(t *testing.T) { - xmlData := []byte(` - - - test_service - 0 - 1 - - -`) - parsed, err := ParseMonitStatus(xmlData) +// TestFetchMonitStatus_Success checks if FetchMonitStatus can retrieve mock XML successfully. +func TestFetchMonitStatus_Success(t *testing.T) { + t.Log("Testing FetchMonitStatus with a mock server providing valid XML") + + mockXML := `5.26.0` + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = fmt.Fprint(w, mockXML) + })) + defer server.Close() + + cfg := &config.Config{MonitScrapeURI: server.URL, IgnoreSSL: false} + data, err := FetchMonitStatus(cfg) if err != nil { - t.Fatalf("ParseMonitStatus failed: %v", err) + t.Fatalf("FetchMonitStatus returned error: %v", err) } - if len(parsed.Services) != 1 { - t.Errorf("expected 1 service, got %d", len(parsed.Services)) + if len(data) == 0 { + t.Errorf("Expected non-empty data, got empty") } +} - svc := parsed.Services[0] - if svc.Type != 5 { - t.Errorf("expected type=5, got %d", svc.Type) - } - if svc.Name != "test_service" { - t.Errorf("expected name=test_service, got %s", svc.Name) - } - if svc.Status != 0 { - t.Errorf("expected status=0, got %d", svc.Status) - } - if svc.Monitored != "1" { - t.Errorf("expected monitor=1, got %s", svc.Monitored) +// TestFetchMonitStatus_Non2xx checks if FetchMonitStatus returns an error on 400 status code. +func TestFetchMonitStatus_Non2xx(t *testing.T) { + t.Log("Testing FetchMonitStatus with a mock server returning 400 status") + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "some error", http.StatusBadRequest) + })) + defer server.Close() + + cfg := &config.Config{MonitScrapeURI: server.URL} + _, err := FetchMonitStatus(cfg) + if err == nil { + t.Fatal("Expected an error for HTTP 400 status, got nil") } } -// TestParseMonitStatusFullXML checks parsing with the full Monit XML snippet -// that includes , , and . -func TestParseMonitStatusFullXML(t *testing.T) { - xmlData := []byte(` - - - - acfbb9e9118e68d3754761a79d3aae16 - 1504605214 - 5.23.0 - 136736 - 60 - 0 - fc566edc8b68 - /opt/monit/etc/monitrc - -
172.17.0.2
- 2812 - 0 -
-
- - Linux - 4.9.27-moby - #1 SMP Thu May 11 04:01:18 UTC 2017 - x86_64 - 4 - 2046768 - 1048572 - - - fc566edc8b68 - 0 - 1 - -
-`) +// TestParseMonitStatus_Success verifies parsing a valid Monit XML. +func TestParseMonitStatus_Success(t *testing.T) { + t.Log("Testing ParseMonitStatus with a valid XML string") - parsed, err := ParseMonitStatus(xmlData) + mockXML := `5.26.0rootfs` + monitData, err := ParseMonitStatus([]byte(mockXML)) if err != nil { t.Fatalf("ParseMonitStatus failed: %v", err) } - // Check - if parsed.Server.ID != "acfbb9e9118e68d3754761a79d3aae16" { - t.Errorf("expected Server.ID=acfbb9e9118e68d3754761a79d3aae16, got %s", parsed.Server.ID) - } - if parsed.Server.Uptime != 136736 { - t.Errorf("expected Server.Uptime=136736, got %d", parsed.Server.Uptime) + if len(monitData.Services) != 1 { + t.Errorf("Expected 1 service, got %d", len(monitData.Services)) } - if parsed.Server.HTTPD.Port != 2812 { - t.Errorf("expected Server.HTTPD.Port=2812, got %d", parsed.Server.HTTPD.Port) - } - - // Check - if parsed.Platform.Name != "Linux" { - t.Errorf("expected platform name=Linux, got %s", parsed.Platform.Name) - } - if parsed.Platform.Release != "4.9.27-moby" { - t.Errorf("expected platform release=4.9.27-moby, got %s", parsed.Platform.Release) - } - if parsed.Platform.CPU != 4 { - t.Errorf("expected CPU=4, got %d", parsed.Platform.CPU) - } - - // Check - if len(parsed.Services) != 1 { - t.Errorf("expected 1 service, got %d", len(parsed.Services)) - return - } - svc := parsed.Services[0] - if svc.Type != 5 { - t.Errorf("expected service type=5, got %d", svc.Type) - } - if svc.Name != "fc566edc8b68" { - t.Errorf("expected service name=fc566edc8b68, got %s", svc.Name) - } - if svc.Status != 0 { - t.Errorf("expected status=0, got %d", svc.Status) - } - if svc.Monitored != "1" { - t.Errorf("expected monitored=1, got %s", svc.Monitored) + if monitData.Services[0].Name != "rootfs" { + t.Errorf("Expected service name 'rootfs', got '%s'", monitData.Services[0].Name) } } -// TestParseMonitStatusInvalidXML checks behavior with invalid XML. -func TestParseMonitStatusInvalidXML(t *testing.T) { - xmlData := []byte(`<<>>`) - _, err := ParseMonitStatus(xmlData) +// TestParseMonitStatus_Error verifies error handling for malformed XML. +func TestParseMonitStatus_Error(t *testing.T) { + t.Log("Testing ParseMonitStatus with malformed XML string") + + invalidXML := `missing closing tags` + _, err := ParseMonitStatus([]byte(invalidXML)) if err == nil { - t.Errorf("expected parsing error but got nil") + t.Fatal("Expected an XML parse error, got nil") } } diff --git a/main.go b/main.go index c78736a..0e86b8a 100644 --- a/main.go +++ b/main.go @@ -2,9 +2,12 @@ package main import ( "github.com/ririnto/monit_exporter/cmd" + "github.com/sirupsen/logrus" ) -// main calls the Execute function to start the Cobra-based CLI. +// main is the entry point of the Monit Exporter application. func main() { + logrus.Debug("main() function invoked: calling cmd.Execute()") cmd.Execute() + logrus.Debug("main() function completed: cmd.Execute() returned without error") } From a664d7948fb1d6a8b08b6754d10bfe92c23c2f36 Mon Sep 17 00:00:00 2001 From: ririnto Date: Sat, 18 Jan 2025 11:51:39 +0900 Subject: [PATCH 04/11] update README.md --- README.md | 140 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 87 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index fc99771..bf225bb 100644 --- a/README.md +++ b/README.md @@ -6,22 +6,25 @@ ### Introduction -Monit Exporter is a Prometheus Exporter that scrapes Monit status in XML format, then exposes the metrics via an HTTP -endpoint. It uses [Cobra](https://github.com/spf13/cobra) for the command-line interface and logs HTTP requests in -Common Log Format (CLF). +Monit Exporter is a Prometheus Exporter +that scrapes Monit status in XML format and exposes the metrics via an HTTP endpoint. ### Features -- Scrapes Monit status periodically -- Exposes Prometheus-compatible metrics -- CLI integration with Cobra -- Logs HTTP requests in CLF -- Fully configurable via command-line flags +- **Enhanced Logging:** + - Detailed logs provide insights into HTTP requests, metric collection processes, and potential issues. + +- **Exposes Prometheus-Compatible Metrics:** + - Seamlessly integrates with Prometheus for monitoring Monit-managed services. + +- **Fully Configurable via Command-Line Flags:** + - Customize exporter behavior and Monit scraping parameters as needed. ### Installation -1. Install [Go](https://golang.org/dl/) (version 1.16 or higher recommended). -2. Clone the repository and build: +1. **Install [Go](https://golang.org/dl/) (version 1.16 or higher recommended).** + +2. **Clone the repository and build:** ```bash git clone https://github.com/yourusername/monit_exporter.git @@ -39,17 +42,17 @@ Common Log Format (CLF). Below is an overview of the flags defined in `cmd/root.go`: -| Flag | Default | Description | -|--------------------|-------------------------------------------------------|--------------------------------------------------------------------------| -| `listen-address` | `localhost:9388` | The address on which the exporter.go will listen (e.g., '0.0.0.0:9388'). | -| `metrics-path` | `/metrics` | The HTTP path at which metrics are served (e.g., '/metrics'). | -| `ignore-ssl` | `false` | Whether to skip SSL certificate verification for Monit endpoints. | -| `monit-scrape-uri` | `http://localhost:2812/_status?format=xml&level=full` | The Monit status URL to scrape (XML format). | -| `monit-user` | *(empty)* | Basic auth username for accessing Monit. | -| `monit-password` | *(empty)* | Basic auth password for accessing Monit. | -| `log-level` | `info` | Log level for the application (debug, info, warn, error, fatal, panic). | +| Flag | Default | Description | +|--------------------|-------------------------------------------------------|-------------------------------------------------------------------------| +| `listen-address` | `localhost:9388` | The address on which the exporter will listen (e.g., '0.0.0.0:9388'). | +| `metrics-path` | `/metrics` | The HTTP path at which metrics are served (e.g., '/metrics'). | +| `ignore-ssl` | `false` | Whether to skip SSL certificate verification for Monit endpoints. | +| `monit-scrape-uri` | `http://localhost:2812/_status?format=xml&level=full` | The Monit status URL to scrape (XML format). | +| `monit-user` | *(empty)* | Basic auth username for accessing Monit. | +| `monit-password` | *(empty)* | Basic auth password for accessing Monit. | +| `log-level` | `info` | Log level for the application (debug, info, warn, error, fatal, panic). | -Launch the exporter with desired flags: +**Launch the exporter with desired flags:** ```bash ./monit_exporter serve \ @@ -60,12 +63,23 @@ Launch the exporter with desired flags: --log-level="info" ``` -Visit the metrics endpoint: +**Visit the metrics endpoint:** ```bash curl http://localhost:9388/metrics ``` +### Running Tests + +To run the unit tests for the exporter and Monit components: + +```bash +go test ./internal/exporter -v +go test ./internal/monit -v +``` + +Ensure that all tests pass to verify the integrity of the exporter before deployment. + ### Project / Package Structure ``` @@ -80,34 +94,39 @@ curl http://localhost:9388/metrics │ │ └── exporter.go (Implements the Prometheus Exporter logic) │ └── monit │ └── monit.go (Fetches and parses Monit status data) -└── main.go (Entrypoint: calls cmd.Execute()) +├── main.go (Entrypoint: calls cmd.Execute()) +├── README.md (This file) +├── LICENSE (MIT License) +├── exporter_test.go (Unit tests for exporter.go) +└── monit_test.go (Unit tests for monit.go) ``` ### License This project is licensed under the [MIT License](LICENSE). ---- - ## 한국어 -### 개요 +### 소개 -Monit Exporter는 Monit 상태 정보를 XML 형식으로 수집하고, 이를 Prometheus 메트릭으로 변환하여 HTTP 엔드포인트로 노출하는 -Exporter입니다. [Cobra](https://github.com/spf13/cobra)를 사용하여 CLI를 제공하며, HTTP 요청을 Common Log Format(CLF)으로 로깅합니다. +Monit Exporter는 Monit 상태 정보를 XML 형식으로 수집하고 이를 Prometheus 메트릭으로 변환하여 HTTP 엔드포인트로 노출하는 익스포터입니다. ### 기능 -- 주기적으로 Monit 상태를 스크랩 -- Prometheus 호환 형식으로 메트릭 노출 -- Cobra 기반 CLI -- Common Log Format 로깅 -- 커맨드 라인 플래그로 모든 설정 가능 +- **향상된 로깅:** + - 자세한 로그를 통해 HTTP 요청, 메트릭 수집 과정 및 잠재적인 문제를 파악할 수 있습니다. + +- **Prometheus 호환 메트릭 제공:** + - Monit에서 관리하는 서비스를 Prometheus와 원활히 통합하여 모니터링할 수 있습니다. + +- **커맨드라인 플래그로 완벽히 구성 가능:** + - 익스포터의 동작과 Monit 스크래핑 매개변수를 필요에 따라 사용자 정의할 수 있습니다. ### 설치 -1. [Go](https://golang.org/dl/) (버전 1.16 이상 권장)을 설치합니다. -2. 저장소를 클론하고 빌드합니다: +1. **[Go](https://golang.org/dl/) (버전 1.16 이상 권장)을 설치합니다.** + +2. **레포지토리를 클론하고 빌드합니다:** ```bash git clone https://github.com/yourusername/monit_exporter.git @@ -123,19 +142,19 @@ Exporter입니다. [Cobra](https://github.com/spf13/cobra)를 사용하여 CLI #### 플래그 -`cmd/root.go`에서 정의된 플래그는 다음 표와 같습니다: +`cmd/root.go`에 정의된 플래그는 다음과 같습니다: -| Flag | 기본값 | 설명 | -|--------------------|-------------------------------------------------------|--------------------------------------------------------| -| `listen-address` | `localhost:9388` | Exporter가 수신할 주소 및 포트 (예: `0.0.0.0:9388`) | -| `metrics-path` | `/metrics` | 메트릭이 제공될 HTTP 경로 (예: `/metrics`) | -| `ignore-ssl` | `false` | Monit 엔드포인트에 대해 SSL 인증서 검증을 무시할지 여부 | -| `monit-scrape-uri` | `http://localhost:2812/_status?format=xml&level=full` | Monit 상태를 스크랩할 XML URL | -| `monit-user` | *(없음)* | Monit에 접근하기 위한 Basic auth 사용자 이름 | -| `monit-password` | *(없음)* | Monit에 접근하기 위한 Basic auth 비밀번호 | -| `log-level` | `info` | 애플리케이션의 로그 레벨 (debug, info, warn, error, fatal, panic) | +| 플래그 | 기본값 | 설명 | +|--------------------|-------------------------------------------------------|---------------------------------------------------------| +| `listen-address` | `localhost:9388` | 익스포터가 수신할 주소 및 포트 (예: '0.0.0.0:9388'). | +| `metrics-path` | `/metrics` | 메트릭을 제공할 HTTP 경로 (예: '/metrics'). | +| `ignore-ssl` | `false` | Monit 엔드포인트에 대해 SSL 인증서 검증을 무시할지 여부. | +| `monit-scrape-uri` | `http://localhost:2812/_status?format=xml&level=full` | Monit 상태 정보를 수집할 XML URL. | +| `monit-user` | *(없음)* | Monit에 접근하기 위한 Basic auth 사용자 이름. | +| `monit-password` | *(없음)* | Monit에 접근하기 위한 Basic auth 비밀번호. | +| `log-level` | `info` | 애플리케이션의 로그 레벨 (debug, info, warn, error, fatal, panic). | -Exporter를 다음과 같이 실행할 수 있습니다: +**익스포터를 실행하려면 다음 명령어를 사용합니다:** ```bash ./monit_exporter serve \ @@ -146,27 +165,42 @@ Exporter를 다음과 같이 실행할 수 있습니다: --log-level="info" ``` -그리고 다음처럼 메트릭 엔드포인트를 확인합니다: +**메트릭 엔드포인트를 확인하려면 다음 명령어를 사용합니다:** ```bash curl http://localhost:9388/metrics ``` -### 패키지 구조 +### 테스트 실행 + +익스포터 및 Monit 컴포넌트의 단위 테스트를 실행하려면: + +```bash +go test ./internal/exporter -v +go test ./internal/monit -v +``` + +모든 테스트를 통과시켜 익스포터의 무결성을 검증한 후 배포하십시오. + +### 프로젝트 / 패키지 구조 ``` . ├── cmd -│ ├── root.go (루트 명령 및 플래그 설정) -│ └── serve.go (serve 명령 구현 및 서버 실행) +│ ├── root.go (루트 명령어와 플래그 정의) +│ └── serve.go (서버 실행 명령어 구현) ├── internal │ ├── config -│ │ └── config.go (Exporter를 위한 설정 구조체) +│ │ └── config.go (익스포터 설정 구조체 정의) │ ├── exporter -│ │ └── exporter.go (Prometheus Exporter 로직 구현) +│ │ └── exporter.go (Prometheus 익스포터 로직 구현) │ └── monit -│ └── monit.go (Monit 상태를 가져오고 파싱) -└── main.go (진입점: cmd.Execute() 호출) +│ └── monit.go (Monit 상태 수집 및 파싱) +├── main.go (진입점: cmd.Execute() 호출) +├── README.md (이 파일) +├── LICENSE (MIT 라이선스) +├── exporter_test.go (exporter.go 단위 테스트) +└── monit_test.go (monit.go 단위 테스트) ``` ### 라이선스 From 7c6a035bf71d63639ae1e6ff7bd54d2fc1a70874 Mon Sep 17 00:00:00 2001 From: ririnto Date: Sat, 18 Jan 2025 11:53:01 +0900 Subject: [PATCH 05/11] update module name --- cmd/serve.go | 4 ++-- go.mod | 2 +- internal/exporter/exporter.go | 4 ++-- internal/exporter/exporter_test.go | 2 +- internal/monit/monit.go | 2 +- internal/monit/monit_test.go | 2 +- main.go | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 3022c14..d4d02e6 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -12,8 +12,8 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/ririnto/monit_exporter/internal/config" - "github.com/ririnto/monit_exporter/internal/exporter" + "github.com/ririnto/monit-exporter/internal/config" + "github.com/ririnto/monit-exporter/internal/exporter" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) diff --git a/go.mod b/go.mod index be6e043..a727b3f 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/ririnto/monit_exporter +module github.com/ririnto/monit-exporter go 1.23.5 diff --git a/internal/exporter/exporter.go b/internal/exporter/exporter.go index 5d88bb6..480d60e 100644 --- a/internal/exporter/exporter.go +++ b/internal/exporter/exporter.go @@ -6,8 +6,8 @@ import ( "sync" "github.com/prometheus/client_golang/prometheus" - "github.com/ririnto/monit_exporter/internal/config" - "github.com/ririnto/monit_exporter/internal/monit" + "github.com/ririnto/monit-exporter/internal/config" + "github.com/ririnto/monit-exporter/internal/monit" "github.com/sirupsen/logrus" ) diff --git a/internal/exporter/exporter_test.go b/internal/exporter/exporter_test.go index 941f034..e29ceec 100644 --- a/internal/exporter/exporter_test.go +++ b/internal/exporter/exporter_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/prometheus/client_golang/prometheus/testutil" - "github.com/ririnto/monit_exporter/internal/config" + "github.com/ririnto/monit-exporter/internal/config" "github.com/sirupsen/logrus" ) diff --git a/internal/monit/monit.go b/internal/monit/monit.go index d12e383..dd8ffc5 100644 --- a/internal/monit/monit.go +++ b/internal/monit/monit.go @@ -11,7 +11,7 @@ import ( "net/http" "time" - "github.com/ririnto/monit_exporter/internal/config" + "github.com/ririnto/monit-exporter/internal/config" "github.com/sirupsen/logrus" ) diff --git a/internal/monit/monit_test.go b/internal/monit/monit_test.go index 4593025..9489f01 100644 --- a/internal/monit/monit_test.go +++ b/internal/monit/monit_test.go @@ -6,7 +6,7 @@ import ( "net/http/httptest" "testing" - "github.com/ririnto/monit_exporter/internal/config" + "github.com/ririnto/monit-exporter/internal/config" ) // TestFetchMonitStatus_Success checks if FetchMonitStatus can retrieve mock XML successfully. diff --git a/main.go b/main.go index 0e86b8a..f4b62e1 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,7 @@ package main import ( - "github.com/ririnto/monit_exporter/cmd" + "github.com/ririnto/monit-exporter/cmd" "github.com/sirupsen/logrus" ) From d22b31ab986cf7e032611b494ece1b871c519359 Mon Sep 17 00:00:00 2001 From: ririnto Date: Sat, 18 Jan 2025 11:54:42 +0900 Subject: [PATCH 06/11] update module name --- .github/workflows/deploy.yml | 6 +++--- README.md | 16 ++++++++-------- cmd/root.go | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5f1fc85..26ac77b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -31,15 +31,15 @@ jobs: run: | mkdir -p build if [ "${GOOS}" = "windows" ]; then - BINARY_NAME=monit_exporter-${GOOS}-${GOARCH}.exe + BINARY_NAME=monit-exporter-${GOOS}-${GOARCH}.exe else - BINARY_NAME=monit_exporter-${GOOS}-${GOARCH} + BINARY_NAME=monit-exporter-${GOOS}-${GOARCH} fi CGO_ENABLED=0 go build -ldflags="-s -w" -o build/${BINARY_NAME} - name: Upload binaries to GitHub Releases uses: softprops/action-gh-release@v1 with: - files: build/monit_exporter-* + files: build/monit-exporter-* env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index bf225bb..c5785bd 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,9 @@ that scrapes Monit status in XML format and exposes the metrics via an HTTP endp 2. **Clone the repository and build:** ```bash - git clone https://github.com/yourusername/monit_exporter.git - cd monit_exporter - go build -o monit_exporter + git clone https://github.com/ririnto/monit-exporter.git + cd monit-exporter + go build -o monit-exporter ``` ### Usage @@ -55,7 +55,7 @@ Below is an overview of the flags defined in `cmd/root.go`: **Launch the exporter with desired flags:** ```bash -./monit_exporter serve \ +./monit-exporter serve \ --listen-address="0.0.0.0:9388" \ --monit-scrape-uri="http://localhost:2812/_status?format=xml&level=full" \ --monit-user="admin" \ @@ -129,9 +129,9 @@ Monit Exporter는 Monit 상태 정보를 XML 형식으로 수집하고 이를 Pr 2. **레포지토리를 클론하고 빌드합니다:** ```bash - git clone https://github.com/yourusername/monit_exporter.git - cd monit_exporter - go build -o monit_exporter + git clone https://github.com/ririnto/monit-exporter.git + cd monit-exporter + go build -o monit-exporter ``` ### 사용법 @@ -157,7 +157,7 @@ Monit Exporter는 Monit 상태 정보를 XML 형식으로 수집하고 이를 Pr **익스포터를 실행하려면 다음 명령어를 사용합니다:** ```bash -./monit_exporter serve \ +./monit-exporter serve \ --listen-address="0.0.0.0:9388" \ --monit-scrape-uri="http://localhost:2812/_status?format=xml&level=full" \ --monit-user="admin" \ diff --git a/cmd/root.go b/cmd/root.go index fe1f110..da339bf 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -20,7 +20,7 @@ var ( // RootCmd is the base command for this application. var RootCmd = &cobra.Command{ - Use: "monit_exporter", + Use: "monit-exporter", Short: "Monit Exporter for Prometheus", Long: "Prometheus Exporter that collects Monit status information and exposes metrics.", RunE: func(cmd *cobra.Command, args []string) error { From 448111789a322c2490b8622364cb968c86036e89 Mon Sep 17 00:00:00 2001 From: ririnto Date: Sat, 18 Jan 2025 12:20:50 +0900 Subject: [PATCH 07/11] update labels --- README.md | 4 +- internal/exporter/exporter.go | 86 +++++++++++++++++++---------------- 2 files changed, 48 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index c5785bd..03321ab 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ that scrapes Monit status in XML format and exposes the metrics via an HTTP endp ### Installation -1. **Install [Go](https://golang.org/dl/) (version 1.16 or higher recommended).** +1. **Install [Go](https://golang.org/dl/) (version 1.23 or higher recommended).** 2. **Clone the repository and build:** @@ -124,7 +124,7 @@ Monit Exporter는 Monit 상태 정보를 XML 형식으로 수집하고 이를 Pr ### 설치 -1. **[Go](https://golang.org/dl/) (버전 1.16 이상 권장)을 설치합니다.** +1. **[Go](https://golang.org/dl/) (버전 1.23 이상 권장)을 설치합니다.** 2. **레포지토리를 클론하고 빌드합니다:** diff --git a/internal/exporter/exporter.go b/internal/exporter/exporter.go index 480d60e..900497c 100644 --- a/internal/exporter/exporter.go +++ b/internal/exporter/exporter.go @@ -2,6 +2,7 @@ package exporter import ( "errors" + "slices" "strconv" "sync" @@ -75,7 +76,7 @@ func NewExporter(cfg *config.Config) (*Exporter, error) { logrus.Debugf("NewExporter: creating exporter with ListenAddress=%s, MonitScrapeURI=%s", cfg.ListenAddress, cfg.MonitScrapeURI) - labelNames := []string{"check_name", "type", "monitored"} + labelNames := []string{"service_name", "service_type", "service_monitor_status"} return &Exporter{ cfg: cfg, @@ -347,64 +348,69 @@ func (e *Exporter) scrape() error { e.up.Set(1) logrus.Debug("Exporter.scrape: set exporter_up to 1 (Monit is reachable)") - for _, svc := range parsed.Services { - typ, ok := serviceTypes[svc.Type] + for service := range slices.Values(parsed.Services) { + serviceType, ok := serviceTypes[service.Type] if !ok { - typ = "unknown" - logrus.Warnf("Exporter.scrape: unknown service type=%d, name=%s", svc.Type, svc.Name) + serviceType = "unknown" + logrus.Warnf("Exporter.scrape: unknown service service_type=%d, serviceNameservice_name=%s", service.Type, service.Name) } - monitored := strconv.Itoa(svc.Monitor) + serviceMonitorStatus := strconv.Itoa(service.Monitor) e.status.With(prometheus.Labels{ - "check_name": svc.Name, - "type": typ, - "monitored": monitored, - }).Set(float64(svc.Status)) - - logrus.Debugf("Exporter.scrape: service=%s, type=%s, monitor=%d, status=%d", - svc.Name, typ, svc.Monitor, svc.Status) - - e.collectServiceMetrics(svc, typ, monitored) + "service_name": service.Name, + "service_type": serviceType, + "service_monitor_status": serviceMonitorStatus, + }).Set(float64(service.Status)) + + logrus.Debugf( + "Exporter.scrape: service_name=%s, service_type=%s, service_monitor_status=%d, service_status=%d", + service.Name, + serviceType, + service.Monitor, + service.Status, + ) + + e.collectServiceMetrics(service, serviceType, serviceMonitorStatus) } return nil } // collectServiceMetrics updates detailed metrics for a single Monit service. -func (e *Exporter) collectServiceMetrics(svc monit.Service, typeStr, monitored string) { +func (e *Exporter) collectServiceMetrics(service monit.Service, serviceType, serviceMonitorStatus string) { labels := prometheus.Labels{ - "check_name": svc.Name, - "type": typeStr, - "monitored": monitored, + "service_name": service.Name, + "service_type": serviceType, + "service_monitor_status": serviceMonitorStatus, } - if svc.Block != nil { - e.blockUsage.With(labels).Set(svc.Block.Usage) - e.blockTotal.With(labels).Set(svc.Block.Total) - e.blockPercent.With(labels).Set(svc.Block.Percent) + if service.Block != nil { + e.blockUsage.With(labels).Set(service.Block.Usage) + e.blockTotal.With(labels).Set(service.Block.Total) + e.blockPercent.With(labels).Set(service.Block.Percent) } - if svc.Inode != nil { - e.inodeUsage.With(labels).Set(float64(svc.Inode.Usage)) - e.inodeTotal.With(labels).Set(float64(svc.Inode.Total)) - e.inodePercent.With(labels).Set(svc.Inode.Percent) + if service.Inode != nil { + e.inodeUsage.With(labels).Set(float64(service.Inode.Usage)) + e.inodeTotal.With(labels).Set(float64(service.Inode.Total)) + e.inodePercent.With(labels).Set(service.Inode.Percent) } - if svc.Port != nil { - e.portResponseTime.With(labels).Set(svc.Port.Responsetime) + if service.Port != nil { + e.portResponseTime.With(labels).Set(service.Port.Responsetime) } - if svc.System != nil { - e.systemLoadAvg01.With(labels).Set(svc.System.Load.Avg01) - e.systemLoadAvg05.With(labels).Set(svc.System.Load.Avg05) - e.systemLoadAvg15.With(labels).Set(svc.System.Load.Avg15) + if service.System != nil { + e.systemLoadAvg01.With(labels).Set(service.System.Load.Avg01) + e.systemLoadAvg05.With(labels).Set(service.System.Load.Avg05) + e.systemLoadAvg15.With(labels).Set(service.System.Load.Avg15) - e.systemCPUUser.With(labels).Set(svc.System.CPU.User) - e.systemCPUSystem.With(labels).Set(svc.System.CPU.System) - e.systemCPUWait.With(labels).Set(svc.System.CPU.Wait) + e.systemCPUUser.With(labels).Set(service.System.CPU.User) + e.systemCPUSystem.With(labels).Set(service.System.CPU.System) + e.systemCPUWait.With(labels).Set(service.System.CPU.Wait) - e.systemMemPercent.With(labels).Set(svc.System.Memory.Percent) - e.systemMemKilobytes.With(labels).Set(float64(svc.System.Memory.Kilobyte)) - e.systemSwapPercent.With(labels).Set(svc.System.Swap.Percent) - e.systemSwapKilobytes.With(labels).Set(float64(svc.System.Swap.Kilobyte)) + e.systemMemPercent.With(labels).Set(service.System.Memory.Percent) + e.systemMemKilobytes.With(labels).Set(float64(service.System.Memory.Kilobyte)) + e.systemSwapPercent.With(labels).Set(service.System.Swap.Percent) + e.systemSwapKilobytes.With(labels).Set(float64(service.System.Swap.Kilobyte)) } } From d90d5a63e964e0583223c78a8694aafda0147831 Mon Sep 17 00:00:00 2001 From: ririnto Date: Sat, 18 Jan 2025 13:23:59 +0900 Subject: [PATCH 08/11] add favicon.ico --- cmd/serve.go | 32 ++++++++++++++++++-------------- cmd/static/favicon.ico | Bin 0 -> 15086 bytes 2 files changed, 18 insertions(+), 14 deletions(-) create mode 100644 cmd/static/favicon.ico diff --git a/cmd/serve.go b/cmd/serve.go index d4d02e6..1df2082 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -7,9 +7,12 @@ import ( "net/http" "os" "os/signal" + "path/filepath" "syscall" "time" + _ "embed" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/ririnto/monit-exporter/internal/config" @@ -18,7 +21,9 @@ import ( "github.com/spf13/cobra" ) -// serveCmd starts the Monit Exporter server. +//go:embed static/favicon.ico +var embeddedFavicon []byte + var serveCmd = &cobra.Command{ Use: "serve", Short: "Run the Monit Exporter server", @@ -52,28 +57,27 @@ var serveCmd = &cobra.Command{ prometheus.MustRegister(exp) mux := http.NewServeMux() - mux.Handle(cfg.MetricsPath, commonLogHandler(promhttp.Handler())) + mux.Handle(cfg.MetricsPath, promhttp.Handler()) + mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { + if 0 < len(embeddedFavicon) { + w.Header().Set("Content-Type", "image/x-icon") + _, _ = w.Write(embeddedFavicon) + } else { + http.ServeFile(w, r, filepath.Join("static", "favicon.ico")) + } + }) + logrus.Infof("Serving embedded favicon.ico") mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { logrus.Debugf("Root path request received from %s", r.RemoteAddr) w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = fmt.Fprintf( w, - ` - Monit Exporter - -

Monit Exporter

-

Metrics

- - `, + `Monit Exporter

Monit Exporter

Metrics

`, cfg.MetricsPath, ) }) - server := &http.Server{ - Addr: cfg.ListenAddress, - Handler: mux, - } - + server := &http.Server{Addr: cfg.ListenAddress, Handler: commonLogHandler(mux)} shutdownCh := make(chan os.Signal, 1) signal.Notify(shutdownCh, os.Interrupt, syscall.SIGTERM) go func() { diff --git a/cmd/static/favicon.ico b/cmd/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..34bd1fbf0d44900e5e6e2298c58b4b124126d3e3 GIT binary patch literal 15086 zcmd6u2e6gJ6~{kr=8iW=i1SQ13BpdwP9%H#E3;g!w*xA*SlyZ3$VUY;|ang8AI+j@4t-LvPM-RpV% zy#C(in|lfedlNVDygfb78$3AmK3nlK6<1W0e&5ma4*I3%4N)E?sYEJXf#)e-6d7X% z3Wo}_ySvSrn)>d}+J^2Ne)(1#RZeCz{NS4>x9kxYyokGu@UA!|%<)Cc?2?_xgqe4QC_Qn?wPE=wJhKjQo*2eKE_3k2O0)BEss8UZuH5W4rrew{wZ^oyXN?`j z?iUUgYIuQ|16SNtZ_X;NHH_KuQ>rcR&uLt8s9f*+2?K;X{Ar94bLD`uE6q(a8_gY$ zH<_}kEZ$+{dZ9nFSmBn!vw~%?ReV<0b(>SKtTBr*`TFYk@U7OIW2f*QW8H<12)R1?KAjm!Gg>kmr>S z^t=-Fq_|%S{&fGdOD8X7D2tpKEbK2JpDZDG3Lx>udK@N97M>H93(PSFA2bMG3aq&a z!VZG{1_d;iDWW=PcQ4^Sp;X9xY~)s5W4Ebq>NcHPV?J81=dT6kPLc4VOoe$osN6OJ zGO=37P+Lcjd8wq$OuC^qB}Xr;G8f%aXWo3j-O18SUOkiv#X^=`jblUk>GQinR>zRv z%zNa}kfT%U9~rS%J}u7H{NSEyBwctZ>ZYzyPzPA=GwUKfuM^pVnr2Qi>{ zzV|!1Wt+7xRMvJ`8H=p>wmceZl?QoTr83By0D4(*zwq;*_x{3d0eQ{C&o^7Sd)TBZ zb9h3K_t0}I4RR`gm=Blbw7-wykYT-uy>4P` z_Jj`udMUb6FIu#8ok7Q7&cOq^2D*pSox;LhB^!5@jDke7=$`A&?d-mVGRX z&_!>1v{5>4wYmK7_2%(;E#_){hq(6P^FZk)vtDj7cRkf)&cCtF9Qo%ebD!jL2(40l zuMfo7BWoI(yY1Lj*LPdJ7g-I(S=>}{DnHH1eq|J`;w zrml^skZ~V(d5zV%!|LMl{@+^7tsJ^4zDT?1il`s0}+he-BU)FkTvovQvkAyEF%HuT{a z>&%`f%XU!T{B7}!J+ZgeDLOsxjMLeFt2G_bvX5%noJI*lgcU+4HA;q^B>BhOVtu27 zI6kna##~%&*SX_wmh80zgf6hP@FT(6sZnMAF{ep-R++V1uz}rLB%E?vYMrBpVOygE zB%rBo4_4k00(MU*EmfxB{(@|R(D&HuV&?t(%DRKU&D>|D?<)lIpm<9ivVHj8?2k*o-mtfoS9b;3 zAg~#oZL;1sj#LMHY$uEtW(tc1 zm*90vTf2feu^StypZuzM6M8JEPyl~KNt2EE)ei-eJHFI(1Q`! z0O5(>kFjK&&k>LbKPjNejV_{gv5kfb7YqLu66tHo=ZLjOALj`Zg~39id9g9fs|~kq z?$R^k8M;W3m+UR*x@ljuvIi2fXt^*=z`yjp^h5P-C0r{6${oeqHUF{p+&6(Gv z{Nebau}$xt(`>#fUGMzaWX~nWf_^X$3zADg-$r%y6GjRPg;2T8IHS-0Ui-JT<)pKq zHvn`u!UMFAb2iW}+c$!kTQ3T`1o4wkoaz`LoGFBkxzY@_c=5drR_}4Tp3?*UA$`C{ zhCd9ya||t0`Vm4w^GW446)qK0ihv%%Xx@L?tQ{N0yXyye(6R7SC!h-DIYr3J*QxxB zJG=)viWHwA_PNP7)fxOc=($Ni-kasGOh}v+j1v-n2jx9Oh#hz3v2&U8f(vf0v;JCq zwsD}p*wWbJ2?<$yNRqj)d?ST5!Vir;6GKCz`k-`l^#xIkb1ula8FJ(?2fY)QH`3*) z3tJd}Yz#fE^q&UxLGj2*^vX!###xiKuaJ#ei+uvsg$?)7m$7TQL;Rf{!aw>*?7V@0 zzn$#l%EpAqB>VtN;`tYgW{I$^zYlsxFPI_3(kqgcjCm3{lRQtEJ*!PH{_w~AxHyM@ z&cM)TV(HW6>&?`A8ssy{2ULH15dy#EKD zXA6=3O)|~ZJsj$0zW+%!2xqpuQPkjWAK~NFWPx~QREpR zY(eh{_z6M@9dXY5R%`zucUG5mX3l?S>I|2)95*k5(iv~YnR7hObE}@#9xv)Y%%upAWEoPf0Km&p!&Jnm4wHrJlMtf_!G*T zAU5DX*2c;$$eA7IkLWj1{Kcg)hq!x&d?=8Xsa&LO9#k*B_|tVKBk9>$SU->n`{^7P z-+zI`y^clsmc_rzSr4)=jJG85@W1bA71l45FAwSb>|KxLTb8?r=U!LaC*!~WnH5&1 z<;#QevwvM7OcL_`fHyqUxY7J?|>m#Glw0IVIg@q6WZsL)Y+iRM?AKBf%JgNS-+M-e5QF3 zdnU5m$sqQ9^gG8}Zg|<9^;Y+A{Y8)EPREMEWDm;4(LVcYN4>Ags{_i+-D&g(bYJfI zVFz*-IG1<&#(Ct?+%rzgEW%rLmnG!>M;t5fD?{c^tDfYKGGO_?y>pqnCDg<^^SEg$&I}W6^9L<*Ec{%;qEf$z1)H3ZUs82 z8)Lo$^M$p`xd7+X?AgS*^Wi8)L(({d%GmCXe?rCMvrT6Gq$@`^Weu<&pvQ1#?_US> zg}V#bg)?7hw&VYY@fGHa@Vis#ngauLofG{$`=6Dzg^-slS}0oyeI5TFgbxH8!L3vJ zHtqffoA$p?JM4WF=2*xbOq(y_=OLy2*w1tBv(iThDij8%Wu%T@{Ja1leF>Ws`Umq;w;9kVb&`5b2wXa^x(72 zhP^vaVqNYV#A{HT^5RdNDCFG`dnxxtm^aL!hn{OzH5;g0Yn=ad6Sga|)BcAf`m8ak z&c#aHFNm*v<5bV?!aO07o{FqIS$7>Ty0tE!Ga^-Q34<8l;P(0Q8I@o4Z7xg}mJ6{T zpSyjm)0lfSswzV)a(q$agzx2Egz81l*g=>k;JeU{6;8;V1nZOq^06>Q*g9$q`zlTC z(2qgFSps*UN`;h=*Fy~P?bt{8j!y_q5LI`QG!32FNB2pC%S1ev>S7ULi7p!v7V91nA(iS!ca3zj$@6sY zNH>r4-zdUCo~QdrDyaWU`9uD1#k&jj4<`Gru$XwAOy`C~6W=EkP=7HME53x#$B%s% ppQXerbp3Im>!<6t8wU-E9hb~Fm88c_1?@Ph;7Dc4jB|S2{||<&`cnV^ literal 0 HcmV?d00001 From 0b1fe77704c0c5a060df7ea9b5e61a9ac209410f Mon Sep 17 00:00:00 2001 From: ririnto Date: Sat, 18 Jan 2025 13:24:13 +0900 Subject: [PATCH 09/11] add favicon.ico --- cmd/serve.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/serve.go b/cmd/serve.go index 1df2082..dcd8f14 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -66,7 +66,6 @@ var serveCmd = &cobra.Command{ http.ServeFile(w, r, filepath.Join("static", "favicon.ico")) } }) - logrus.Infof("Serving embedded favicon.ico") mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { logrus.Debugf("Root path request received from %s", r.RemoteAddr) w.Header().Set("Content-Type", "text/html; charset=utf-8") From 479c3891f9cc698fa1a9e4bfe1f6b583010b1ca5 Mon Sep 17 00:00:00 2001 From: ririnto Date: Sat, 18 Jan 2025 13:34:52 +0900 Subject: [PATCH 10/11] fix. README.md --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 03321ab..33a092c 100644 --- a/README.md +++ b/README.md @@ -96,9 +96,7 @@ Ensure that all tests pass to verify the integrity of the exporter before deploy │ └── monit.go (Fetches and parses Monit status data) ├── main.go (Entrypoint: calls cmd.Execute()) ├── README.md (This file) -├── LICENSE (MIT License) -├── exporter_test.go (Unit tests for exporter.go) -└── monit_test.go (Unit tests for monit.go) +└── LICENSE (MIT License) ``` ### License @@ -198,9 +196,7 @@ go test ./internal/monit -v │ └── monit.go (Monit 상태 수집 및 파싱) ├── main.go (진입점: cmd.Execute() 호출) ├── README.md (이 파일) -├── LICENSE (MIT 라이선스) -├── exporter_test.go (exporter.go 단위 테스트) -└── monit_test.go (monit.go 단위 테스트) +└── LICENSE (MIT 라이선스) ``` ### 라이선스 From 28b95172081c12fb9a7787ced15b7f79c42f38c0 Mon Sep 17 00:00:00 2001 From: ririnto Date: Sat, 18 Jan 2025 13:48:15 +0900 Subject: [PATCH 11/11] add: Additional Metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Change Log ## Major Changes 1. **Updated `serviceTypes` to Match Monit Naming** - The `serviceTypes` map has been updated to align with Monit naming conventions for better compatibility. ```go var serviceTypes = map[int]string{ 0: "Filesystem", 1: "Directory", 2: "File", 3: "Process", 4: "Remote host", 5: "System", 6: "Fifo", 7: "Program", 8: "Network", } ``` 2. **Renamed Metric Labels** - Metric label names have been updated to improve clarity and consistency with Monit conventions. ```go labelNames := []string{"service_name", "service_type", "service_monitor_status"} ``` 3. **Extended Supported Metrics** - New Monit-related metrics have been added to enhance monitoring capabilities: - `monit_exporter_up` - `monit_exporter_service_check` - `monit_service_block_usage_bytes` - `monit_service_block_total_bytes` - `monit_service_block_usage_percent` - `monit_service_inode_usage` - `monit_service_inode_total` - `monit_service_inode_usage_percent` - `monit_service_port_response_seconds` - `monit_service_system_loadavg_01` - `monit_service_system_loadavg_05` - `monit_service_system_loadavg_15` - `monit_service_system_cpu_user_percent` - `monit_service_system_cpu_system_percent` - `monit_service_system_cpu_wait_percent` - `monit_service_system_memory_usage_percent` - `monit_service_system_memory_usage_kilobytes` - `monit_service_system_swap_usage_percent` - `monit_service_system_swap_usage_kilobytes` 4. **Automated Release Creation on Tag** - Configured GitHub Actions or CI/CD pipelines to automatically generate a Release when a tag is created. 5. **Renamed Executable File** - The executable file name has been changed from `monit_exporter` to `monit-exporter` to standardize naming conventions. These changes aim to improve compatibility with Monit, enhance monitoring capabilities, and provide a more consistent user experience. --- # 변경 로그 ## 주요 변경 사항 1. **`serviceTypes`를 Monit 명칭과 동일하게 업데이트** - `serviceTypes` 맵을 Monit 명칭과 동일하게 변경하여 호환성을 강화하였습니다. ```go var serviceTypes = map[int]string{ 0: "Filesystem", 1: "Directory", 2: "File", 3: "Process", 4: "Remote host", 5: "System", 6: "Fifo", 7: "Program", 8: "Network", } ``` 2. **Metric Label 이름 변경** - Metric 라벨 이름을 보다 명확하고 Monit 명명 규칙에 맞게 수정하였습니다. ```go labelNames := []string{"service_name", "service_type", "service_monitor_status"} ``` 3. **지원하는 Metric 확장** - 모니터링 기능 강화를 위해 새로운 Monit 관련 Metric을 추가하였습니다: - `monit_exporter_up` - `monit_exporter_service_check` - `monit_service_block_usage_bytes` - `monit_service_block_total_bytes` - `monit_service_block_usage_percent` - `monit_service_inode_usage` - `monit_service_inode_total` - `monit_service_inode_usage_percent` - `monit_service_port_response_seconds` - `monit_service_system_loadavg_01` - `monit_service_system_loadavg_05` - `monit_service_system_loadavg_15` - `monit_service_system_cpu_user_percent` - `monit_service_system_cpu_system_percent` - `monit_service_system_cpu_wait_percent` - `monit_service_system_memory_usage_percent` - `monit_service_system_memory_usage_kilobytes` - `monit_service_system_swap_usage_percent` - `monit_service_system_swap_usage_kilobytes` 4. **태그 지정 시 Release 생성 자동화** - GitHub Actions 또는 CI/CD 파이프라인에서 태그가 생성되면 자동으로 Release를 생성하도록 구성하였습니다. 5. **실행 파일명 변경** - 실행 파일명을 기존 `monit_exporter`에서 `monit-exporter`로 변경하여 파일명 스타일을 표준화하였습니다. 이 변경 사항은 Monit과의 호환성을 강화하고 모니터링 기능을 확장하며, 사용자 경험을 개선하기 위해 설계되었습니다. --- README.md | 12 +++++------- cmd/serve.go | 4 ++-- go.mod | 2 +- internal/exporter/exporter.go | 4 ++-- internal/exporter/exporter_test.go | 2 +- internal/monit/monit.go | 2 +- internal/monit/monit_test.go | 2 +- main.go | 2 +- 8 files changed, 14 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 33a092c..473d1ad 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ # Monit Exporter for Prometheus -> **Forked from** [commercetools/monit_exporter](https://github.com/commercetools/monit_exporter) - ## English ### Introduction -Monit Exporter is a Prometheus Exporter +Monit Exporter is a Prometheus Exporter that scrapes Monit status in XML format and exposes the metrics via an HTTP endpoint. ### Features @@ -27,8 +25,8 @@ that scrapes Monit status in XML format and exposes the metrics via an HTTP endp 2. **Clone the repository and build:** ```bash - git clone https://github.com/ririnto/monit-exporter.git - cd monit-exporter + git clone https://github.com/commercetools/monit_exporter.git + cd monit_exporter go build -o monit-exporter ``` @@ -127,8 +125,8 @@ Monit Exporter는 Monit 상태 정보를 XML 형식으로 수집하고 이를 Pr 2. **레포지토리를 클론하고 빌드합니다:** ```bash - git clone https://github.com/ririnto/monit-exporter.git - cd monit-exporter + git clone https://github.com/commercetools/monit_exporter.git + cd monit_exporter go build -o monit-exporter ``` diff --git a/cmd/serve.go b/cmd/serve.go index dcd8f14..ab0af5e 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -13,10 +13,10 @@ import ( _ "embed" + "github.com/commercetools/monit-exporter/internal/config" + "github.com/commercetools/monit-exporter/internal/exporter" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/ririnto/monit-exporter/internal/config" - "github.com/ririnto/monit-exporter/internal/exporter" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) diff --git a/go.mod b/go.mod index a727b3f..20ab80f 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/ririnto/monit-exporter +module github.com/commercetools/monit-exporter go 1.23.5 diff --git a/internal/exporter/exporter.go b/internal/exporter/exporter.go index 900497c..9bbbff5 100644 --- a/internal/exporter/exporter.go +++ b/internal/exporter/exporter.go @@ -6,9 +6,9 @@ import ( "strconv" "sync" + "github.com/commercetools/monit-exporter/internal/config" + "github.com/commercetools/monit-exporter/internal/monit" "github.com/prometheus/client_golang/prometheus" - "github.com/ririnto/monit-exporter/internal/config" - "github.com/ririnto/monit-exporter/internal/monit" "github.com/sirupsen/logrus" ) diff --git a/internal/exporter/exporter_test.go b/internal/exporter/exporter_test.go index e29ceec..4144581 100644 --- a/internal/exporter/exporter_test.go +++ b/internal/exporter/exporter_test.go @@ -7,8 +7,8 @@ import ( "net/http/httptest" "testing" + "github.com/commercetools/monit-exporter/internal/config" "github.com/prometheus/client_golang/prometheus/testutil" - "github.com/ririnto/monit-exporter/internal/config" "github.com/sirupsen/logrus" ) diff --git a/internal/monit/monit.go b/internal/monit/monit.go index dd8ffc5..7f4aea3 100644 --- a/internal/monit/monit.go +++ b/internal/monit/monit.go @@ -11,7 +11,7 @@ import ( "net/http" "time" - "github.com/ririnto/monit-exporter/internal/config" + "github.com/commercetools/monit-exporter/internal/config" "github.com/sirupsen/logrus" ) diff --git a/internal/monit/monit_test.go b/internal/monit/monit_test.go index 9489f01..7095202 100644 --- a/internal/monit/monit_test.go +++ b/internal/monit/monit_test.go @@ -6,7 +6,7 @@ import ( "net/http/httptest" "testing" - "github.com/ririnto/monit-exporter/internal/config" + "github.com/commercetools/monit-exporter/internal/config" ) // TestFetchMonitStatus_Success checks if FetchMonitStatus can retrieve mock XML successfully. diff --git a/main.go b/main.go index f4b62e1..14dc70b 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,7 @@ package main import ( - "github.com/ririnto/monit-exporter/cmd" + "github.com/commercetools/monit-exporter/cmd" "github.com/sirupsen/logrus" )