1
0
mirror of https://github.com/chylex/Apache-Prometheus-Exporter.git synced 2025-09-15 08:32:10 +02:00

Compare commits

...

5 Commits

Author SHA1 Message Date
589eaf4bc7 wip 2023-01-13 16:27:48 +01:00
54120e1b33 Add access log parser 2023-01-13 16:26:03 +01:00
723fd0b323 Add warning to README about known issues 2023-01-08 11:51:15 +01:00
3b3bf887f0 Add README 2023-01-07 20:38:55 +01:00
e4fc38538d Improve logs when starting web server 2023-01-07 20:33:13 +01:00
5 changed files with 191 additions and 17 deletions

104
README.md Normal file
View File

@@ -0,0 +1,104 @@
# Apache Prometheus Exporter
Exports Prometheus metrics from Apache access logs.
See the [docker](./docker) folder for an example setup using Docker Compose.
## 1. Configure Apache Access Log Format
The following snippet will create a log format named `prometheus` that includes all information the exporter expects. See [Apache documentation](https://httpd.apache.org/docs/2.4/mod/mod_log_config.html#formats) for explanation of the format.
```apache
LogFormat "%t %h \"%r\" %>s %O %{ms}T \"%{Referer}i\" \"%{User-Agent}i\"" prometheus
```
## 2. Configure Apache Virtual Hosts
The following snippet is an example of how you could configure Apache to serve 3 domains from different folders using macros.
Each domain has its own access and error log file. The log files are rotated daily, with a dedicated folder for each day, and a `${APACHE_LOG_DIR}/latest/` folder with hard links to today's log files - this folder will be watched by the exporter.
```apache
<Macro Logs $domain>
ErrorLog "|/usr/bin/rotatelogs -l -f -D -L ${APACHE_LOG_DIR}/latest/$domain.error.log ${APACHE_LOG_DIR}/%Y-%m-%d/$domain.error.log 86400"
CustomLog "|/usr/bin/rotatelogs -l -f -D -L ${APACHE_LOG_DIR}/latest/$domain.access.log ${APACHE_LOG_DIR}/%Y-%m-%d/$domain.access.log 86400" prometheus
</Macro>
<Macro Domain $domain>
<VirtualHost *:80>
ServerName $domain
DocumentRoot /var/www/html/$domain
Use Logs $domain
</VirtualHost>
</Macro>
Domain first.example.com
Domain second.example.com
Domain third.example.com
UndefMacro Domain
UndefMacro Logs
```
In this example, the `first.example.com` domain will be served from `/var/www/html/first.example.com`, and its logs will be written to:
- `${APACHE_LOG_DIR}/latest/first.example.com.access.log`
- `${APACHE_LOG_DIR}/latest/first.example.com.error.log`
## 3. Configure the Exporter
The exporter requires the following environment variables:
### `HTTP_HOST`
The host that the HTTP server for metrics will listen on. If omitted, defaults to `127.0.0.1`.
### `ACCESS_LOG_FILE_PATTERN`, `ERROR_LOG_FILE_PATTERN`
The path to the access/error log files. You may use a single wildcard to match multiple files in a folder, or to match multiple folders in one level of the path. Whatever is matched by the wildcard will become the Prometheus label `file`. If there is no wildcard, the `file` label will be empty.
#### Example 1 (File Name Wildcard)
Log files for all domains are in `/var/log/apache2/latest/` and are named `<domain>.access.log` and `<domain>.error.log`. This is the set up from the Apache configuration example above.
**Pattern:** `/var/log/apache2/latest/*.access.log`
- Metrics for `/var/log/apache2/latest/first.example.com.access.log` will be labeled: `first.example.com`
- Metrics for `/var/log/apache2/latest/first.example.com.error.log` will be labeled: `first.example.com`
- Metrics for `/var/log/apache2/latest/second.example.com.access.log` will be labeled: `second.example.com`
The wildcard may appear anywhere in the file name.
#### Example 2 (Folder Wildcard)
Every domain has its own folder in `/var/log/apache2/latest/` containing log files named `access.log` and `error.log`.
**Pattern:** `/var/log/apache2/latest/*/access.log`
- Metrics for `/var/log/apache2/latest/first.example.com/access.log` will be labeled: `first.example.com`
- Metrics for `/var/log/apache2/latest/first.example.com/error.log` will be labeled: `first.example.com`
- Metrics for `/var/log/apache2/latest/second.example.com/access.log` will be labeled: `second.example.com`
The wildcard must not include any prefix or suffix, so `/*/` is accepted, but `/prefix_*/` or `/*_suffix/` is not.
#### Notes
> At least one access log file and one error log file must be found when the exporter starts, otherwise the exporter immediately exits with an error.
> If a log file is deleted, the exporter will automatically resume watching it if it is re-created later. If you want the exporter to forget about deleted log files, restart the exporter.
## 4. Launch the Exporter
Start the exporter. The standard output will show which log files have been found, the web server host, and the metrics endpoint URL.
Press `Ctrl-C` to stop the exporter.
**Important:** Due to library bugs, the exporter will currently not watch rotated log files. If you want to use this project right now, you will need to add the `-c` flag to `rotatelogs`, and restart the exporter after every rotation.
## 5. Collect Prometheus Metrics
Currently, the exporter exposes only these metrics:
- `apache_requests_total` total number of requests
- `apache_errors_total` total number of errors
More detailed metrics will be added in the future.

58
src/log_parser.rs Normal file
View File

@@ -0,0 +1,58 @@
use std::fmt::{Display, Error, Formatter};
pub struct AccessLogLineParts<'a> {
pub time: &'a str,
pub remote_host: &'a str,
pub request: &'a str,
pub response_status: &'a str,
pub response_bytes: &'a str,
pub response_time_ms: &'a str,
pub referer: &'a str,
pub user_agent: &'a str,
}
impl Display for AccessLogLineParts<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
write!(f, "[{}] {} \"{}\" {} {} {} \"{}\" \"{}\"", self.time, self.remote_host, self.request, self.response_status, self.response_bytes, self.response_time_ms, self.referer, self.user_agent)
}
}
impl<'a> AccessLogLineParts<'a> {
pub fn parse(line: &'a str) -> Result<AccessLogLineParts<'a>, ParseError> {
let (time, line) = extract_between_chars(line, '[', ']').ok_or(ParseError::TimeBracketsNotFound)?;
let (remote_host, line) = next_space_delimited_part(line).ok_or(ParseError::RemoteHostNotFound)?;
let (request, line) = extract_between_chars(line.trim_start_matches(' '), '"', '"').ok_or(ParseError::RequestNotFound)?;
let (response_status, line) = next_space_delimited_part(line).ok_or(ParseError::ResponseStatusNotFound)?;
let (response_bytes, line) = next_space_delimited_part(line).ok_or(ParseError::ResponseBytesNotFound)?;
let (response_time_ms, line) = next_space_delimited_part(line).ok_or(ParseError::ResponseTimeNotFound)?;
let (referer, line) = extract_between_chars(line.trim_start_matches(' '), '"', '"').ok_or(ParseError::RefererNotFound)?;
let (user_agent, _) = extract_between_chars(line.trim_start_matches(' '), '"', '"').ok_or(ParseError::UserAgentNotFound)?;
Ok(AccessLogLineParts { time, remote_host, request, response_status, response_bytes, response_time_ms, referer, user_agent })
}
}
fn next_space_delimited_part(str: &str) -> Option<(&str, &str)> {
return str.trim_start_matches(' ').split_once(' ')
}
fn extract_between_chars(str: &str, left_side: char, right_side: char) -> Option<(&str, &str)> {
let str = str.trim_start_matches(' ');
let next_char = str.chars().next()?;
return if next_char == left_side {
str.get(1..)?.split_once(right_side)
} else {
None
};
}
#[derive(Debug, Copy, Clone)]
pub enum ParseError {
TimeBracketsNotFound,
RemoteHostNotFound,
RequestNotFound,
ResponseStatusNotFound,
ResponseBytesNotFound,
ResponseTimeNotFound,
RefererNotFound,
UserAgentNotFound,
}

View File

@@ -6,7 +6,7 @@ use std::path::PathBuf;
use linemux::{Line, MuxedLines};
use tokio::sync::mpsc::UnboundedSender;
use crate::ApacheMetrics;
use crate::{ApacheMetrics, log_parser};
use crate::log_file_pattern::LogFilePath;
#[derive(Copy, Clone, PartialEq)]
@@ -77,22 +77,33 @@ impl<'a> LogWatcher<'a> {
}
}
fn handle_line(&mut self, event: Line, metrics: &ApacheMetrics) {
match self.files.get(event.source()) {
Some(metadata) => {
let label = metadata.label;
let (kind, family) = match metadata.kind {
LogFileKind::Access => ("access log", &metrics.requests_total),
LogFileKind::Error => ("error log", &metrics.errors_total),
};
println!("[LogWatcher] Received {} line from \"{}\": {}", kind, label, event.line());
family.get_or_create(&metadata.get_label_set()).inc();
fn handle_line(&self, event: Line, metrics: &ApacheMetrics) {
if let Some(file) = self.files.get(event.source()) {
match file.kind {
LogFileKind::Access => self.handle_access_log_line(event.line(), file, metrics),
LogFileKind::Error => self.handle_error_log_line(event.line(), file, metrics),
}
None => {
println!("[LogWatcher] Received line from unknown file: {}", event.source().display());
} else {
println!("[LogWatcher] Received line from unknown file: {}", event.source().display());
}
}
fn handle_access_log_line(&self, line: &str, file: &LogFileInfo, metrics: &ApacheMetrics) {
match log_parser::AccessLogLineParts::parse(line) {
Ok(parts) => {
println!("[LogWatcher] Received access log line from \"{}\": {}", file.label, parts)
}
Err(err) => {
println!("[LogWatcher] Received access log line from \"{}\" with invalid format ({:?}): {}", file.label, err, line)
}
}
metrics.requests_total.get_or_create(&file.get_label_set()).inc();
}
fn handle_error_log_line(&self, line: &str, file: &LogFileInfo, metrics: &ApacheMetrics) {
println!("[LogWatcher] Received error log line from \"{}\": {}", file.label, line);
metrics.errors_total.get_or_create(&file.get_label_set()).inc();
}
}

View File

@@ -10,9 +10,10 @@ use crate::log_file_pattern::{LogFilePath, parse_log_file_pattern_from_env};
use crate::log_watcher::watch_logs_task;
use crate::web_server::{create_web_server, run_web_server};
mod log_file_pattern;
mod log_watcher;
mod apache_metrics;
mod log_file_pattern;
mod log_parser;
mod log_watcher;
mod web_server;
const ACCESS_LOG_FILE_PATTERN: &'static str = "ACCESS_LOG_FILE_PATTERN";

View File

@@ -23,7 +23,7 @@ pub fn create_web_server(host: &str, port: u16, metrics_registry: Mutex<Registry
let server = server.workers(1);
let server = server.bind((host, port));
println!("[WebServer] Starting web server on http://{}:{}", host, port);
println!("[WebServer] Starting web server on {0}:{1} with metrics endpoint: http://{0}:{1}/metrics", host, port);
return server.unwrap().run();
}