///|
/// A ranked operational finding derived from a report.
pub struct MetricFinding {
code : String
severity : String
message : String
value : Int
}
///|
/// A compact route-level summary for dashboards and endpoint reviews.
pub struct EndpointSummary {
route : String
requests : Int
errors : Int
total_bytes : Int
error_rate_percent : Int
average_bytes : Int
depth : Int
}
///|
/// Return the most frequent User-Agent values.
pub fn Report::top_user_agents(
self : Report,
limit : Int,
) -> Array[(String, Int)] {
take_string_counts(self.user_agent_counts, limit)
}
///|
/// Return the most frequent referrer values.
pub fn Report::top_referrers(
self : Report,
limit : Int,
) -> Array[(String, Int)] {
take_string_counts(self.referrer_counts, limit)
}
///|
/// Return query parameter names, independent of their values.
pub fn Report::top_query_keys(
self : Report,
limit : Int,
) -> Array[(String, Int)] {
take_string_counts(self.query_key_counts, limit)
}
///|
/// Return protocol usage counts ranked by frequency.
pub fn Report::top_protocols(
self : Report,
limit : Int,
) -> Array[(String, Int)] {
take_string_counts(self.protocol_counts, limit)
}
///|
/// Count requests by route depth, where `/a/b` has depth two.
pub fn Report::route_depth_counts(self : Report) -> Map[Int, Int] {
let result : Map[Int, Int] = Map([])
for route, count in self.route_counts {
let depth = route_depth(route)
let current = match result.get(depth) {
Some(value) => value
None => 0
}
result[depth] = current + count
}
result
}
///|
/// Count requests by broad operational class.
pub fn Report::request_class_counts(self : Report) -> Map[String, Int] {
let result : Map[String, Int] = Map([])
for status, count in self.status_counts {
let class = request_class(status)
let current = match result.get(class) {
Some(value) => value
None => 0
}
result[class] = current + count
}
result
}
///|
/// Return the percentage represented by one status class.
pub fn Report::status_class_percent(self : Report, class : String) -> Int {
let count = match self.status_class_counts().get(class) {
Some(value) => value
None => 0
}
if self.requests == 0 {
0
} else {
count * 100 / self.requests
}
}
///|
/// Return the percentage of requests carrying a query string.
pub fn Report::query_request_percent(self : Report) -> Int {
let with_query = self.query_requests
if self.requests == 0 {
0
} else {
with_query * 100 / self.requests
}
}
///|
/// Return the average response size in bytes.
pub fn Report::average_bytes(self : Report) -> Int {
if self.requests == 0 {
0
} else {
self.total_bytes / self.requests
}
}
///|
/// Return the ratio of malformed lines among all non-empty input lines.
pub fn Report::invalid_rate_percent(self : Report) -> Int {
if self.total_lines == 0 {
0
} else {
self.invalid_lines * 100 / self.total_lines
}
}
///|
/// Return a stable route-level summary sorted by request count.
pub fn Report::endpoint_summaries(
self : Report,
limit : Int,
) -> Array[EndpointSummary] {
let ranked = take_string_counts(self.route_counts, limit)
let result : Array[EndpointSummary] = []
for entry in ranked {
let route = entry.0
let requests = entry.1
let errors = route_error_count(self, route)
let bytes = route_bytes(self, route)
let rate = if requests == 0 { 0 } else { errors * 100 / requests }
result.push(EndpointSummary::{
route,
requests,
errors,
total_bytes: bytes,
error_rate_percent: rate,
average_bytes: if requests == 0 {
0
} else {
bytes / requests
},
depth: route_depth(route),
})
}
result
}
///|
/// Find common operational problems in a report.
pub fn Report::findings(self : Report) -> Array[MetricFinding] {
let result : Array[MetricFinding] = []
if self.error_rate_percent() >= 20 {
result.push(MetricFinding::{
code: "HIGH_ERROR_RATE",
severity: "error",
message: "error rate is at least 20 percent",
value: self.error_rate_percent(),
})
}
if self.server_error_requests() > 0 {
result.push(MetricFinding::{
code: "SERVER_ERRORS",
severity: "error",
message: "server-side failures were observed",
value: self.server_error_requests(),
})
}
if self.invalid_rate_percent() >= 5 {
result.push(MetricFinding::{
code: "MALFORMED_INPUT",
severity: "warning",
message: "at least five percent of lines were malformed",
value: self.invalid_rate_percent(),
})
}
if self.filtered_lines > 0 {
result.push(MetricFinding::{
code: "FILTERED_INPUT",
severity: "info",
message: "valid lines were excluded by analysis filters",
value: self.filtered_lines,
})
}
if self.requests > 0 && self.query_request_percent() >= 80 {
result.push(MetricFinding::{
code: "QUERY_HEAVY",
severity: "info",
message: "most requests contain query parameters",
value: self.query_request_percent(),
})
}
result
}
///|
/// Render a Prometheus text exposition with stable metric names.
pub fn Report::to_prometheus(self : Report, prefix : String) -> String {
let name = sanitize_metric_name(prefix)
let mut out = "# TYPE \{name}_requests_total counter\n"
out = out + "\{name}_requests_total \{self.requests}\n"
out = out + "\{name}_lines_total \{self.total_lines}\n"
out = out + "\{name}_invalid_lines_total \{self.invalid_lines}\n"
out = out + "\{name}_filtered_lines_total \{self.filtered_lines}\n"
out = out + "\{name}_bytes_total \{self.total_bytes}\n"
out = out + "\{name}_error_requests_total \{self.error_requests()}\n"
out = out + "\{name}_success_requests_total \{self.success_requests()}\n"
out = out + "\{name}_error_rate_percent \{self.error_rate_percent()}\n"
out = out + "\{name}_success_rate_percent \{self.success_rate_percent()}\n"
out = out + "\{name}_invalid_rate_percent \{self.invalid_rate_percent()}\n"
out = out + "\{name}_average_bytes \{self.average_bytes()}\n"
out = out + "\{name}_query_request_percent \{self.query_request_percent()}\n"
out + render_prometheus_map(name, "status", self.status_counts)
}
///|
/// Render a compact Markdown report for issue comments and release notes.
pub fn Report::to_markdown(self : Report, top? : Int = 10) -> String {
let limit = if top < 0 { 0 } else { top }
let mut out = "# Access log report\n\n"
out = out + "| Metric | Value |\n| --- | ---: |\n"
out = out + "| Requests | \{self.requests} |\n"
out = out + "| Invalid lines | \{self.invalid_lines} |\n"
out = out + "| Filtered lines | \{self.filtered_lines} |\n"
out = out + "| Total bytes | \{self.total_bytes} |\n"
out = out + "| Error rate | \{self.error_rate_percent()}% |\n"
out = out + "| Success rate | \{self.success_rate_percent()}% |\n"
out = out + "| Average bytes | \{self.average_bytes()} |\n\n"
out = out + markdown_counts("## Routes", self.route_counts, limit)
out = out +
markdown_counts("## Status classes", self.status_class_counts(), limit)
out = out + markdown_counts("## User agents", self.user_agent_counts, limit)
out = out + markdown_counts("## Query keys", self.query_key_counts, limit)
out
}
///|
/// Render one JSON object per request dimension for line-oriented pipelines.
pub fn Report::to_ndjson(self : Report) -> String {
let mut out = ""
for _, entry in sorted_int_entries(self.status_counts) {
let status = entry.0
let count = entry.1
out = out +
"{\"type\":\"status\",\"key\":\"" +
status.to_string() +
"\",\"count\":" +
"\{count}" +
"}\n"
}
for entry in sorted_string_counts(self.route_counts) {
out = out +
"{\"type\":\"route\",\"key\":\"" +
json_escape(entry.0) +
"\",\"count\":" +
entry.1.to_string() +
"}\n"
}
out
}
///|
/// Return all query parameter names in a query string.
fn query_keys(query : String) -> Array[String] {
let result : Array[String] = []
if query == "" || query == "-" {
return result
}
for part in query.split("&") {
let key = query_key(part.to_owned())
if key != "" {
result.push(key)
}
}
result
}
///|
fn query_key(part : String) -> String {
let chars = part.to_array()
let mut index = 0
while index < chars.length() && chars[index] != '=' {
index += 1
}
String::from_array(chars[0:index]).trim().to_owned()
}
///|
fn route_depth(route : String) -> Int {
let mut depth = 0
for ch in route.to_array() {
if ch == '/' {
depth += 1
}
}
if route == "/" || route == "" {
0
} else {
depth
}
}
///|
fn request_class(status : Int) -> String {
if status >= 200 && status < 300 {
"success"
} else if status >= 300 && status < 400 {
"redirect"
} else if status >= 400 && status < 500 {
"client_error"
} else if status >= 500 {
"server_error"
} else {
"other"
}
}
///|
fn route_error_count(report : Report, route : String) -> Int {
match report.route_error_counts.get(route) {
Some(value) => value
None => 0
}
}
///|
fn route_bytes(report : Report, route : String) -> Int {
match report.route_bytes_counts.get(route) {
Some(value) => value
None => 0
}
}
///|
fn sanitize_metric_name(value : String) -> String {
let out : Array[Char] = []
for ch in value.to_array() {
if is_metric_char(ch) {
out.push(ch)
} else {
out.push('_')
}
}
let result = String::from_array(out)
if result == "" {
"moon_accesslog"
} else {
result
}
}
///|
fn is_metric_char(ch : Char) -> Bool {
(ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '_'
}
///|
fn render_prometheus_map(
prefix : String,
metric : String,
counts : Map[Int, Int],
) -> String {
let mut out = ""
for _, entry in sorted_int_entries(counts) {
out = out + "\{prefix}_\{metric}_total{code=\"\{entry.0}\"} \{entry.1}\n"
}
out
}
///|
fn markdown_counts(
heading : String,
counts : Map[String, Int],
limit : Int,
) -> String {
let mut out = heading + "\n\n| Key | Count |\n| --- | ---: |\n"
let mut index = 0
for entry in sorted_string_counts(counts) {
if index < limit {
out = out + "| " + entry.0 + " | " + entry.1.to_string() + " |\n"
}
index += 1
}
out + "\n"
}
///|
fn sorted_int_entries(counts : Map[Int, Int]) -> Array[(Int, Int)] {
let entries : Array[(Int, Int)] = []
for key, value in counts {
entries.push((key, value))
}
entries.sort_by(fn(a, b) {
if a.0 < b.0 {
-1
} else if a.0 > b.0 {
1
} else {
0
}
})
entries
}