katie/http-syslog-transformer
parses and forwards Nginx syslog data to VictoriaLogs
4d2d49f
main
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 { 29SyslogLine ( host : String , tag : String , body : String ) 30} 31 32pub type NginxLine { 33NginxLine ( 34tag : String , 35hostname : String , 36remote_addr : String , 37remote_user : String , 38time : timestamp . Timestamp , 39method : String , 40uri : String , 41version : String , 42response_code : Int , 43size : Int , 44referer : String , 45user_agent : String , 46) 47} 48 49pub type ParseError { 50NotSyslogData 51NotNginxData 52} 53 54pub opaque type Parser { 55Parser ( syslog_regex : regexp . Regexp , nginx_regex : regexp . Regexp ) 56} 57 58pub fn new () -> Parser { 59let assert Ok ( syslog_regex ) = 60regexp . 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 64let assert Ok ( nginx_regex ) = 65regexp . from_string ( 66"^([a-f0-9:.]+) - ([^ ]+) \\[([^\\]]+)\\] \"([^ ]+) ([^ ]+) HTTP\\/([\\d.]+)\" (\\d{3}) (\\d+) \"([^\"]+)\" \"([^\"]*)\"$" , 67) 68 69Parser ( syslog_regex , nginx_regex ) 70} 71 72pub fn parse_data ( 73parser : Parser , 74data : BitArray , 75) -> Result ( NginxLine , ParseError ) { 76data 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 ) { 83use syslog_line <- result . try ( get_syslog ( parser , line )) 84get_nginx ( parser , syslog_line ) 85} 86 87fn get_syslog ( parser : Parser , data : String ) -> Result ( SyslogLine , ParseError ) { 88case regexp . scan ( parser . syslog_regex , data ) { 89[ regexp . Match ( submatches : [ _ , Some ( host ), Some ( tag ), Some ( body )], ..), ..] -> 90Ok ( SyslogLine ( host , tag , body )) 91_ -> Error ( NotSyslogData ) 92} 93} 94 95fn get_nginx ( 96parser : Parser , 97data : SyslogLine , 98) -> Result ( NginxLine , ParseError ) { 99case regexp . scan ( parser . nginx_regex , data . body ) { 100[ 101regexp . Match ( 102submatches : [ 103Some ( remote_addr ), 104Some ( remote_user ), 105Some ( time ), 106Some ( method ), 107Some ( uri ), 108Some ( version ), 109Some ( code ), 110Some ( size ), 111Some ( referer ), 112Some ( user_agent ), 113], 114.., 115), 116.. 117] -> { 118use time <- result . try ( 119parse_time ( time ) |> result . replace_error ( NotNginxData ), 120) 121 122Ok ( NginxLine ( 123data . tag , 124data . host , 125remote_addr , 126remote_user , 127time , 128method , 129uri , 130version , 131int . parse ( code ) |> result . unwrap ( 0 ), 132int . parse ( size ) |> result . unwrap ( 0 ), 133referer , 134user_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 🚀 143case time |> string . to_graphemes { 144[] -> Error ( Nil ) 145[ 146day_a , 147day_b , 148_ , 149month_a , 150month_b , 151month_c , 152_ , 153year_a , 154year_b , 155year_c , 156year_d , 157_ , 158hour_a , 159hour_b , 160_ , 161minute_a , 162minute_b , 163_ , 164second_a , 165second_b , 166_ , 167offset_dir , 168offset_a , 169offset_b , 170offset_c , 171offset_d , 172.. 173] -> { 174let month_from_abbr = fn ( abbr ) { 175all_months 176|> list . find ( fn ( m ) { string . compare ( abbr , m . 0 ) == order . Eq }) 177} 178 179let month = month_a <> month_b <> month_c 180use month <- result . try ( month_from_abbr ( month )) 181let month = month . 1 182 183let offset_sign = fn ( offset : duration . Duration , offset_dir : String ) -> Result ( 184duration . Duration , 185Nil , 186) { 187case offset_dir { 188"+" -> Ok ( offset ) 189"-" -> Ok ( duration . subtract ( duration . seconds ( 0 ), offset )) 190_ -> Error ( Nil ) 191} 192} 193 194use year <- result . try ( int . parse ( year_a <> year_b <> year_c <> year_d )) 195use day <- result . try ( int . parse ( day_a <> day_b )) 196 197use hour <- result . try ( int . parse ( hour_a <> hour_b )) 198use minute <- result . try ( int . parse ( minute_a <> minute_b )) 199use second <- result . try ( int . parse ( second_a <> second_b )) 200use offset <- result . try ( int . parse ( 201offset_a <> offset_b <> offset_c <> offset_d , 202)) 203let offset = 204duration . add ( 205duration . hours ( offset / 100 ), 206duration . minutes ( offset % 100 ), 207) 208use offset <- result . try ( offset_sign ( offset , offset_dir )) 209 210Ok ( timestamp . from_calendar ( 211date : calendar . Date ( year , month , day ), 212time : calendar . TimeOfDay ( hour , minute , second , 0 ), 213offset : offset , 214)) 215} 216_ -> Error ( Nil ) 217} 218}