| #!@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() |