katie/http-syslog-transformer

parses and forwards Nginx syslog data to VictoriaLogs

Katie KlossLucy!4d2d49f

main
5.0 KiB218 linesraw
1import gleam/bit_array
2import gleam/int
3import gleam/list
4import gleam/option.{Some}
5import gleam/order
6import gleam/regexp
7import gleam/result
8import gleam/string
9import gleam/time/calendar
10import gleam/time/duration
11import gleam/time/timestamp
12
13const all_months = [
14  #("Jan", calendar.January),
15  #("Feb", calendar.February),
16  #("Mar", calendar.March),
17  #("Apr", calendar.April),
18  #("May", calendar.May),
19  #("Jun", calendar.June),
20  #("Jul", calendar.July),
21  #("Aug", calendar.August),
22  #("Sep", calendar.September),
23  #("Oct", calendar.October),
24  #("Nov", calendar.November),
25  #("Dec", calendar.December),
26]
27
28pub type SyslogLine {
29  SyslogLine(host: String, tag: String, body: String)
30}
31
32pub type NginxLine {
33  NginxLine(
34    tag: String,
35    hostname: String,
36    remote_addr: String,
37    remote_user: String,
38    time: timestamp.Timestamp,
39    method: String,
40    uri: String,
41    version: String,
42    response_code: Int,
43    size: Int,
44    referer: String,
45    user_agent: String,
46  )
47}
48
49pub type ParseError {
50  NotSyslogData
51  NotNginxData
52}
53
54pub opaque type Parser {
55  Parser(syslog_regex: regexp.Regexp, nginx_regex: regexp.Regexp)
56}
57
58pub fn new() -> Parser {
59  let assert Ok(syslog_regex) =
60    regexp.from_string(
61      "^<\\d+>([A-Z][a-z][a-z]\\s{1,2}\\d{1,2}\\s\\d{2}[:]\\d{2}[:]\\d{2})\\s([\\w][\\w\\d\\.@-]*)\\s(\\w+)\\W\\s(.*)",
62    )
63
64  let assert Ok(nginx_regex) =
65    regexp.from_string(
66      "^([a-f0-9:.]+) - ([^ ]+) \\[([^\\]]+)\\] \"([^ ]+) ([^ ]+) HTTP\\/([\\d.]+)\" (\\d{3}) (\\d+) \"([^\"]+)\" \"([^\"]*)\"$",
67    )
68
69  Parser(syslog_regex, nginx_regex)
70}
71
72pub fn parse_data(
73  parser: Parser,
74  data: BitArray,
75) -> Result(NginxLine, ParseError) {
76  data
77  |> bit_array.to_string
78  |> result.replace_error(NotSyslogData)
79  |> result.try(fn(s) { parse_line(parser, s) })
80}
81
82fn parse_line(parser: Parser, line: String) -> Result(NginxLine, ParseError) {
83  use syslog_line <- result.try(get_syslog(parser, line))
84  get_nginx(parser, syslog_line)
85}
86
87fn get_syslog(parser: Parser, data: String) -> Result(SyslogLine, ParseError) {
88  case regexp.scan(parser.syslog_regex, data) {
89    [regexp.Match(submatches: [_, Some(host), Some(tag), Some(body)], ..), ..] ->
90      Ok(SyslogLine(host, tag, body))
91    _ -> Error(NotSyslogData)
92  }
93}
94
95fn get_nginx(
96  parser: Parser,
97  data: SyslogLine,
98) -> Result(NginxLine, ParseError) {
99  case regexp.scan(parser.nginx_regex, data.body) {
100    [
101      regexp.Match(
102        submatches: [
103          Some(remote_addr),
104          Some(remote_user),
105          Some(time),
106          Some(method),
107          Some(uri),
108          Some(version),
109          Some(code),
110          Some(size),
111          Some(referer),
112          Some(user_agent),
113        ],
114        ..,
115      ),
116      ..
117    ] -> {
118      use time <- result.try(
119        parse_time(time) |> result.replace_error(NotNginxData),
120      )
121
122      Ok(NginxLine(
123        data.tag,
124        data.host,
125        remote_addr,
126        remote_user,
127        time,
128        method,
129        uri,
130        version,
131        int.parse(code) |> result.unwrap(0),
132        int.parse(size) |> result.unwrap(0),
133        referer,
134        user_agent,
135      ))
136    }
137    _ -> Error(NotNginxData)
138  }
139}
140
141pub fn parse_time(time: String) -> Result(timestamp.Timestamp, Nil) {
142  // this is slow make it 🚀
143  case time |> string.to_graphemes {
144    [] -> Error(Nil)
145    [
146      day_a,
147      day_b,
148      _,
149      month_a,
150      month_b,
151      month_c,
152      _,
153      year_a,
154      year_b,
155      year_c,
156      year_d,
157      _,
158      hour_a,
159      hour_b,
160      _,
161      minute_a,
162      minute_b,
163      _,
164      second_a,
165      second_b,
166      _,
167      offset_dir,
168      offset_a,
169      offset_b,
170      offset_c,
171      offset_d,
172      ..
173    ] -> {
174      let month_from_abbr = fn(abbr) {
175        all_months
176        |> list.find(fn(m) { string.compare(abbr, m.0) == order.Eq })
177      }
178
179      let month = month_a <> month_b <> month_c
180      use month <- result.try(month_from_abbr(month))
181      let month = month.1
182
183      let offset_sign = fn(offset: duration.Duration, offset_dir: String) -> Result(
184        duration.Duration,
185        Nil,
186      ) {
187        case offset_dir {
188          "+" -> Ok(offset)
189          "-" -> Ok(duration.subtract(duration.seconds(0), offset))
190          _ -> Error(Nil)
191        }
192      }
193
194      use year <- result.try(int.parse(year_a <> year_b <> year_c <> year_d))
195      use day <- result.try(int.parse(day_a <> day_b))
196
197      use hour <- result.try(int.parse(hour_a <> hour_b))
198      use minute <- result.try(int.parse(minute_a <> minute_b))
199      use second <- result.try(int.parse(second_a <> second_b))
200      use offset <- result.try(int.parse(
201        offset_a <> offset_b <> offset_c <> offset_d,
202      ))
203      let offset =
204        duration.add(
205          duration.hours(offset / 100),
206          duration.minutes(offset % 100),
207        )
208      use offset <- result.try(offset_sign(offset, offset_dir))
209
210      Ok(timestamp.from_calendar(
211        date: calendar.Date(year, month, day),
212        time: calendar.TimeOfDay(hour, minute, second, 0),
213        offset: offset,
214      ))
215    }
216    _ -> Error(Nil)
217  }
218}