You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pulsar.apache.org by si...@apache.org on 2019/08/18 05:06:41 UTC
[pulsar] branch master updated: [dashboard] integrate peek into
messages page (#4966)
This is an automated email from the ASF dual-hosted git repository.
sijie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar.git
The following commit(s) were added to refs/heads/master by this push:
new 1c3b6f2 [dashboard] integrate peek into messages page (#4966)
1c3b6f2 is described below
commit 1c3b6f2f3071a01d257366af2ed6f97a64493c0e
Author: Yi Tang <ss...@gmail.com>
AuthorDate: Sun Aug 18 13:06:34 2019 +0800
[dashboard] integrate peek into messages page (#4966)
### Motivation
messages page with full list would be under huge load if there are massive messages in backlog.
### Modifications
- render messages with a prompt and input to choose which message to peek instead of full backlog message list, and render message peeked within messages page.
- distinguish admin v1/v2 path from namespace.
---
dashboard/django/stats/models.py | 6 ++
dashboard/django/stats/static/stats/additional.css | 54 +++++++++++++++
.../django/stats/templates/stats/messages.html | 43 +++++++++++-
dashboard/django/stats/templates/stats/peek.html | 25 -------
dashboard/django/stats/urls.py | 1 -
dashboard/django/stats/views.py | 81 +++++++++++++---------
6 files changed, 147 insertions(+), 63 deletions(-)
diff --git a/dashboard/django/stats/models.py b/dashboard/django/stats/models.py
index 736a11c..80904b3 100644
--- a/dashboard/django/stats/models.py
+++ b/dashboard/django/stats/models.py
@@ -73,6 +73,9 @@ class Namespace(Model):
def is_global(self):
return self.name.split('/', 2)[1] == 'global'
+ def is_v2(self):
+ return len(self.name.split('/', 2)) == 2
+
def __str__(self):
return self.name
@@ -130,6 +133,9 @@ class Topic(Model):
def is_global(self):
return self.namespace.is_global()
+ def is_v2(self):
+ return self.namespace.is_v2()
+
def url_name(self):
return '/'.join(self.name.split('://', 1))
diff --git a/dashboard/django/stats/static/stats/additional.css b/dashboard/django/stats/static/stats/additional.css
index bd6ced8..db1705b 100644
--- a/dashboard/django/stats/static/stats/additional.css
+++ b/dashboard/django/stats/static/stats/additional.css
@@ -21,6 +21,60 @@
overflow: auto;
}
+.em {
+ font-weight: bold;
+}
+
+.tab {
+ border-bottom: 1px solid #79aec8;
+}
+
+.tab span {
+ cursor: pointer;
+ display: inline-block;
+ float: left;
+ font-weight: bold;
+ height: 30px;
+ line-height: 30px;
+ padding: 0 15px;
+}
+
+.tab span.active {
+ background-color: #79aec8;
+ color: #fff;
+}
+
+.tab span.warn {
+ background-color: #c13b3b;
+}
+
+.tab-content {
+ display: none
+}
+
+.clearfix:after{
+ content: "";
+ display: table;
+ clear: both;
+}
+
+.message {
+ max-width: 800px;
+}
+
+.message-title {
+ padding-right: 25px;
+}
+
+.message-content {
+ margin: 10px 0;
+ padding: 0 10px;
+ border-left: 5px solid #79aec8;
+ border-radius: 0 2px 2px 0;
+ background-color: #f2f2f2;
+ max-height: 400px;
+}
+
input.small-button {
padding: 2px;
}
diff --git a/dashboard/django/stats/templates/stats/messages.html b/dashboard/django/stats/templates/stats/messages.html
index 9374e79..53a69c2 100644
--- a/dashboard/django/stats/templates/stats/messages.html
+++ b/dashboard/django/stats/templates/stats/messages.html
@@ -43,9 +43,46 @@
{% endblock %}
{% block content %}
-{% for i in subscription.msgBacklog|times %}
-<li><a class ='btn' href="{% url 'peek' topic.url_name subscription.name i %}" rel="modal:open">view message {{ i }}</a></li>
+<form id="view-message-form" method="get" action="{% url 'messages' topic.url_name subscription.name %}">
+ <label for="message-position">Total <span class="em">{{subscription.msgBacklog}}</span> messages in backlog, view Message:</label>
+ <input name="message-position"
+ id="message-position"
+ value="{{ position }}"
+ type="number"
+ min="1" max="{{subscription.msgBacklog}}"/>
+</form>
+{% if message %}
+<script>
+$(function() {
+ function activeTab(tab) {
+ tab.addClass('active');
+ $("#message-" + tab.attr('id')).show();
+ }
+ let tabSel = "#message-tab .tab span";
+ let tabContentSel = "#message-tab .tab-content";
+ let firstTab = $(tabSel).eq(0);
+ activeTab(firstTab);
+ $(tabSel).bind('click', function(){
+ $(tabSel).removeClass('active');
+ $(tabContentSel).hide();
+ activeTab($(this))
+ });
+});
+</script>
+
+<div id="message-tab" class="message">
+ <div class="tab clearfix message-title">
+{% for type in message.keys %}
+ <span id="tab-{{type | lower}}"{% if type == 'ERROR' %} class="warn"{% endif %}>{{type}}</span>
+{% endfor %}
+ </div>
+{% for type, content in message.items %}
+ <div class="tab-content autoscroll message-content" id="message-tab-{{type | lower}}">
+ <pre>{{content}}</pre>
+ </div>
{% endfor %}
+</div>
-{% endblock %}
\ No newline at end of file
+{% endif %}
+{% endblock %}
diff --git a/dashboard/django/stats/templates/stats/peek.html b/dashboard/django/stats/templates/stats/peek.html
deleted file mode 100644
index 17d66b0..0000000
--- a/dashboard/django/stats/templates/stats/peek.html
+++ /dev/null
@@ -1,25 +0,0 @@
-<!--
-
- 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.
-
--->
-
-<div id="output" class="autoscroll">
- <pre>{{ message_type }}</pre>
- <pre>{{ message_body }}</pre>
-</div>
diff --git a/dashboard/django/stats/urls.py b/dashboard/django/stats/urls.py
index af1c8d6..3fa9003 100644
--- a/dashboard/django/stats/urls.py
+++ b/dashboard/django/stats/urls.py
@@ -37,6 +37,5 @@ urlpatterns = [
url(r'^clusters/$', views.clusters, name='clusters'),
url(r'^clearSubscription/(?P<topic_name>.+)/(?P<subscription_name>.+)$', views.clearSubscription, name='clearSubscription'),
url(r'^deleteSubscription/(?P<topic_name>.+)/(?P<subscription_name>.+)$', views.deleteSubscription, name='deleteSubscription'),
- url(r'^peek/(?P<topic_name>.+)/(?P<subscription_name>.+)/(?P<message_number>.+)$', views.peek, name='peek'),
url(r'^messages/(?P<topic_name>.+)/(?P<subscription_name>.+)$', views.messages, name='messages'),
]
diff --git a/dashboard/django/stats/views.py b/dashboard/django/stats/views.py
index d0a7af4..896d2e4 100644
--- a/dashboard/django/stats/views.py
+++ b/dashboard/django/stats/views.py
@@ -19,6 +19,7 @@
import logging
import struct
+import chardet
from django.shortcuts import render, get_object_or_404, redirect
from django.template import loader
@@ -348,22 +349,35 @@ def deleteSubscription(request, topic_name, subscription_name):
topic.save(update_fields=['backlog'])
return redirect('topic', topic_name=topic_name)
+
def messages(request, topic_name, subscription_name):
- topic_name = extract_topic_db_name(topic_name)
+ topic_db_name = extract_topic_db_name(topic_name)
timestamp = get_timestamp()
cluster_name = request.GET.get('cluster')
if cluster_name:
- topic = get_object_or_404(Topic, name=topic_name, cluster__name=cluster_name, timestamp=timestamp)
+ topic_obj = get_object_or_404(Topic,
+ name=topic_db_name,
+ cluster__name=cluster_name,
+ timestamp=timestamp)
else:
- topic = get_object_or_404(Topic, name=topic_name, timestamp=timestamp)
- subscription = get_object_or_404(Subscription, topic=topic, name=subscription_name)
+ topic_obj = get_object_or_404(Topic,
+ name=topic_db_name, timestamp=timestamp)
+ subscription_obj = get_object_or_404(Subscription,
+ topic=topic_obj, name=subscription_name)
+
+ message = None
+ message_position = request.GET.get('message-position')
+ if message_position and message_position.isnumeric():
+ message = peek_message(topic_obj, subscription_name, message_position)
return render(request, 'stats/messages.html', {
- 'topic' : topic,
- 'subscription' : subscription,
- 'title' : topic.name,
- 'subtitle' : subscription_name,
+ 'topic': topic_obj,
+ 'subscription': subscription_obj,
+ 'title': topic_obj.name,
+ 'subtitle': subscription_name,
+ 'message': message,
+ 'position': message_position or 1,
})
@@ -379,7 +393,7 @@ def message_skip_meta(message_view):
def get_message_from_http_response(response):
if response.status_code != 200:
- return "ERROR", "status_code=%d" % response.status_code
+ return {"ERROR": "%s(%d)" % (response.reason, response.status_code)}
message_view = memoryview(response.content)
if 'X-Pulsar-num-batch-message' in response.headers:
batch_size = int(response.headers['X-Pulsar-num-batch-message'])
@@ -387,32 +401,31 @@ def get_message_from_http_response(response):
message_view = message_skip_meta(message_view)
else:
# TODO: can not figure out multi-message batch for now
- return "Batch(size=%d)" % batch_size, "<omitted>"
-
- try:
- text = str(message_view,
- encoding=response.encoding or response.apparent_encoding,
- errors='replace')
- if not text.isprintable():
- return "Hex", hexdump.hexdump(message_view, result='return')
- except (LookupError, TypeError):
- return "Hex", hexdump.hexdump(message_view, result='return')
+ return {"Batch": "(size=%d)<omitted>" % batch_size}
+ message = {"Hex": hexdump.hexdump(message_view, result='return')}
try:
- return "JSON", json.dumps(json.loads(text),
- ensure_ascii=False, indent=4)
- except json.JSONDecodeError:
- return "Text", text
-
-
-def peek(request, topic_name, subscription_name, message_number):
- url = settings.SERVICE_URL + '/admin/v2/' + topic_name + '/subscription/' + subscription_name + '/position/' + message_number
- response = requests.get(url)
- message_type, message = get_message_from_http_response(response)
- context = {
- 'message_type': message_type,
- 'message_body': message,
- }
- return render(request, 'stats/peek.html', context)
+ message_bytes = message_view.tobytes()
+ text = str(message_bytes,
+ encoding=chardet.detect(message_bytes)['encoding'],
+ errors='strict')
+ message["Text"] = text
+ message["JSON"] = json.dumps(json.loads(text),
+ ensure_ascii=False, indent=4)
+ except Exception:
+ pass
+ return message
+
+
+def peek_message(topic_obj, subscription_name, message_position):
+ peek_url = "%s/subscription/%s/position/%s" % (
+ topic_path(topic_obj), subscription_name, message_position)
+ peek_response = requests.get(peek_url)
+ return get_message_from_http_response(peek_response)
+
+
+def topic_path(topic_obj):
+ admin_base = "/admin/v2/" if topic_obj.is_v2() else "/admin/"
+ return settings.SERVICE_URL + admin_base + topic_obj.url_name()
def extract_topic_db_name(topic_name):