Skip to content

Commit 90237da

Browse files
committed
[reports] add report-access-log.lnav
1 parent cc2e997 commit 90237da

File tree

40 files changed

+533
-330
lines changed

40 files changed

+533
-330
lines changed

NEWS.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@ Features:
44
* Log message timestamps are now represented with microsecond
55
precision internally instead of just millisecond.
66
* The `log_time` and `log_level` fields can now be hidden.
7+
* Added a `report-access-log` script that generates a report that
8+
is similar to the output of the [goaccess](https://goaccess.io)
9+
utility.
10+
11+
Interface changes:
12+
* DB query results that start with a number are right justified
13+
instead of only full numbers.
14+
15+
Breaking changes:
16+
* The `parse_url()` SQL function no longer raises an error for an
17+
invalid URL.
18+
Instead, it will return a JSON object with an object with the
19+
following properties:
20+
- `error` - An identifier for the error.
21+
- `url` - The invalid URL itself.
22+
- `reason` - A description of the error.
723

824
Bug Fixes:
925
* Improved startup time.

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ set(BUILTIN_LNAV_SCRIPTS
270270
scripts/partition-by-boot.lnav
271271
scripts/piper-url-handler.lnav
272272
scripts/rename-stdin.lnav
273+
scripts/report-access-log.lnav
273274
scripts/search-for.lnav
274275
)
275276

src/db_sub_source.cc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,14 @@ db_label_source::text_value_for_line(textview_curses& tc,
6868
auto cell_length
6969
= utf8_string_length(cell_str).unwrapOr(actual_col_size);
7070
auto padding = actual_col_size - cell_length;
71+
auto rjust = cell_length > 0 && isdigit(cell_str[0]);
7172
this->dls_cell_width[lpc] = cell_str.length() + padding;
72-
if (this->dls_headers[lpc].hm_column_type != SQLITE3_TEXT) {
73+
if (rjust) {
7374
label_out.append(padding, ' ');
7475
}
7576
shift_string_attrs(cell_attrs, 0, label_out.size());
7677
label_out.append(cell_str);
77-
if (this->dls_headers[lpc].hm_column_type == SQLITE3_TEXT) {
78+
if (!rjust) {
7879
label_out.append(padding, ' ');
7980
}
8081
label_out.append(1, ' ');

src/internals/sql-ref.rst

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -334,10 +334,10 @@ SELECT *result-column* FROM *table* WHERE *\[cond\]* GROUP BY *grouping-expr* OR
334334

335335
;SELECT * FROM lnav_example_log
336336
log_line log_part log_time log_actual_time log_idle_msecs log_level log_mark log_comment log_tags log_filters ex_procname ex_duration log_time_msecs log_path log_text log_body
337-
0 <NULL> 2017-02⋯:06.100 2017-02⋯:06.100 0 info 0 <NULL> <NULL> <NULL> hw 2 1486094706000 /tmp/log 2017-02⋯ World! Hello, World!
338-
1 <NULL> 2017-02⋯:06.200 2017-02⋯:06.200 100 error 0 <NULL> <NULL> <NULL> gw 4 1486094706000 /tmp/log 2017-02⋯ World! Goodbye, World!
339-
2 new 2017-02⋯:06.200 2017-02⋯:06.200 1200000 warn 0 <NULL> <NULL> <NULL> gw 1 1486095906000 /tmp/log 2017-02⋯ World! Goodbye, World!
340-
3 new 2017-02⋯:06.200 2017-02⋯:06.200 1800000 debug 0 <NULL> <NULL> <NULL> gw 10 1486097706000 /tmp/log 2017-02⋯ World! Goodbye, World!
337+
0 <NULL> 2017-02⋯:06.100 2017-02⋯:06.100 0 info 0 <NULL> <NULL> <NULL> hw 2 1486094706000 /tmp/log 2017-02⋯ World! Hello, World!
338+
1 <NULL> 2017-02⋯:06.200 2017-02⋯:06.200 100 error 0 <NULL> <NULL> <NULL> gw 4 1486094706000 /tmp/log 2017-02⋯ World! Goodbye, World!
339+
2 new 2017-02⋯:06.200 2017-02⋯:06.200 1200000 warn 0 <NULL> <NULL> <NULL> gw 1 1486095906000 /tmp/log 2017-02⋯ World! Goodbye, World!
340+
3 new 2017-02⋯:06.200 2017-02⋯:06.200 1800000 debug 0 <NULL> <NULL> <NULL> gw 10 1486097706000 /tmp/log 2017-02⋯ World! Goodbye, World!
341341

342342

343343
----
@@ -1960,7 +1960,7 @@ json_each(*X*, *\[P\]*)
19601960
;SELECT * FROM json_each('[null,1,"two",{"three":4.5}]')
19611961
key value type atom id parent fullkey path
19621962
0 <NULL> null <NULL> 2 <NULL> $[0] $
1963-
1 1 integer 1 3 <NULL> $[1] $
1963+
1 1 integer 1 3 <NULL> $[1] $
19641964
2 two text two 5 <NULL> $[2] $
19651965
3 {"three":4.5} object <NULL> 9 <NULL> $[3] $
19661966

@@ -2303,11 +2303,11 @@ json_tree(*X*, *\[P\]*)
23032303
;SELECT key,value,type,atom,fullkey,path FROM json_tree('[null,1,"two",{"three":4.5}]')
23042304
key value type atom fullkey path
23052305
<NULL> [null,1⋯":4.5}] array <NULL> $ $
2306-
0 <NULL> null <NULL> $[0] $
2307-
1 1 integer 1 $[1] $
2308-
2 two text two $[2] $
2309-
3 {"three":4.5} object <NULL> $[3] $
2310-
three 4.5 real 4.5 $[3].three $[3]
2306+
0 <NULL> null <NULL> $[0] $
2307+
1 1 integer 1 $[1] $
2308+
2 two text two $[2] $
2309+
3 {"three":4.5} object <NULL> $[3] $
2310+
three 4.5 real 4.5 $[3].three $[3]
23112311

23122312
**See Also**
23132313
:ref:`jget`, :ref:`json_array_length`, :ref:`json_array`, :ref:`json_concat`, :ref:`json_contains`, :ref:`json_each`, :ref:`json_extract`, :ref:`json_group_array`, :ref:`json_group_object`, :ref:`json_insert`, :ref:`json_object`, :ref:`json_quote`, :ref:`json_remove`, :ref:`json_replace`, :ref:`json_set`, :ref:`json_type`, :ref:`json_valid`, :ref:`json`, :ref:`yaml_to_json`
@@ -3401,12 +3401,12 @@ regexp_capture(*string*, *pattern*)
34013401

34023402
;SELECT * FROM regexp_capture('a=1; b=2', '(\w+)=(\d+)')
34033403
match_index capture_index capture_name capture_count range_start range_stop content
3404-
0 0 <NULL> 3 1 4 a=1
3405-
0 1 <NULL> 3 1 2 a
3406-
0 2 <NULL> 3 3 4 1
3407-
1 0 <NULL> 3 6 9 b=2
3408-
1 1 <NULL> 3 6 7 b
3409-
1 2 <NULL> 3 8 9 2
3404+
0 0 <NULL> 3 1 4 a=1
3405+
0 1 <NULL> 3 1 2 a
3406+
0 2 <NULL> 3 3 4 1
3407+
1 0 <NULL> 3 6 9 b=2
3408+
1 1 <NULL> 3 6 7 b
3409+
1 2 <NULL> 3 8 9 2
34103410

34113411
**See Also**
34123412
:ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`humanize_id`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`pretty_print`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`timezone`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
@@ -3840,28 +3840,28 @@ spooky_hash(*str*)
38403840
.. code-block:: custsqlite
38413841

38423842
;SELECT spooky_hash('Hello, World!')
3843-
0b1d52cc5427db4c6a9eed9d3e5700f4
3843+
0b1d52cc-5427-db4c-6a9e-ed9d3e5700f4
38443844

38453845
To produce a hash for the parameters where one is NULL:
38463846

38473847
.. code-block:: custsqlite
38483848

38493849
;SELECT spooky_hash('Hello, World!', NULL)
3850-
c96ee75d48e6ea444fee8af948f6da25
3850+
c96ee75d-48e6-ea44-4fee-8af948f6da25
38513851

38523852
To produce a hash for the parameters where one is an empty string:
38533853

38543854
.. code-block:: custsqlite
38553855

38563856
;SELECT spooky_hash('Hello, World!', '')
3857-
c96ee75d48e6ea444fee8af948f6da25
3857+
c96ee75d-48e6-ea44-4fee-8af948f6da25
38583858

38593859
To produce a hash for the parameters where one is a number:
38603860

38613861
.. code-block:: custsqlite
38623862

38633863
;SELECT spooky_hash('Hello, World!', 123)
3864-
f96b3d9c1a19f4394c97a1b79b1880df
3864+
f96b3d9c-1a19-f439-4c97-a1b79b1880df
38653865

38663866
**See Also**
38673867
:ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`humanize_id`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`pretty_print`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`timezone`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`

src/lnav_commands.cc

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
#include "db_sub_source.hh"
6161
#include "external_opener.hh"
6262
#include "field_overlay_source.hh"
63+
#include "fmt/color.h"
6364
#include "fmt/printf.h"
6465
#include "hasher.hh"
6566
#include "itertools.similar.hh"
@@ -1667,9 +1668,11 @@ com_save_to(exec_context& ec,
16671668

16681669
for (const auto& hdr : dls.dls_headers) {
16691670
auto centered_hdr = center_str(hdr.hm_name, hdr.hm_column_size);
1671+
auto style = fmt::text_style{};
16701672

16711673
fprintf(outfile, "\u2503");
1672-
fprintf(outfile, "%s", centered_hdr.c_str());
1674+
style |= fmt::emphasis::bold;
1675+
fmt::print(outfile, style, FMT_STRING("{}"), centered_hdr);
16731676
}
16741677
fprintf(outfile, "\u2503\n");
16751678

@@ -1703,12 +1706,13 @@ com_save_to(exec_context& ec,
17031706
auto cell_length
17041707
= utf8_string_length(cell).unwrapOr(cell.size());
17051708
auto padding = anonymize ? 1 : hdr.hm_column_size - cell_length;
1709+
auto rjust = cell_length > 0 && isdigit(cell[0]);
17061710

1707-
if (hdr.hm_column_type != SQLITE3_TEXT) {
1711+
if (rjust) {
17081712
fprintf(outfile, "%s", std::string(padding, ' ').c_str());
17091713
}
17101714
fprintf(outfile, "%s", cell.c_str());
1711-
if (hdr.hm_column_type == SQLITE3_TEXT) {
1715+
if (!rjust) {
17121716
fprintf(outfile, "%s", std::string(padding, ' ').c_str());
17131717
}
17141718
}

src/scripts/report-access-log.lnav

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#
2+
# @synopsis: report-access-log
3+
# @description: Generate a report of the access_log table
4+
#
5+
6+
;DROP TABLE IF EXISTS report_access_log
7+
;CREATE TABLE report_access_log AS
8+
SELECT
9+
log_time_msecs,
10+
c_ip,
11+
cs_method,
12+
cs_uri_stem,
13+
cs_version,
14+
sc_status,
15+
sc_bytes,
16+
CASE cs_referer
17+
WHEN '-' THEN NULL
18+
ELSE jget(parse_url(cs_referer), '/host')
19+
END AS cs_referer,
20+
cs_user_agent
21+
FROM access_log
22+
23+
;SELECT count(*) AS total_requests,
24+
count(DISTINCT spooky_hash(c_ip, cs_user_agent)) AS unique_visitors,
25+
count(DISTINCT cs_uri_stem) AS requested_files,
26+
humanize_file_size(sum(sc_bytes)) AS tx_amount
27+
FROM report_access_log
28+
29+
;SELECT count(*) AS not_found
30+
FROM (SELECT 1 FROM report_access_log
31+
WHERE sc_status = 404
32+
GROUP BY cs_uri_stem)
33+
34+
:echo Total Requests ${total_requests} Unique Visitors ${unique_visitors} Requested Files ${requested_files}
35+
:echo Not Found ${not_found} Tx. Amount ${tx_amount}
36+
37+
;SELECT
38+
count(*) AS Hits,
39+
printf('%.2f%%', 100.0 * count(*) / $total_requests) AS "h%",
40+
count(DISTINCT c_ip) AS Vis,
41+
humanize_file_size(sum(sc_bytes)) AS "Tx. Amount",
42+
strftime('%F', min(log_time_msecs) / 1000, 'unixepoch') AS Date
43+
FROM report_access_log
44+
GROUP BY timeslice(log_time_msecs, '1d')
45+
ORDER BY Hits DESC
46+
LIMIT 10
47+
:write-cols-to -
48+
49+
;SELECT
50+
count(*) AS Hits,
51+
printf('%.2f%%', 100.0 * count(*) / $total_requests) AS "h%",
52+
count(DISTINCT c_ip) AS Vis,
53+
humanize_file_size(sum(sc_bytes)) AS "Tx. Amount",
54+
cs_method AS Mtd,
55+
cs_version AS Proto,
56+
cs_uri_stem AS URIs
57+
FROM report_access_log
58+
GROUP BY cs_uri_stem, cs_method
59+
ORDER BY Hits DESC
60+
LIMIT 10
61+
:write-cols-to -
62+
63+
;SELECT
64+
count(*) AS Hits,
65+
printf('%.2f%%', 100.0 * count(*) / $total_requests) AS "h%",
66+
count(DISTINCT c_ip) AS Vis,
67+
humanize_file_size(sum(sc_bytes)) AS "Tx. Amount",
68+
cs_method AS Mtd,
69+
cs_version AS Proto,
70+
cs_uri_stem AS URIs
71+
FROM report_access_log
72+
WHERE sc_status = 404
73+
GROUP BY cs_uri_stem, cs_method
74+
ORDER BY Hits DESC
75+
LIMIT 10
76+
:write-cols-to -
77+
78+
;SELECT
79+
count(*) AS Hits,
80+
printf('%.2f%%', 100.0 * count(*) / $total_requests) AS "h%",
81+
count(DISTINCT c_ip) AS Vis,
82+
humanize_file_size(sum(sc_bytes)) AS "Tx. Amount",
83+
c_ip AS Host
84+
FROM report_access_log
85+
GROUP BY c_ip
86+
ORDER BY Hits DESC
87+
LIMIT 10
88+
:write-cols-to -
89+
90+
;SELECT
91+
count(*) AS Hits,
92+
printf('%.2f%%', 100.0 * count(*) / $total_requests) AS "h%",
93+
count(DISTINCT c_ip) AS Vis,
94+
humanize_file_size(sum(sc_bytes)) AS "Tx. Amount",
95+
cs_referer AS Referer
96+
FROM report_access_log
97+
WHERE cs_referer IS NOT NULL
98+
GROUP BY cs_referer
99+
ORDER BY Hits DESC
100+
LIMIT 10
101+
:write-cols-to -
102+
103+
;SELECT
104+
count(*) AS Hits,
105+
printf('%.2f%%', 100.0 * count(*) / $total_requests) AS "h%",
106+
count(DISTINCT c_ip) AS Vis,
107+
humanize_file_size(sum(sc_bytes)) AS "Tx. Amount",
108+
printf('%sXX', sc_status / 100) AS Status
109+
FROM report_access_log
110+
GROUP BY sc_status / 100
111+
ORDER BY Hits DESC
112+
LIMIT 10
113+
:write-cols-to -
114+
115+
;DROP TABLE IF EXISTS report_access_log

src/scripts/scripts.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ BUILTIN_LNAVSCRIPTS = \
88
$(srcdir)/scripts/partition-by-boot.lnav \
99
$(srcdir)/scripts/piper-url-handler.lnav \
1010
$(srcdir)/scripts/rename-stdin.lnav \
11+
$(srcdir)/scripts/report-access-log.lnav \
1112
$(srcdir)/scripts/search-for.lnav \
1213
$(srcdir)/scripts/zk-set-ops.lnav \
1314
$()

src/string-extension-functions.cc

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <string.h>
1919

2020
#include "base/humanize.hh"
21+
#include "base/is_utf8.hh"
2122
#include "base/lnav.gzip.hh"
2223
#include "base/string_util.hh"
2324
#include "column_namer.hh"
@@ -631,9 +632,21 @@ sql_parse_url(std::string url)
631632
auto rc = curl_url_set(
632633
cu, CURLUPART_URL, url.c_str(), CURLU_NON_SUPPORT_SCHEME);
633634
if (rc != CURLUE_OK) {
634-
throw lnav::console::user_message::error(
635-
attr_line_t("invalid URL: ").append(lnav::roles::file(url)))
636-
.with_reason(curl_url_strerror(rc));
635+
auto_mem<char> url_part(curl_free);
636+
yajlpp_gen gen;
637+
yajl_gen_config(gen, yajl_gen_beautify, false);
638+
639+
{
640+
yajlpp_map root(gen);
641+
root.gen("error");
642+
root.gen("invalid-url");
643+
root.gen("url");
644+
root.gen(url);
645+
root.gen("reason");
646+
root.gen(curl_url_strerror(rc));
647+
}
648+
649+
return json_string(gen);
637650
}
638651

639652
auto_mem<char> url_part(curl_free);
@@ -682,7 +695,18 @@ sql_parse_url(std::string url)
682695
root.gen("path");
683696
rc = curl_url_get(cu, CURLUPART_PATH, url_part.out(), CURLU_URLDECODE);
684697
if (rc == CURLUE_OK) {
685-
root.gen(string_fragment::from_c_str(url_part.in()));
698+
auto path_frag = string_fragment::from_c_str(url_part.in());
699+
auto path_utf_res = is_utf8(path_frag);
700+
if (path_utf_res.is_valid()) {
701+
root.gen(path_frag);
702+
} else {
703+
rc = curl_url_get(cu, CURLUPART_PATH, url_part.out(), 0);
704+
if (rc == CURLUE_OK) {
705+
root.gen(string_fragment::from_c_str(url_part.in()));
706+
} else {
707+
root.gen();
708+
}
709+
}
686710
} else {
687711
root.gen();
688712
}
@@ -721,12 +745,24 @@ sql_parse_url(std::string url)
721745
if (eq_index_opt) {
722746
auto key = kv_pair_frag.sub_range(0, eq_index_opt.value());
723747
auto val = kv_pair_frag.substr(eq_index_opt.value() + 1);
724-
auto key_str = key.to_string();
725748

726-
if (seen_keys.count(key_str) == 0) {
727-
seen_keys.emplace(key_str);
728-
query_map.gen(key);
729-
query_map.gen(val);
749+
auto key_utf_res = is_utf8(key);
750+
auto val_utf_res = is_utf8(val);
751+
if (key_utf_res.is_valid()) {
752+
auto key_str = key.to_string();
753+
754+
if (seen_keys.count(key_str) == 0) {
755+
seen_keys.emplace(key_str);
756+
query_map.gen(key);
757+
if (val_utf_res.is_valid()) {
758+
query_map.gen(val);
759+
} else {
760+
auto eq = strchr(kv_pair_encoded.data(), '=');
761+
query_map.gen(eq + 1);
762+
}
763+
}
764+
} else {
765+
730766
}
731767
} else {
732768
auto val_str = split_res.first.to_string();

src/vtab_module.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ sqlite3_error_to_user_message(sqlite3* db)
6565
return from_res.unwrap();
6666
}
6767

68+
log_error("unable to parse error message: %s", errmsg);
6869
return lnav::console::user_message::error("internal error")
6970
.with_reason(from_res.unwrapErr()[0].um_message.get_string());
7071
}

0 commit comments

Comments
 (0)