blob: 034c388958d5e3a5795137eb2c2d8d8abf0537a4 [file]
#!@pythonbin@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# logresolve.py -- Python port of the historical Perl logresolve.pl.
#
# usage: logresolve.py <infile >outfile
#
# input = Apache/NCSA/.. logfile with IP numbers at start of lines
# output = same logfile with IP addresses resolved to hostnames where
# name lookups succeeded.
#
# this differs from the C based 'logresolve' in that this script
# resolves a number (CHILDREN) of addresses concurrently and sets a
# short timeout (TIMEOUT) for each lookup in order to keep things moving
# quickly.
#
# The original Perl version forked CHILDREN subprocesses and exchanged
# IPs/hostnames over Unix sockets, with a per-lookup alarm(TIMEOUT).
# This Python port reproduces the *semantics* without the fork/socket
# IPC machinery: a concurrent.futures.ThreadPoolExecutor with
# max_workers=CHILDREN performs up to CHILDREN reverse lookups in
# parallel, and the per-lookup timeout is enforced via
# socket.setdefaulttimeout(TIMEOUT) (socket.gethostbyaddr() honours the
# process default socket timeout; it has no timeout argument of its own).
#
# Results are cached in a dict so each unique IP is resolved only once,
# and output lines are emitted in the exact same order as the input
# (logfiles must stay in order).
#
# Concurrency / timeout / order-preservation:
# * Concurrency: ThreadPoolExecutor(max_workers=CHILDREN). Each unique
# IP is submitted exactly once; the cache dict guards against
# resolving the same IP twice.
# * Timeout: socket.setdefaulttimeout(TIMEOUT) bounds each DNS lookup.
# As a belt-and-braces measure the worker also bounds itself via
# future.result(timeout=...) when collecting results.
# * Order: we buffer all input lines (remembering each line's leading
# IP), resolve the unique IPs concurrently, then walk the buffered
# lines in their original order substituting the cached hostname.
import socket
import sys
from concurrent.futures import ThreadPoolExecutor
CHILDREN = 40
TIMEOUT = 5
def nslookup(ip):
"""Reverse-resolve an IP to a hostname.
Equivalent to the Perl gethostbyaddr(gethostbyname($ip), AF_INET).
Returns the resolved hostname, or the original IP on any failure or
timeout (matching the Perl behaviour of leaving the IP as-is).
"""
try:
hostname = socket.gethostbyaddr(ip)[0]
except Exception:
return ip
return hostname if hostname else ip
def main():
# Bound every DNS lookup to TIMEOUT seconds. socket.gethostbyaddr()
# has no timeout argument, so we rely on the process-wide default
# socket timeout, which it honours.
socket.setdefaulttimeout(TIMEOUT)
# Read the whole logfile, buffering each line and the IP that starts
# it. Order is preserved by replaying this buffer at the end.
lines = [] # list of (ip, rest, had_space); rest keeps its newline
unique_ips = [] # unique IPs in first-seen order (for stable submit)
seen = set()
for line in sys.stdin:
# split on the FIRST space only; a line with no space is all IP.
parts = line.split(' ', 1)
if len(parts) > 1:
ip = parts[0]
rest = parts[1]
had_space = True
else:
# No space: the whole line is the IP. Strip the trailing
# newline so it resolves cleanly, but remember the line
# ending so we can reproduce it verbatim on output.
stripped = line.rstrip('\n')
ip = stripped
rest = line[len(stripped):]
had_space = False
lines.append((ip, rest, had_space))
if ip not in seen:
seen.add(ip)
unique_ips.append(ip)
# Resolve all unique IPs concurrently, up to CHILDREN at a time.
cache = {}
if unique_ips:
with ThreadPoolExecutor(max_workers=CHILDREN) as pool:
futures = {ip: pool.submit(nslookup, ip) for ip in unique_ips}
for ip, fut in futures.items():
try:
cache[ip] = fut.result(timeout=TIMEOUT + 1)
except Exception:
# On timeout/failure leave the IP unchanged.
cache[ip] = ip
# Emit lines in the original input order, substituting hostnames.
out = sys.stdout
for ip, rest, had_space in lines:
host = cache.get(ip, ip)
if had_space:
out.write("%s %s" % (host, rest))
else:
# No space in the original line: emit host then the original
# line ending (rest is "\n", "" with EOF no-newline, etc.).
out.write(host)
if rest:
out.write(rest)
if __name__ == "__main__":
main()