local input = arg[1] or '/var/log/tutorial-access.log' local output = arg[2] or '/var/log/access-summary.txt' local function fail(message) io.stderr:write(message, '\n') os.exit(1) end if input == output then fail('Input and output must be different files') end local source, err = io.open(input, 'r') if not source then fail('Cannot read ' .. input .. ': ' .. tostring(err)) end local requests, skipped, bytes, errors4, errors5 = 0, 0, 0, 0, 0 local statuses, paths = {}, {} for line in source:lines() do if line:match('%S') then local request, code, size = line:match('^%S+%s+%S+%s+%S+%s+%b[]%s+"([^"]*)"%s+(%d%d%d)%s+(%S+)') local method, path = (request or ''):match('^([A-Z]+)%s+(%S+)%s+HTTP/%d+%.%d+$') local status = tonumber(code) if method and status and status >= 100 and status <= 599 and (size == '-' or size:match('^%d+$')) then requests = requests + 1 bytes = bytes + (tonumber(size) or 0) statuses[code] = (statuses[code] or 0) + 1 paths[path] = (paths[path] or 0) + 1 if status >= 400 and status < 500 then errors4 = errors4 + 1 end if status >= 500 then errors5 = errors5 + 1 end else skipped = skipped + 1 end end end source:close() local lines = { 'Source: ' .. input, 'Generated: ' .. os.date('!%Y-%m-%d %H:%M:%S UTC'), 'Requests: ' .. requests, 'Skipped: ' .. skipped, 'Response bytes: ' .. bytes, '4xx: ' .. errors4, '5xx: ' .. errors5, '', 'Statuses:' } local codes = {} for code in pairs(statuses) do codes[#codes + 1] = code end table.sort(codes) for _, code in ipairs(codes) do lines[#lines + 1] = code .. ': ' .. statuses[code] end lines[#lines + 1] = '' lines[#lines + 1] = 'Top paths:' local ranked = {} for path, count in pairs(paths) do ranked[#ranked + 1] = {path = path, count = count} end table.sort(ranked, function(a, b) if a.count ~= b.count then return a.count > b.count end return a.path < b.path end) for i = 1, math.min(5, #ranked) do lines[#lines + 1] = ranked[i].count .. ' ' .. ranked[i].path end local temporary = output .. '.tmp' local report, open_err = io.open(temporary, 'w') if not report then fail('Cannot write ' .. temporary .. ': ' .. tostring(open_err)) end local written, write_err = report:write(table.concat(lines, '\n'), '\n') local closed, close_err = report:close() if not written or not closed then os.remove(temporary) fail('Cannot save report: ' .. tostring(write_err or close_err)) end local renamed, rename_err = os.rename(temporary, output) if not renamed then os.remove(temporary) fail('Cannot replace ' .. output .. ': ' .. tostring(rename_err)) end print('Saved: ' .. output)