diff --git a/manpages.mk b/manpages.mk
index 742bd66..38b9468 100644
--- a/manpages.mk
+++ b/manpages.mk
@@ -1,5 +1,13 @@
 # Generated automatically -- do not modify!    -*- buffer-read-only: t -*-
 
+ovn/utilities/ovn-detrace.1: \
+	ovn/utilities/ovn-detrace.1.in \
+	lib/common-syn.man \
+	lib/common.man
+ovn/utilities/ovn-detrace.1.in:
+lib/common-syn.man:
+lib/common.man:
+
 ovn/utilities/ovn-sbctl.8: \
 	ovn/utilities/ovn-sbctl.8.in \
 	lib/common.man \
diff --git a/ovn/utilities/automake.mk b/ovn/utilities/automake.mk
index 08e48ea..b96f9bf 100644
--- a/ovn/utilities/automake.mk
+++ b/ovn/utilities/automake.mk
@@ -6,14 +6,18 @@ man_MANS += \
     ovn/utilities/ovn-ctl.8 \
     ovn/utilities/ovn-nbctl.8 \
     ovn/utilities/ovn-sbctl.8 \
-    ovn/utilities/ovn-trace.8
+    ovn/utilities/ovn-trace.8 \
+    ovn/utilities/ovn-detrace.1
 
-MAN_ROOTS += ovn/utilities/ovn-sbctl.8.in
+MAN_ROOTS += \
+    ovn/utilities/ovn-sbctl.8.in \
+    ovn/utilities/ovn-detrace.1.in
 
 # Docker drivers
 bin_SCRIPTS += \
     ovn/utilities/ovn-docker-overlay-driver \
-    ovn/utilities/ovn-docker-underlay-driver
+    ovn/utilities/ovn-docker-underlay-driver \
+    ovn/utilities/ovn-detrace
 
 EXTRA_DIST += \
     ovn/utilities/ovn-ctl \
@@ -22,13 +26,16 @@ EXTRA_DIST += \
     ovn/utilities/ovn-docker-underlay-driver \
     ovn/utilities/ovn-nbctl.8.xml \
     ovn/utilities/ovn-trace.8.xml \
+    ovn/utilities/ovn-detrace.in \
     ovn/utilities/ovndb-servers.ocf
 
 CLEANFILES += \
     ovn/utilities/ovn-ctl.8 \
     ovn/utilities/ovn-nbctl.8 \
     ovn/utilities/ovn-sbctl.8 \
-    ovn/utilities/ovn-trace.8
+    ovn/utilities/ovn-trace.8 \
+    ovn/utilities/ovn-detrace.1 \
+    ovn/utilities/ovn-detrace
 
 # ovn-nbctl
 bin_PROGRAMS += ovn/utilities/ovn-nbctl
