You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by ch...@apache.org on 2018/11/30 20:19:08 UTC
[4/7] qpid-dispatch git commit: DISPATCH-1199: move scraper tool to
tools/scraper directory
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b7ab3390/tools/scraper/amqp_detail.py
----------------------------------------------------------------------
diff --git a/tools/scraper/amqp_detail.py b/tools/scraper/amqp_detail.py
new file mode 100755
index 0000000..042a19b
--- /dev/null
+++ b/tools/scraper/amqp_detail.py
@@ -0,0 +1,633 @@
+#!/usr/bin/env python
+
+#
+# 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.
+#
+from __future__ import unicode_literals
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import print_function
+
+import sys
+import traceback
+
+import common
+import text
+
+"""
+Given a map of all connections with lists of the associated frames
+analyze and show per-connection, per-session, and per-link details.
+
+This is done in a two-step process:
+ * Run through the frame lists and generates an intermediate structure
+ with the the details for display.
+ * Generate the html from the detail structure.
+This strategy allows for a third step that would allow more details
+to be gleaned from the static details. For instance, if router A
+sends a transfer to router B then router A's details could show
+how long it took for the transfer to reach router B. Similarly
+router B's details could show how long ago router A sent the transfer.
+"""
+
+
+class ConnectionDetail():
+ """
+ Holds facts about sessions over the connection's lifetime
+ """
+
+ def __init__(self, id):
+ # id in form 'A_15':
+ # A is the router logfile key
+ # 15 is the log connection number [15]
+ self.id = id
+
+ # seq_no number differentiates items that otherwise have same identifiers.
+ # Sessions, for example: a given connection may have N distinct session
+ # with local channel 0.
+ self.seq_no = 0
+
+ # combined amqp_error frames on this connection
+ self.amqp_errors = 0
+
+ # session_list holds all SessionDetail records either active or retired
+ # Sessions for a connection are identified by the local channel number.
+ # There may be many sessions all using the same channel number.
+ # This list holds all of them.
+ self.session_list = []
+
+ # this map indexed by the channel refers to the current item in the session_list
+ self.chan_map = {}
+
+ # count of AMQP performatives for this connection that are not accounted
+ # properly in session and link processing.
+ # Server Accepting, SASL mechs, init, outcome, AMQP, and so on
+ self.unaccounted_frame_list = []
+
+ def FindSession(self, channel):
+ """
+ Find the current session by channel number
+ :param channel: the performative channel
+ :return: the session or None
+ """
+ return self.chan_map[channel] if channel in self.chan_map else None
+
+ def GetId(self):
+ return self.id
+
+ def GetSeqNo(self):
+ self.seq_no += 1
+ return str(self.seq_no)
+
+ def EndChannel(self, channel):
+ # take existing session out of connection chan map
+ if channel in self.chan_map:
+ del self.chan_map[channel]
+
+ def GetLinkEventCount(self):
+ c = 0
+ for session in self.session_list:
+ c += session.GetLinkEventCount()
+ return c
+
+
+class SessionDetail:
+ """
+ Holds facts about a session
+ """
+
+ def __init__(self, conn_detail, conn_seq, start_time):
+ # parent connection
+ self.conn_detail = conn_detail
+
+ # some seq number
+ self.conn_epoch = conn_seq
+
+ # Timing
+ self.time_start = start_time
+ self.time_end = start_time
+
+ self.amqp_errors = 0
+
+ self.channel = -1
+ self.peer_chan = -1
+
+ self.direction = ""
+
+ # seq_no number differentiates items that otherwise have same identifiers.
+ # links for example
+ self.seq_no = 0
+
+ self.log_line_list = []
+
+ # link_list holds LinkDetail records
+ # Links for a session are identified by a (handle, remote-handle) number pair.
+ # There may be many links all using the same handle pairs.
+ # This list holds all of them.
+ self.link_list = []
+
+ # link_list holds all links either active or retired
+ # this map indexed by the handle refers to the current item in the link_list
+ self.input_handle_link_map = {} # link created by peer
+ self.output_handle_link_map = {} # link created locally
+
+ # Link name in attach finds link details in link_list
+ # This map contains the link handle to disambiguate the name
+ self.link_name_to_detail_map = {}
+ #
+ # The map contains the pure link name and is used only to resolve name collisions
+ self.link_name_conflict_map = {}
+
+ # count of AMQP performatives for this connection that are not accounted
+ # properly in link processing
+ self.session_frame_list = []
+
+ # Session dispositions
+ # Sender/receiver dispositions may be sent or received
+ self.rx_rcvr_disposition_map = {} # key=delivery id, val=disposition plf
+ self.rx_sndr_disposition_map = {} # key=delivery id, val=disposition plf
+ self.tx_rcvr_disposition_map = {} # key=delivery id, val=disposition plf
+ self.tx_sndr_disposition_map = {} # key=delivery id, val=disposition plf
+
+ def FrameCount(self):
+ count = 0
+ for link in self.link_list:
+ count += len(link.frame_list)
+ count += len(self.session_frame_list)
+ return count
+
+ def FindLinkByName(self, attach_name, link_name_unambiguous, parsed_log_line):
+ # find conflicted name
+ cnl = None
+ if attach_name in self.link_name_conflict_map:
+ cnl = self.link_name_conflict_map[attach_name]
+ if cnl.input_handle == -1 and cnl.output_handle == -1:
+ cnl = None
+ # find non-conflicted name
+ nl = None
+ if link_name_unambiguous in self.link_name_to_detail_map:
+ nl = self.link_name_to_detail_map[link_name_unambiguous]
+ if nl.input_handle == -1 and nl.output_handle == -1:
+ nl = None
+ # report conflict
+ # TODO: There's an issue with this logic generating false positives
+ # if nl is None and (not cnl is None):
+ # parsed_log_line.data.amqp_error = True
+ # parsed_log_line.data.web_show_str += " <span style=\"background-color:yellow\">Link name conflict</span>"
+ # return unambiguous link
+ return nl
+
+ def FindLinkByHandle(self, handle, find_remote):
+ """
+ Find the current link by handle number
+ qualify lookup based on packet direction
+ :param link: the performative channel
+ :param dst_is_broker: packet direction
+ :return: the session or None
+ """
+ if find_remote:
+ return self.input_handle_link_map[handle] if handle in self.input_handle_link_map else None
+ else:
+ return self.output_handle_link_map[handle] if handle in self.output_handle_link_map else None
+
+ def GetId(self):
+ return self.conn_detail.GetId() + "_" + str(self.conn_epoch)
+
+ def GetSeqNo(self):
+ self.seq_no += 1
+ return self.seq_no
+
+ def DetachOutputHandle(self, handle):
+ # take existing link out of session handle map
+ if handle in self.output_handle_link_map:
+ nl = self.output_handle_link_map[handle]
+ del self.output_handle_link_map[handle]
+ nl.output_handle = -1
+
+ def DetachInputHandle(self, handle):
+ # take existing link out of session remote handle map
+ if handle in self.input_handle_link_map:
+ nl = self.input_handle_link_map[handle]
+ del self.input_handle_link_map[handle]
+ nl.input_handle = -1
+
+ def DetachHandle(self, handle, is_remote):
+ if is_remote:
+ self.DetachInputHandle(handle)
+ else:
+ self.DetachOutputHandle(handle)
+
+ def GetLinkEventCount(self):
+ c = 0
+ for link in self.link_list:
+ c += link.GetLinkEventCount()
+ return c
+
+
+class LinkDetail():
+ """
+ Holds facts about a link endpoint
+ This structure binds input and output links with same name
+ """
+
+ def __init__(self, session_detail, session_seq, link_name, start_time):
+ # parent session
+ self.session_detail = session_detail
+
+ # some seq number
+ self.session_seq = session_seq
+
+ # link name
+ self.name = link_name # plf.data.link_short_name
+ self.display_name = link_name # show short name; hover to see long name
+
+ # Timing
+ self.time_start = start_time
+ self.time_end = start_time
+
+ self.amqp_errors = 0
+
+ # paired handles
+ self.output_handle = -1
+ self.input_handle = -1
+
+ # link originator
+ self.direction = ""
+ self.is_receiver = True
+ self.first_address = ''
+
+ # set by sender
+ self.snd_settle_mode = ''
+ self.sender_target_address = "none"
+ self.sender_class = ''
+
+ # set by receiver
+ self.rcv_settle_mode = ''
+ self.receiver_source_address = "none"
+ self.receiver_class = ''
+
+ self.frame_list = []
+
+ def GetId(self):
+ return self.session_detail.GetId() + "_" + str(self.session_seq)
+
+ def FrameCount(self):
+ return len(self.frame_list)
+
+
+class AllDetails():
+ #
+ #
+ def format_errors(self, n_errors):
+ return ("<span style=\"background-color:yellow\">%d</span>" % n_errors) if n_errors > 0 else ""
+
+ def classify_connection(self, id):
+ """
+ Return probable connection class based on the kinds of links the connection uses.
+ TODO: This assumes that the connection has one session and one
+ :param id:
+ :return:
+ """
+ return "oops"
+
+ def time_offset(self, ttest, t0):
+ """
+ Return a string time delta between two datetime objects in seconds formatted
+ to six significant decimal places.
+ :param ttest:
+ :param t0:
+ :return:
+ """
+ delta = ttest - t0
+ t = float(delta.seconds) + float(delta.microseconds) / 1000000.0
+ return "%0.06f" % t
+
+ def links_in_connection(self, id):
+ conn_details = self.conn_details[id]
+ n_links = 0
+ for sess in conn_details.session_list:
+ n_links += len(sess.link_list)
+ return n_links
+
+ def settlement_display(self, transfer, disposition):
+ """
+ Generate the details for a disposition settlement
+ :param transfer: plf
+ :param disposition: plf
+ :return: display string
+ """
+ state = disposition.data.disposition_state # accept, reject, release, ...
+ if state != "accepted":
+ state = "<span style=\"background-color:orange\">%s</span>" % state
+ l2disp = "<a href=\"#%s\">%s</a>" % (disposition.fid, state)
+ sttld = "settled" if disposition.data.settled == "true" else "unsettled"
+ delay = self.time_offset(disposition.datetime, transfer.datetime)
+ return "(%s %s %s S)" % (l2disp, sttld, delay)
+
+ def resolve_settlement(self, link, transfer, rcv_disposition, snd_disposition):
+ """
+ Generate the settlement display string for this transfer.
+ :param link: linkDetails - holds settlement modes
+ :param transfer: plf of the transfer frame
+ :param rcv_disposition: plf of receiver role disposition
+ :param snd_disposition: plf of sender role disposition
+ :return: display string
+ """
+ if transfer.data.settled is not None and transfer.data.settled == "true":
+ result = "transfer presettled"
+ if rcv_disposition is not None:
+ sys.stderr.write("WARING: Receiver disposition for presettled message. connid:%s, line:%s\n" %
+ (rcv_disposition.data.conn_id, rcv_disposition.lineno))
+ if snd_disposition is not None:
+ sys.stderr.write("WARING: Sender disposition for presettled message. connid:%s, line:%s\n" %
+ (snd_disposition.data.conn_id, snd_disposition.lineno))
+ else:
+ if "1" in link.snd_settle_mode:
+ # link mode sends only settled transfers
+ result = "link presettled"
+ if rcv_disposition is not None:
+ sys.stderr.write("WARING: Receiver disposition for presettled link. connid:%s, line:%s\n" %
+ (rcv_disposition.data.conn_id, rcv_disposition.lineno))
+ if snd_disposition is not None:
+ sys.stderr.write("WARING: Sender disposition for presettled link. connid:%s, line:%s\n" %
+ (snd_disposition.data.conn_id, snd_disposition.lineno))
+ else:
+ # transfer unsettled and link mode requires settlement
+ if rcv_disposition is not None:
+ rtext = self.settlement_display(transfer, rcv_disposition)
+ transfer.data.final_disposition = rcv_disposition
+ if snd_disposition is not None:
+ stext = self.settlement_display(transfer, snd_disposition)
+ transfer.data.final_disposition = snd_disposition
+
+ if "0" in link.rcv_settle_mode:
+ # one settlement expected
+ if rcv_disposition is not None:
+ result = rtext
+ if snd_disposition is not None:
+ sys.stderr.write("WARING: Sender disposition for single first(0) settlement link. "
+ "connid:%s, line:%s\n" %
+ (snd_disposition.data.conn_id, snd_disposition.lineno))
+ else:
+ result = "rcvr: absent"
+ else:
+ # two settlements expected
+ if rcv_disposition is not None:
+ result = "rcvr: " + rtext
+ if snd_disposition is not None:
+ result += ", sndr: " + stext
+ else:
+ result += ", sndr: absent"
+ else:
+ result = "rcvr: absent"
+ if snd_disposition is not None:
+ result += ", sndr: " + stext
+ else:
+ result += ", sndr: absent"
+ return result
+
+ def __init__(self, _router, _common):
+ self.rtr = _router
+ self.comn = _common
+
+ # conn_details - AMQP analysis
+ # key= connection id '1', '2'
+ # val= ConnectionDetails
+ # for each connection, for each session, for each link:
+ # what happened
+ self.conn_details = {}
+
+ for conn in self.rtr.conn_list:
+ id = self.rtr.conn_id(conn)
+ self.conn_details[id] = ConnectionDetail(id)
+ conn_details = self.conn_details[id]
+ conn_frames = self.rtr.conn_to_frame_map[id]
+ for plf in conn_frames:
+ pname = plf.data.name
+ if plf.data.amqp_error:
+ conn_details.amqp_errors += 1
+ if pname in ['', 'open', 'close']:
+ conn_details.unaccounted_frame_list.append(plf)
+ continue
+ # session required
+ channel = plf.data.channel
+ sess_details = conn_details.FindSession(channel)
+ if sess_details == None:
+ sess_details = SessionDetail(conn_details, conn_details.GetSeqNo(), plf.datetime)
+ conn_details.session_list.append(sess_details)
+ conn_details.EndChannel(channel)
+ conn_details.chan_map[channel] = sess_details
+ sess_details.direction = plf.data.direction
+ sess_details.channel = channel
+ if plf.data.amqp_error:
+ sess_details.amqp_errors += 1
+
+ if pname in ['begin', 'end', 'disposition']:
+ sess_details.session_frame_list.append(plf)
+
+ elif pname in ['attach']:
+ handle = plf.data.handle # proton local handle
+ link_name = plf.data.link_short_name
+ link_name_unambiguous = link_name + "_" + str(handle)
+ error_was = plf.data.amqp_error
+ nl = sess_details.FindLinkByName(link_name, link_name_unambiguous, plf)
+ # if finding an ambiguous link name generated an error then propagate to session/connection
+ if not error_was and plf.data.amqp_error:
+ conn_details.amqp_errors += 1
+ sess_details.amqp_errors += 1
+ if nl is None:
+ # Creating a new link from scratch resulting in a half attached link pair
+ nl = LinkDetail(sess_details, sess_details.GetSeqNo(), link_name, plf.datetime)
+ sess_details.link_list.append(nl)
+ sess_details.link_name_to_detail_map[link_name_unambiguous] = nl
+ sess_details.link_name_conflict_map[link_name] = nl
+ nl.display_name = plf.data.link_short_name_popup
+ nl.direction = plf.data.direction
+ nl.is_receiver = plf.data.role == "receiver"
+ nl.first_address = plf.data.source if nl.is_receiver else plf.data.target
+ if plf.data.amqp_error:
+ nl.amqp_errors += 1
+
+ if plf.data.direction_is_in():
+ # peer is creating link
+ nl.input_handle = handle
+ sess_details.DetachInputHandle(handle)
+ sess_details.input_handle_link_map[handle] = nl
+ else:
+ # local is creating link
+ nl.output_handle = handle
+ sess_details.DetachOutputHandle(handle)
+ sess_details.output_handle_link_map[handle] = nl
+ if plf.data.is_receiver:
+ nl.rcv_settle_mode = plf.data.rcv_settle_mode
+ nl.receiver_source_address = plf.data.source
+ nl.receiver_class = plf.data.link_class
+ else:
+ nl.snd_settle_mode = plf.data.snd_settle_mode
+ nl.sender_target_address = plf.data.target
+ nl.sender_class = plf.data.link_class
+ nl.frame_list.append(plf)
+
+ elif pname in ['detach']:
+ ns = conn_details.FindSession(channel)
+ if ns is None:
+ conn_details.unaccounted_frame_list.append(plf)
+ continue
+ handle = plf.data.handle
+ nl = ns.FindLinkByHandle(handle, plf.data.direction_is_in())
+ ns.DetachHandle(handle, plf.data.direction_is_in())
+ if nl is None:
+ ns.session_frame_list.append(plf)
+ else:
+ if plf.data.amqp_error:
+ nl.amqp_errors += 1
+ nl.frame_list.append(plf)
+
+ elif pname in ['transfer', 'flow']:
+ ns = conn_details.FindSession(channel)
+ if ns is None:
+ conn_details.unaccounted_frame_list.append(plf)
+ continue
+ handle = plf.data.handle
+ nl = ns.FindLinkByHandle(handle, plf.data.direction_is_in())
+ if nl is None:
+ ns.session_frame_list.append(plf)
+ else:
+ if plf.data.amqp_error:
+ nl.amqp_errors += 1
+ nl.frame_list.append(plf)
+ # identify and index dispositions
+ for conn in self.rtr.conn_list:
+ id = self.rtr.conn_id(conn)
+ conn_detail = self.conn_details[id]
+ for sess in conn_detail.session_list:
+ # for each disposition add state to disposition_map
+ for splf in sess.session_frame_list:
+ if splf.data.name == "disposition":
+ if splf.data.direction == "<-":
+ sdispmap = sess.rx_rcvr_disposition_map if splf.data.is_receiver else sess.rx_sndr_disposition_map
+ else:
+ sdispmap = sess.tx_rcvr_disposition_map if splf.data.is_receiver else sess.tx_sndr_disposition_map
+ for sdid in range(int(splf.data.first), (int(splf.data.last) + 1)):
+ did = str(sdid)
+ if did in sdispmap:
+ sys.stderr.write("ERROR: Delivery ID collision in disposition map. connid:%s, \n" %
+ (splf.data.conn_id))
+ sdispmap[did] = splf
+
+ def show_html(self):
+ for conn in self.rtr.conn_list:
+ id = self.rtr.conn_id(conn)
+ conn_detail = self.rtr.details.conn_details[id]
+ conn_frames = self.rtr.conn_to_frame_map[id]
+ print("<a name=\"cd_%s\"></a>" % id)
+ # This lozenge shows/hides the connection's data
+ print("<a href=\"javascript:toggle_node('%s_data')\">%s%s</a>" %
+ (id, text.lozenge(), text.nbsp()))
+ dir = self.rtr.conn_dir[id] if id in self.rtr.conn_dir else ""
+ peer = self.rtr.conn_peer_display.get(id, "") # peer container id
+ peerconnid = self.comn.conn_peers_connid.get(id, "")
+ # show the connection title
+ print("%s %s %s %s (nFrames=%d) %s<br>" % \
+ (id, dir, peerconnid, peer, len(conn_frames), self.format_errors(conn_detail.amqp_errors)))
+ # data div
+ print("<div id=\"%s_data\" style=\"display:none; margin-bottom: 2px; margin-left: 10px\">" % id)
+
+ # unaccounted frames
+ print("<a href=\"javascript:toggle_node('%s_data_unacc')\">%s%s</a>" %
+ (id, text.lozenge(), text.nbsp()))
+ # show the connection-level frames
+ errs = sum(1 for plf in conn_detail.unaccounted_frame_list if plf.data.amqp_error)
+ print("Connection-based entries %s<br>" % self.format_errors(errs))
+ print("<div id=\"%s_data_unacc\" style=\"display:none; margin-bottom: 2px; margin-left: 10px\">" % id)
+ for plf in conn_detail.unaccounted_frame_list:
+ print(plf.adverbl_link_to(), plf.datetime, plf.data.direction, peer, plf.data.web_show_str, "<br>")
+ print("</div>") # end unaccounted frames
+
+ # loop to print session details
+ for sess in conn_detail.session_list:
+ # show the session toggle and title
+ print("<a href=\"javascript:toggle_node('%s_sess_%s')\">%s%s</a>" %
+ (id, sess.conn_epoch, text.lozenge(), text.nbsp()))
+ print("Session %s: channel: %s, peer channel: %s; Time: start %s, Counts: frames: %d %s<br>" % \
+ (sess.conn_epoch, sess.channel, sess.peer_chan, sess.time_start, \
+ sess.FrameCount(), self.format_errors(sess.amqp_errors)))
+ print("<div id=\"%s_sess_%s\" style=\"display:none; margin-bottom: 2px; margin-left: 10px\">" %
+ (id, sess.conn_epoch))
+ # show the session-level frames
+ errs = sum(1 for plf in sess.session_frame_list if plf.data.amqp_error)
+ print("<a href=\"javascript:toggle_node('%s_sess_%s_unacc')\">%s%s</a>" %
+ (id, sess.conn_epoch, text.lozenge(), text.nbsp()))
+ print("Session-based entries %s<br>" % self.format_errors(errs))
+ print("<div id=\"%s_sess_%s_unacc\" style=\"display:none; margin-bottom: 2px; margin-left: 10px\">" %
+ (id, sess.conn_epoch))
+ for plf in sess.session_frame_list:
+ print(plf.adverbl_link_to(), plf.datetime, plf.data.direction, peer, plf.data.web_show_str, "<br>")
+ print("</div>") # end <id>_sess_<conn_epoch>_unacc
+ # loops to print session link details
+ # first loop prints link table
+ print("<table")
+ print("<tr><th>Link</th> <th>Dir</th> <th>Role</th> <th>Address</th> <th>Class</th> "
+ "<th>snd-settle-mode</th> <th>rcv-settle-mode</th> <th>Start time</th> <th>Frames</th> "
+ "<th>AMQP errors</tr>")
+ for link in sess.link_list:
+ # show the link toggle and title
+ showthis = ("<a href=\"javascript:toggle_node('%s_sess_%s_link_%s')\">%s</a>" %
+ (id, sess.conn_epoch, link.session_seq, link.display_name))
+ role = "receiver" if link.is_receiver else "sender"
+ print("<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td>"
+ "<td>%s</td><td>%d</td><td>%s</td></tr>" % \
+ (showthis, link.direction, role, link.first_address,
+ (link.sender_class + '-' + link.receiver_class), link.snd_settle_mode,
+ link.rcv_settle_mode, link.time_start, link.FrameCount(),
+ self.format_errors(link.amqp_errors)))
+ print("</table>")
+ # second loop prints the link's frames
+ for link in sess.link_list:
+ print(
+ "<div id=\"%s_sess_%s_link_%s\" style=\"display:none; margin-top: 2px; margin-bottom: 2px; margin-left: 10px\">" %
+ (id, sess.conn_epoch, link.session_seq))
+ print("<h4>Connection %s Session %s Link %s</h4>" %
+ (id, sess.conn_epoch, link.display_name))
+ for plf in link.frame_list:
+ if plf.data.name == "transfer":
+ tdid = plf.data.delivery_id
+ if plf.data.direction == "->":
+ rmap = sess.rx_rcvr_disposition_map
+ tmap = sess.rx_sndr_disposition_map
+ else:
+ rmap = sess.tx_rcvr_disposition_map
+ tmap = sess.tx_sndr_disposition_map
+ plf.data.disposition_display = self.resolve_settlement(link, plf,
+ rmap.get(tdid),
+ tmap.get(tdid))
+ print(plf.adverbl_link_to(), plf.datetime, plf.data.direction, peer, plf.data.web_show_str,
+ plf.data.disposition_display, "<br>")
+ print("</div>") # end link <id>_sess_<conn_epoch>_link_<sess_seq>
+
+ print("</div>") # end session <id>_sess_<conn_epoch>
+
+ print("</div>") # end current connection data
+
+
+if __name__ == "__main__":
+
+ try:
+ pass
+ except:
+ traceback.print_exc(file=sys.stdout)
+ pass
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b7ab3390/tools/scraper/common.py
----------------------------------------------------------------------
diff --git a/tools/scraper/common.py b/tools/scraper/common.py
new file mode 100755
index 0000000..0a74f3c
--- /dev/null
+++ b/tools/scraper/common.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python
+
+#
+# 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.
+#
+
+# Common data storage and utilities
+
+import sys
+
+import nicknamer
+
+IS_PY2 = sys.version_info[0] == 2
+
+if IS_PY2:
+ def dict_iteritems(d):
+ return d.iteritems()
+ def dict_iterkeys(d):
+ return d.iterkeys()
+else:
+ def dict_iteritems(d):
+ return iter(d.items())
+ def dict_iterkeys(d):
+ return iter(d.keys())
+
+class Common():
+
+ # analysis_level_ludicrous
+ # Adverbl tries too hard to cross reference data
+ # Use these switchs to turn some of the biggest offenders off
+ per_link_detail = True
+ message_progress_tables = False
+
+ # returned from argparse.parse_args()
+ args = None
+
+ # first letter of the connection names
+ log_char_base = 'A'
+
+ # number of logs processed
+ n_logs = 0
+
+ # array of file name strings from command line
+ # len=n_logs
+ log_fns = []
+
+ # discovered router container names
+ # len=n_logs
+ router_ids = [] # raw long names
+
+ # router display names shortened with popups
+ router_display_names = []
+
+ # router modes in plain text
+ router_modes = []
+
+ # list of router-instance lists
+ # [[A0, A1], [B0], [C0, C1, C2]]
+ routers = []
+
+ # ordered list of connection names across all routers
+ all_conn_names = []
+
+ # conn_details_map -
+ # key=conn_id, val=ConnectionDetail for that connection
+ conn_details_map = {}
+
+ # mapping of connected routers by connection id
+ # A0_1 is connected to B3_2
+ # key = full conn_id 'A0_5'
+ # val = full conn_id 'B0_8'
+ # note names[key]=val and names[val]=key mutual reference
+ conn_peers_connid = {}
+
+ # short display name for peer indexed by connection id
+ # A0_1 maps to B's container_name nickname
+ conn_peers_display = {}
+
+ # conn_to_frame_map - global list for easier iteration in main
+ # key = conn_id full A0_3
+ # val = list of plf lines
+ conn_to_frame_map = {}
+
+ shorteners = nicknamer.Shorteners()
+
+ # when --no-data is in effect, how many log lines were skipped?
+ data_skipped = 0
+
+ def router_id_index(self, id):
+ """
+ Given a router full container name, return the index in router_ids table
+ Throw value error if not found
+ :param id:
+ :return:
+ """
+ return self.router_ids.index(id)
+
+
+def log_letter_of(idx):
+ '''
+ Return the letter A, B, C, ... from the index 0..n
+ :param idx:
+ :return: A..Z
+ '''
+ if idx >= 26:
+ sys.exit('ERROR: too many log files')
+ return "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[idx]
+
+def index_of_log_letter(letter):
+ '''
+ Return the index 0..25 of the firster letter of the 'letter' string
+ Raise error if out of range
+ :param letter:
+ :return:
+ '''
+ val = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".find(letter[0].upper())
+ if val < 0 or val > 25:
+ raise ValueError("index_of_log_letter Invalid log letter: %s", letter)
+ return val
+
+class RestartRec():
+ def __init__(self, _id, _router, _event, _datetime):
+ self.id = _id
+ self.router = _router
+ self.event = _event
+ self.datetime = _datetime
+
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b7ab3390/tools/scraper/log_splitter.py
----------------------------------------------------------------------
diff --git a/tools/scraper/log_splitter.py b/tools/scraper/log_splitter.py
new file mode 100755
index 0000000..cc5664e
--- /dev/null
+++ b/tools/scraper/log_splitter.py
@@ -0,0 +1,445 @@
+#!/usr/bin/env python
+
+#
+# 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.
+#
+
+# Split a gigantic (or not) log file into files of traffic for each connection.
+# Identify probable router and broker connections, QpidJMS client connections,
+# and AMQP errors. Create lists of connections sorted by log line and by transfer counts.
+# Emit a web page summarizing the results.
+
+from __future__ import unicode_literals
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import print_function
+
+import cgi
+import os
+import sys
+import traceback
+from collections import defaultdict
+
+
+class connection():
+ def __init__(self, instance, conn_id, logfile):
+ self.instance = instance
+ self.conn_id = conn_id
+ self.logfile = logfile
+ self.lines = []
+ self.key_name = connection.keyname(instance, conn_id)
+ self.transfers = 0
+ self.peer_open = ""
+ self.peer_type = ""
+ self.log_n_lines = 0
+ self.log_n_dir = ""
+ self.file_name = ""
+ self.path_name = ""
+
+ @staticmethod
+ def keyname(instance, conn_id):
+ tmp = "0000000" + str(conn_id)
+ return str(instance) + "." + tmp[-8:]
+
+ def disp_name(self):
+ return str(self.instance) + "_" + str(self.conn_id)
+
+ def generate_paths(self):
+ self.log_n_dir = "10e%d" % self.log_n_lines
+ self.file_name = self.disp_name() + ".log"
+ self.path_name = self.log_n_dir + "/" + self.file_name
+
+
+class LogFile:
+ def __init__(self, fn, top_n=24):
+ """
+ Represent connections in a file
+ :param fn: file name
+ :param
+ """
+ self.log_fn = fn # file name
+ self.top_n = top_n # how many to report
+ self.instance = 0 # incremented when router restarts in log file
+ self.amqp_lines = 0 # server trace lines
+ self.transfers = 0 # server transfers
+
+ # restarts
+ self.restarts = []
+
+ # connections
+ # dictionary of connection data
+ # key = connection id: <instance>.<conn_id> "0.3"
+ # val = connection class object
+ self.connections = {}
+
+ # router_connections
+ # list of received opens that suggest a router at the other end
+ self.router_connections = []
+
+ # broker connections
+ # list of received opens that suggest a broker at the other end
+ self.broker_connections = []
+
+ # errors
+ # amqp errors in time order
+ self.errors = []
+
+ # conns_by_size_transfer
+ # all connections in transfer size descending order
+ self.conns_by_size_transfer = []
+
+ # conns_by_size_loglines
+ # all connections in log_lines size descending order
+ self.conns_by_size_loglines = []
+
+ # histogram - count of connections with N logs < 10^index
+ # [0] = N < 10^0
+ # [1] = N < 10^1
+ self.histogram = [0,0,0,0,0,0,0,0,0,0]
+ self.hist_max = len(self.histogram) - 1
+
+ def parse_identify(self, text, line, before_col=70):
+ """
+ Look for text in line but make sure it's not in the body of some message,
+ :param text:
+ :param line:
+ :param before_col: limit on how far to search into line
+ """
+ st = line.find(text, 0, (before_col + len(text)))
+ if st < 0:
+ return False
+ return st < 70
+
+ def parse_line(self, line):
+ """
+ Do minimum parsing on line.
+ If container name then bump instance value
+ If server trace then get conn_id and add line to connections data
+ :param line:
+ :return:
+ """
+ key_sstart = "SERVER (info) Container Name:" # Normal 'router is starting' restart discovery line
+ key_strace = "SERVER (trace) [" # AMQP traffic
+ key_error = "@error(29)"
+ key_openin = "<- @open(16)"
+ key_xfer = "@transfer"
+ key_prod_dispatch = ':product="qpid-dispatch-router"'
+ key_prod_aartemis = ':product="apache-activemq-artemis"'
+ key_prod_aqpidcpp = ':product="qpid-cpp"'
+ key_prod_aqpidjms = ':product="QpidJMS"'
+
+ if self.parse_identify(key_sstart, line):
+ self.instance += 1
+ self.restarts.append(line)
+ else:
+ if self.parse_identify(key_strace, line):
+ self.amqp_lines += 1
+ idx = line.find(key_strace)
+ idx += len(key_strace)
+ eidx = line.find("]", idx + 1)
+ conn_id = line[idx:eidx]
+ keyname = connection.keyname(self.instance, conn_id)
+ if keyname not in self.connections:
+ self.connections[keyname] = connection(self.instance, conn_id, self)
+ curr_conn = self.connections[keyname]
+ curr_conn.lines.append(line)
+ # router hint
+ if key_openin in line:
+ # inbound open
+ if key_prod_dispatch in line:
+ self.router_connections.append(curr_conn)
+ curr_conn.peer_open = line
+ curr_conn.peer_type = key_prod_dispatch
+ elif key_prod_aqpidjms in line:
+ curr_conn.peer_type = key_prod_aqpidjms
+ else:
+ for k in [key_prod_aartemis, key_prod_aqpidcpp]:
+ if k in line:
+ self.broker_connections.append(curr_conn)
+ curr_conn.peer_open = line
+ curr_conn.peer_type = k
+ elif self.parse_identify(key_xfer, line):
+ self.transfers += 1
+ curr_conn.transfers += 1
+ if key_error in line:
+ self.errors.append(line)
+
+ def log_of(self, x):
+ """
+ calculate nearest power of 10 > x
+ :param x:
+ :return:
+ """
+ for i in range(self.hist_max):
+ if x < 10 ** i:
+ return i
+ return self.hist_max
+
+ def sort_sizes(self, sortfunc1, sortfunc2):
+ smap = defaultdict(list)
+ conns_by_size = []
+ # create size map. index is size, list holds all connections of that many transfers
+ for k, v in dict_iteritems(self.connections):
+ smap[str(sortfunc1(v))].append(v)
+ # create a sorted list of sizes in sizemap
+ sl = list(dict_iterkeys(smap))
+ sli = [int(k) for k in sl]
+ slist = sorted(sli, reverse=True)
+ # create grand list of all connections
+ for cursize in slist:
+ lsm = smap[str(cursize)]
+ lsm = sorted(lsm, key = sortfunc2, reverse=True)
+ #lsm = sorted(lsm, key = lambda x: int(x.conn_id))
+ for ls in lsm:
+ conns_by_size.append(ls)
+ return conns_by_size
+
+
+ def summarize_connections(self):
+ # sort connections based on transfer count and on n log lines
+ self.conns_by_size_transfer = self.sort_sizes(lambda x: x.transfers, lambda x: len(x.lines))
+ self.conns_by_size_loglines = self.sort_sizes(lambda x: len(x.lines), lambda x: x.transfers)
+
+ # compute log_n and file name facts for all connections
+ for k, v in dict_iteritems(self.connections):
+ v.log_n_lines = self.log_of(len(v.lines))
+ v.generate_paths()
+
+ # Write the web doc to stdout
+ print ("""<!DOCTYPE html>
+ <html>
+ <head>
+ <title>%s qpid-dispatch log split</title>
+
+ <style>
+ * {
+ font-family: sans-serif;
+ }
+ table {
+ border-collapse: collapse;
+ }
+ table, td, th {
+ border: 1px solid black;
+ padding: 3px;
+ }
+ </style>
+""" % self.log_fn)
+
+ print("""
+<h3>Contents</h3>
+<table>
+<tr> <th>Section</th> <th>Description</th> </tr>
+<tr><td><a href=\"#c_summary\" >Summary</a></td> <td>Summary</td></tr>
+<tr><td><a href=\"#c_restarts\" >Router restarts</a></td> <td>Router reboot records</td></tr>
+<tr><td><a href=\"#c_router_conn\" >Interrouter connections</a></td> <td>Probable interrouter connections</td></tr>
+<tr><td><a href=\"#c_broker_conn\" >Broker connections</a></td> <td>Probable broker connections</td></tr>
+<tr><td><a href=\"#c_errors\" >AMQP errors</a></td> <td>AMQP errors</td></tr>
+<tr><td><a href=\"#c_conn_xfersize\" >Conn by N transfers</a></td> <td>Connections sorted by transfer log count</td></tr>
+<tr><td><a href=\"#c_conn_xfer0\" >Conn with no transfers</a></td> <td>Connections with no transfers</td></tr>
+<tr><td><a href=\"#c_conn_logsize\" >Conn by N log lines</a></td> <td>Connections sorted by total log line count</td></tr>
+</table>
+<hr>
+""")
+ print("<a name=\"c_summary\"></a>")
+ print("<table>")
+ print("<tr><th>Statistic</th> <th>Value</th></tr>")
+ print("<tr><td>File</td> <td>%s</td></tr>" % self.log_fn)
+ print("<tr><td>Router starts</td> <td>%s</td></tr>" % str(self.instance))
+ print("<tr><td>Connections</td> <td>%s</td></tr>" % str(len(self.connections)))
+ print("<tr><td>Router connections</td> <td>%s</td></tr>" % str(len(self.router_connections)))
+ print("<tr><td>AMQP log lines</td> <td>%s</td></tr>" % str(self.amqp_lines))
+ print("<tr><td>AMQP errors</td> <td>%s</td></tr>" % str(len(self.errors)))
+ print("<tr><td>AMQP transfers</td> <td>%s</td></tr>" % str(self.transfers))
+ print("</table>")
+ print("<hr>")
+
+ # Restarts
+ print("<a name=\"c_restarts\"></a>")
+ print("<h3>Restarts</h3>")
+ for i in range(1, (self.instance + 1)):
+ rr = self.restarts[i-1]
+ print("(%d) - %s<br>" % (i, rr), end='')
+ print("<hr>")
+
+ # interrouter connections
+ print("<a name=\"c_router_conn\"></a>")
+ print("<h3>Probable inter-router connections (N=%d)</h3>" % (len(self.router_connections)))
+ print("<table>")
+ print("<tr><th>Connection</th> <th>Transfers</th> <th>Log lines</th> <th>AMQP Open<th></tr>")
+ for rc in self.router_connections:
+ print("<tr><td><a href=\"%s/%s\">%s</a></td><td>%d</td><td>%d</td><td>%s</td></tr>" %
+ (rc.logfile.odir(), rc.path_name, rc.disp_name(), rc.transfers, len(rc.lines),
+ cgi.escape(rc.peer_open)))
+ print("</table>")
+ print("<hr>")
+
+ # broker connections
+ print("<a name=\"c_broker_conn\"></a>")
+ print("<h3>Probable broker connections (N=%d)</h3>" % (len(self.broker_connections)))
+ print("<table>")
+ print("<tr><th>Connection</th> <th>Transfers</th> <th>Log lines</th> <th>AMQP Open<th></tr>")
+ for rc in self.broker_connections:
+ print("<tr><td><a href=\"%s/%s\">%s</a></td><td>%d</td><td>%d</td><td>%s</td></tr>" %
+ (rc.logfile.odir(), rc.path_name, rc.disp_name(), rc.transfers, len(rc.lines),
+ cgi.escape(rc.peer_open)))
+ print("</table>")
+ print("<hr>")
+
+ ## histogram
+ #for cursize in self.sizelist:
+ # self.histogram[self.log_of(cursize)] += len(self.sizemap[str(cursize)])
+ #print()
+ #print("Log lines per connection distribution")
+ #for i in range(1, self.hist_max):
+ # print("N < 10e%d : %d" %(i, self.histogram[i]))
+ #print("N >= 10e%d : %d" % ((self.hist_max - 1), self.histogram[self.hist_max]))
+
+ # errors
+ print("<a name=\"c_errors\"></a>")
+ print("<h3>AMQP errors (N=%d)</h3>" % (len(self.errors)))
+ print("<table>")
+ print("<tr><th>N</th> <th>AMQP error</th></tr>")
+ for i in range(len(self.errors)):
+ print("<tr><td>%d</td> <td>%s</td></tr>" % (i, cgi.escape(self.errors[i].strip())))
+ print("</table>")
+ print("<hr>")
+
+ def odir(self):
+ return os.path.join(os.getcwd(), (self.log_fn + ".splits"))
+
+ def write_subfiles(self):
+ # Q: Where to put the generated files? A: odir
+ odir = self.odir()
+ odirs = ['dummy'] # dirs indexed by log of n-lines
+
+ os.makedirs(odir)
+ for i in range(1, self.hist_max):
+ nrange = ("10e%d" % (i))
+ ndir = os.path.join(odir, nrange)
+ os.makedirs(ndir)
+ odirs.append(ndir)
+
+ for k, c in dict_iteritems(self.connections):
+ cdir = odirs[self.log_of(len(c.lines))]
+ opath = os.path.join(cdir, (c.disp_name() + ".log"))
+ with open(opath, 'w') as f:
+ for l in c.lines:
+ f.write(l)
+
+ xfer0 = 0
+ for rc in self.conns_by_size_transfer:
+ if rc.transfers == 0:
+ xfer0 += 1
+ print("<a name=\"c_conn_xfersize\"></a>")
+ print("<h3>Connections by transfer count (N=%d)</h3>" % (len(self.conns_by_size_transfer) - xfer0))
+ print("<table>")
+ n = 1
+ print("<tr><th>N</th><th>Connection</th> <th>Transfers</th> <th>Log lines</th> <th>Type</th> <th>AMQP detail<th></tr>")
+ for rc in self.conns_by_size_transfer:
+ if rc.transfers > 0:
+ print("<tr><td>%d</td><td><a href=\"%s/%s\">%s</a></td> <td>%d</td> <td>%d</td> <td>%s</td> <td>%s</td></tr>" %
+ (n, rc.logfile.odir(), rc.path_name, rc.disp_name(), rc.transfers, len(rc.lines),
+ rc.peer_type, cgi.escape(rc.peer_open)))
+ n += 1
+ print("</table>")
+ print("<hr>")
+
+ print("<a name=\"c_conn_xfer0\"></a>")
+ print("<h3>Connections with no AMQP transfers (N=%d)</h3>" % (xfer0))
+ print("<table>")
+ n = 1
+ print("<tr><th>N</th><th>Connection</th> <th>Transfers</th> <th>Log lines</th> <th>Type</th> <th>AMQP detail<th></tr>")
+ for rc in self.conns_by_size_transfer:
+ if rc.transfers == 0:
+ print("<tr><td>%d</td><td><a href=\"%s/%s\">%s</a></td> <td>%d</td> <td>%d</td> <td>%s</td> <td>%s</td></tr>" %
+ (n, rc.logfile.odir(), rc.path_name, rc.disp_name(), rc.transfers, len(rc.lines),
+ rc.peer_type, cgi.escape(rc.peer_open)))
+ n += 1
+ print("</table>")
+ print("<hr>")
+
+ print("<a name=\"c_conn_logsize\"></a>")
+ print("<h3>Connections by total log line count (N=%d)</h3>" % (len(self.conns_by_size_loglines)))
+ print("<table>")
+ n = 1
+ print("<tr><th>N</th><th>Connection</th> <th>Transfers</th> <th>Log lines</th> <th>Type</th> <th>AMQP detail<th></tr>")
+ for rc in self.conns_by_size_loglines:
+ print("<tr><td>%d</td><td><a href=\"%s/%s\">%s</a></td> <td>%d</td> <td>%d</td> <td>%s</td> <td>%s</td></tr>" %
+ (n, rc.logfile.odir(), rc.path_name, rc.disp_name(), rc.transfers, len(rc.lines),
+ rc.peer_type, cgi.escape(rc.peer_open)))
+ n += 1
+ print("</table>")
+ print("<hr>")
+
+
+# py 2-3 compat
+
+IS_PY2 = sys.version_info[0] == 2
+
+if IS_PY2:
+ def dict_iteritems(d):
+ return d.iteritems()
+ def dict_iterkeys(d):
+ return d.iterkeys()
+else:
+ def dict_iteritems(d):
+ return iter(d.items())
+ def dict_iterkeys(d):
+ return iter(d.keys())
+
+
+#
+#
+def main_except(log_fn):
+ """
+ Given a log file name, split the file into per-connection sub files
+ """
+ log_files = []
+
+ if not os.path.exists(log_fn):
+ sys.exit('ERROR: log file %s was not found!' % log_fn)
+
+ # parse the log file
+ with open(log_fn, 'r') as infile:
+ lf = LogFile(log_fn)
+ odir = lf.odir()
+ if os.path.exists(odir):
+ sys.exit('ERROR: output directory %s exists' % odir)
+ log_files.append(lf)
+ for line in infile:
+ lf.parse_line(line)
+
+ # write output
+ for lf in log_files:
+ lf.summarize_connections() # prints web page to console
+ lf.write_subfiles() # generates split files one-per-connection
+ pass
+
+def main(argv):
+ try:
+ if len(argv) != 2:
+ sys.exit('Usage: %s log-file-name' % argv[0])
+ main_except(argv[1])
+ return 0
+ except Exception as e:
+ traceback.print_exc()
+ return 1
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv))
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b7ab3390/tools/scraper/nicknamer.py
----------------------------------------------------------------------
diff --git a/tools/scraper/nicknamer.py b/tools/scraper/nicknamer.py
new file mode 100755
index 0000000..f198270
--- /dev/null
+++ b/tools/scraper/nicknamer.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python
+
+#
+# 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.
+#
+
+from collections import defaultdict
+import common
+import cgi
+
+class ShortNames():
+ '''
+ Name shortener.
+ The short name for display is "name_" + index(longName)
+ Embellish the display name with an html popup
+ Link and endpoint names, and data are tracked separately
+ Names longer than threshold are shortened
+ Each class has a prefix used when the table is dumped as HTML
+ '''
+ def __init__(self, prefixText, _threshold=25):
+ self.longnames = []
+ self.prefix = prefixText
+ self.threshold = _threshold
+ self.customer_dict = defaultdict(list)
+
+ def translate(self, lname, show_popup=False, customer=None):
+ '''
+ Translate a long name into a short name, maybe.
+ Memorize all names, translated or not
+ Strip leading/trailing double quotes
+ :param lname: the name
+ :return: If shortened HTML string of shortened name with popup containing long name else
+ not-so-long name.
+ '''
+ if lname.startswith("\"") and lname.endswith("\""):
+ lname = lname[1:-1]
+ try:
+ idx = self.longnames.index(lname)
+ except:
+ self.longnames.append(lname)
+ idx = self.longnames.index(lname)
+ # return as-given if short enough
+ if customer is not None:
+ self.customer_dict[lname].append(customer)
+ if len(lname) < self.threshold:
+ return lname
+ sname = self.prefix + "_" + str(idx)
+ if customer is not None:
+ self.customer_dict[sname].append(customer)
+ if show_popup:
+ return "<span title=\"" + cgi.escape(lname) + "\">" + sname + "</span>"
+ else:
+ return sname
+
+ def len(self):
+ return len(self.longnames)
+
+ def prefix(self):
+ return self.prefix
+
+ def shortname(self, idx):
+ name = self.longnames[idx]
+ if len(name) < self.threshold:
+ return name
+ return self.prefix + "_" + str(idx)
+
+ def prefixname(self, idx):
+ return self.prefix + "_" + str(idx)
+
+ def sname_to_popup(self, sname):
+ if not sname.startswith(self.prefix):
+ raise ValueError("Short name '%s' does not start with prefix '%s'" % (sname, self.prefix))
+ try:
+ lname = self.longnames[ int(sname[ (len(self.prefix) + 1): ])]
+ except:
+ raise ValueError("Short name '%s' did not translate to a long name" % (sname))
+ return "<span title=\"" + cgi.escape(lname) + sname + "</span>"
+
+ def longname(self, idx, cgi_escape=False):
+ '''
+ Get the cgi.escape'd long name
+ :param idx:
+ :param cgi_escape: true if caller wants the string for html display
+ :return:
+ '''
+ return cgi.escape(self.longnames[idx]) if cgi_escape else self.longnames[idx]
+
+ def htmlDump(self, with_link=False):
+ '''
+ Print the name table as an unnumbered list to stdout
+ long names are cgi.escape'd
+ :param with_link: true if link name link name is hyperlinked targeting itself
+ :return: null
+ '''
+ if len(self.longnames) > 0:
+ print ("<h3>" + self.prefix + " Name Index</h3>")
+ print ("<ul>")
+ for i in range(0, len(self.longnames)):
+ name = self.prefix + "_" + str(i)
+ dump_anchor = "<a name=\"%s_dump\"></a>" % (name)
+ if with_link:
+ name = "<a href=\"#%s\">%s</a>" % (name, name)
+ print ("<li> " + dump_anchor + name + " - " + cgi.escape(self.longnames[i]) + "</li>")
+ print ("</ul>")
+
+ def sort_customers(self):
+ for c in common.dict_iterkeys(self.customer_dict):
+ l = self.customer_dict[c]
+ self.customer_dict[c] = sorted(l, key=lambda lfl: lfl.datetime)
+
+ def customers(self, sname):
+ return self.customer_dict[sname]
+
+class Shorteners():
+ def __init__(self):
+ self.short_link_names = ShortNames("link", 15)
+ self.short_addr_names = ShortNames("address")
+ self.short_data_names = ShortNames("transfer", 2)
+ self.short_peer_names = ShortNames("peer")
+ self.short_rtr_names = ShortNames("router")
+
+
+if __name__ == "__main__":
+ pass
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org