diff --git a/ovn/utilities/ovn-detrace.1.in b/ovn/utilities/ovn-detrace.1.in
new file mode 100644
index 0000000..2e5e514
--- /dev/null
+++ b/ovn/utilities/ovn-detrace.1.in
@@ -0,0 +1,37 @@
+.TH ovn\-detrace 1 "@VERSION@" "Open vSwitch" "Open vSwitch Manual"
+.
+.SH NAME
+ovn\-detrace \- convert ``ovs\-appctl ofproto/trace'' output to combine
+OVN logical flow information.
+.
+.SH SYNOPSIS
+\fBovn\-detrace < \fIfile\fR
+.so lib/common-syn.man
+.
+.SH DESCRIPTION
+The \fBovn\-detrace\fR program reads \fBovs\-appctl ofproto/trace\fR output on
+stdin, looking for flow cookies, and expand each cookie with corresponding OVN
+logical flows. It expands logical flow further with the north-bound information
+e.g. the ACL that generated the logical flow, when relevant.
+.PP
+.
+.SH "OPTIONS"
+.so lib/common.man
+.
+.IP "\fB\-\-ovnsb=\fIserver\fR"
+The OVN Southbound DB remote to contact.  If the \fBOVN_SB_DB\fR
+environment variable is set, its value is used as the default.
+Otherwise, the default is \fBunix:@RUNDIR@/ovnsb_db.sock\fR, but this
+default is unlikely to be useful outside of single-machine OVN test
+environments.
+.
+.IP "\fB\-\-ovnnb=\fIserver\fR"
+The OVN Northbound DB remote to contact.  If the \fBOVN_NB_DB\fR
+environment variable is set, its value is used as the default.
+Otherwise, the default is \fBunix:@RUNDIR@/ovnnb_db.sock\fR, but this
+default is unlikely to be useful outside of single-machine OVN test
+environments.
+.
+.SH "SEE ALSO"
+.
+.BR ovs\-appctl (8), ovn\-sbctl (8), ovn-\-nbctl (8), ovn\-trace (8)
diff --git a/ovn/utilities/ovn-detrace.in b/ovn/utilities/ovn-detrace.in
new file mode 100755
index 0000000..3d8e433
--- /dev/null
+++ b/ovn/utilities/ovn-detrace.in
@@ -0,0 +1,215 @@
+#! /usr/bin/env @PYTHON@
+#
+# Copyright (c) 2017 eBay Inc.
+#
+# Licensed 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.
+
+import getopt
+import os
+import re
+import sys
+import time
+
+try:
+    from ovs.db import idl
+    from ovs import jsonrpc
+    from ovs.poller import Poller
+    from ovs.stream import Stream
+except Exception:
+    print("ERROR: Please install the correct Open vSwitch python support")
+    print("       libraries (@VERSION@).")
+    print("       Alternatively, check that your PYTHONPATH is pointing to")
+    print("       the correct location.")
+    sys.exit(1)
+
+
+argv0 = sys.argv[0]
+
+
+def usage():
+    print """\
+%(argv0)s:
+usage: %(argv0)s < FILE
+where FILE is output from ovs-appctl ofproto/trace.
+
+The following options are also available:
+  -h, --help                  display this help message
+  -V, --version               display version information
+  --ovnsb=DATABASE            use DATABASE as southbound DB
+  --ovnnb=DATABASE            use DATABASE as northbound DB\
+""" % {'argv0': argv0}
+    sys.exit(0)
+
+
+class OVSDB(object):
+    @staticmethod
+    def wait_for_db_change(idl):
+        seq = idl.change_seqno
+        stop = time.time() + 10
+        while idl.change_seqno == seq and not idl.run():
+            poller = Poller()
+            idl.wait(poller)
+            poller.block()
+            if time.time() >= stop:
+                raise Exception('Retry Timeout')
+
+    def __init__(self, db_sock, schema_name):
+        self._db_sock = db_sock
+        self._txn = None
+        schema = self._get_schema(schema_name)
+        schema.register_all()
+        self._idl_conn = idl.Idl(db_sock, schema)
+        OVSDB.wait_for_db_change(self._idl_conn)  # Initial Sync with DB
+
+    def _get_schema(self, schema_name):
+        error, strm = Stream.open_block(Stream.open(self._db_sock))
+        if error:
+            raise Exception("Unable to connect to %s" % self._db_sock)
+        rpc = jsonrpc.Connection(strm)
+        req = jsonrpc.Message.create_request('get_schema', [schema_name])
+        error, resp = rpc.transact_block(req)
+        rpc.close()
+
+        if error or resp.error:
+            raise Exception('Unable to retrieve schema.')
+        return idl.SchemaHelper(None, resp.result)
+
+    def get_table(self, table_name):
+        return self._idl_conn.tables[table_name]
+
+    def _find_row(self, table_name, find):
+        return next(
+            (row for row in self.get_table(table_name).rows.values()
+             if find(row)), None)
+
+    def _find_row_by_name(self, table_name, value):
+        return self._find_row(table_name, lambda row: row.name == value)
+
+    def find_row_by_partial_uuid(self, table_name, value):
+        return self._find_row(table_name, lambda row: value in str(row.uuid))
+
+
+def get_lflow_from_cookie(ovnsb_db, cookie):
+    return ovnsb_db.find_row_by_partial_uuid('Logical_Flow', cookie)
+
+
+def print_lflow(lflow, prefix):
+    ldp_uuid = lflow.logical_datapath.uuid
+    ldp_name = str(lflow.logical_datapath.external_ids.get('name'))
+
+    print '%sLogical datapath: "%s" (%s) [%s]' % (prefix,
+                                                  ldp_name,
+                                                  ldp_uuid,
+                                                  lflow.pipeline)
+    print "%sLogical flow: table=%s (%s), priority=%s, " \
+          "match=(%s), actions=(%s)" % (prefix,
+                                        lflow.table_id,
+                                        lflow.external_ids.get('stage-name'),
+                                        lflow.priority,
+                                        str(lflow.match).strip('"'),
+                                        str(lflow.actions).strip('"'))
+
+
+def print_lflow_nb_hint(lflow, prefix, ovnnb_db):
+    external_ids = lflow.external_ids
+    if external_ids.get('stage-name') in ['ls_in_acl',
+                                          'ls_out_acl']:
+        acl_hint = external_ids.get('stage-hint')
+        if not acl_hint:
+            return
+        acl = ovnnb_db.find_row_by_partial_uuid('ACL', acl_hint)
+        if not acl:
+            return
+        output = "%sACL: %s, priority=%s, " \
+                 "match=(%s), %s" % (prefix,
+                                     acl.direction,
+                                     acl.priority,
+                                     acl.match.strip('"'),
+                                     acl.action)
+        if acl.log:
+            output += ' (log)'
+        print output
+
+
+def main():
+    try:
+        options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
+                                          ['help', 'version', 'ovnsb=', 'ovnnb='])
+    except getopt.GetoptError, geo:
+        sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
+        sys.exit(1)
+
+    ovnsb_db = None
+    ovnnb_db = None
+
+    for key, value in options:
+        if key in ['-h', '--help']:
+            usage()
+        elif key in ['-V', '--version']:
+            print "%s (Open vSwitch) @VERSION@" % argv0
+        elif key in ['--ovnsb']:
+            ovnsb_db = value
+        elif key in ['--ovnnb']:
+            ovnnb_db = value
+        else:
+            sys.exit(0)
+
+    if len(args) != 0:
+        sys.stderr.write("%s: non-option argument not supported "
+                         "(use --help for help)\n" % argv0)
+        sys.exit(1)
+
+    ovs_rundir = os.getenv('OVS_RUNDIR', '/usr/local/var/run/openvswitch')
+    if not ovnsb_db:
+        ovnsb_db = os.getenv('OVN_SB_DB')
+        if not ovnsb_db:
+            ovnsb_db = 'unix:%s/ovnsb_db.sock' % ovs_rundir
+
+    if not ovnnb_db:
+        ovnnb_db = os.getenv('OVN_NB_DB')
+        if not ovnnb_db:
+            ovnnb_db = 'unix:%s/ovnnb_db.sock' % ovs_rundir
+
+    ovsdb_ovnsb = OVSDB(ovnsb_db, 'OVN_Southbound')
+    ovsdb_ovnnb = OVSDB(ovnnb_db, 'OVN_Northbound')
+
+    regex_cookie = re.compile(r'^.*cookie 0x([0-9a-fA-F]+)')
+    regex_table_id = re.compile(r'^[0-9]+\.')
+    cookie = None
+    while True:
+        line = sys.stdin.readline()
+        if cookie:
+            # print lflow info when the current flow block ends
+            if regex_table_id.match(line) or line.strip() == '':
+                lflow = get_lflow_from_cookie(ovsdb_ovnsb, cookie)
+                print_lflow(lflow, "\t* ")
+                print_lflow_nb_hint(lflow, "\t\t* ", ovsdb_ovnnb)
+                cookie = None
+
+        print line.strip()
+        if line == "":
+            break
+
+        m = regex_cookie.match(line)
+        if not m:
+            continue
+        cookie = m.group(1)
+
+
+if __name__ == "__main__":
+    main()
+
+
+# Local variables:
+# mode: python
+# End:
