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 2018/09/10 20:31:21 UTC
[incubator-pulsar] branch master updated: [docker] introduce a
pulsar standalone image (#2545)
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/incubator-pulsar.git
The following commit(s) were added to refs/heads/master by this push:
new 2812fef [docker] introduce a pulsar standalone image (#2545)
2812fef is described below
commit 2812fefb72aa8f7277a8a97d8842b0ad2dc6719b
Author: Sijie Guo <gu...@gmail.com>
AuthorDate: Mon Sep 10 13:31:19 2018 -0700
[docker] introduce a pulsar standalone image (#2545)
## Motivation
`pulsar` and `pulsar-all` are designed for running pulsar components on production.
although it can be used for running standalone, people still need to run a separate docker container for pulsar dashboard.
## Changes
introduce a `pulsar-standalone` image to package everything into one image. so people can run launch a pulsar standalone in one line command
in docker, including dashboard.
---
docker/pom.xml | 1 +
docker/pulsar-standalone/Dockerfile | 55 ++++
docker/pulsar-standalone/conf/nginx-app.conf | 37 +++
docker/pulsar-standalone/conf/postgresql.conf | 38 +++
docker/pulsar-standalone/conf/supervisor-app.conf | 34 ++
docker/pulsar-standalone/conf/uwsgi.ini | 45 +++
docker/pulsar-standalone/conf/uwsgi_params | 16 +
docker/pulsar-standalone/django/collector.py | 345 +++++++++++++++++++++
docker/pulsar-standalone/django/collector.sh | 23 ++
.../pulsar-standalone/django/dashboard/__init__.py | 19 ++
.../pulsar-standalone/django/dashboard/settings.py | 181 +++++++++++
docker/pulsar-standalone/django/dashboard/urls.py | 45 +++
docker/pulsar-standalone/django/dashboard/wsgi.py | 35 +++
docker/pulsar-standalone/django/manage.py | 41 +++
docker/pulsar-standalone/django/stats/__init__.py | 19 ++
docker/pulsar-standalone/django/stats/admin.py | 34 ++
docker/pulsar-standalone/django/stats/apps.py | 26 ++
.../django/stats/migrations/0001_initial.py | 221 +++++++++++++
.../django/stats/migrations/__init__.py | 20 ++
docker/pulsar-standalone/django/stats/models.py | 200 ++++++++++++
.../django/stats/templates/stats/base.html | 98 ++++++
.../django/stats/templates/stats/broker.html | 71 +++++
.../django/stats/templates/stats/brokers.html | 90 ++++++
.../django/stats/templates/stats/clusters.html | 106 +++++++
.../django/stats/templates/stats/home.html | 74 +++++
.../django/stats/templates/stats/namespace.html | 96 ++++++
.../django/stats/templates/stats/property.html | 72 +++++
.../django/stats/templates/stats/topic.html | 197 ++++++++++++
.../django/stats/templates/stats/topics.html | 91 ++++++
.../django/stats/templatetags/__init__.py | 19 ++
.../django/stats/templatetags/stats_extras.py | 64 ++++
.../django/stats/templatetags/table.py | 91 ++++++
docker/pulsar-standalone/django/stats/tests.py | 22 ++
docker/pulsar-standalone/django/stats/urls.py | 37 +++
docker/pulsar-standalone/django/stats/views.py | 283 +++++++++++++++++
docker/pulsar-standalone/pom.xml | 81 +++++
site2/docs/getting-started-standalone.md | 27 +-
.../getting-started-standalone.md | 14 +-
38 files changed, 2957 insertions(+), 11 deletions(-)
diff --git a/docker/pom.xml b/docker/pom.xml
index 675656a..bdc99f7 100644
--- a/docker/pom.xml
+++ b/docker/pom.xml
@@ -38,5 +38,6 @@
<module>pulsar</module>
<module>grafana</module>
<module>pulsar-all</module>
+ <module>pulsar-standalone</module>
</modules>
</project>
diff --git a/docker/pulsar-standalone/Dockerfile b/docker/pulsar-standalone/Dockerfile
new file mode 100644
index 0000000..869b4c0
--- /dev/null
+++ b/docker/pulsar-standalone/Dockerfile
@@ -0,0 +1,55 @@
+#
+# 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 apachepulsar/pulsar-all:latest
+
+RUN apt-get update
+RUN apt-get -y install postgresql sudo nginx supervisor
+
+# Python dependencies
+RUN pip install uwsgi 'Django<2.0' psycopg2 pytz requests
+
+# Postgres configuration
+COPY conf/postgresql.conf /etc/postgresql/9.6/main/
+
+# Configure nginx and supervisor
+RUN echo "daemon off;" >> /etc/nginx/nginx.conf
+COPY conf/nginx-app.conf /etc/nginx/sites-available/default
+COPY conf/supervisor-app.conf /etc/supervisor/conf.d/
+
+# Copy web-app sources
+COPY . /pulsar/
+
+# Setup database and create tables
+RUN sudo -u postgres /etc/init.d/postgresql start && \
+ sudo -u postgres psql --command "CREATE USER docker WITH PASSWORD 'docker';" && \
+ sudo -u postgres createdb -O docker pulsar_dashboard && \
+ cd /pulsar/django && \
+ ./manage.py migrate && \
+ sudo -u postgres /etc/init.d/postgresql stop
+
+# Collect all static files needed by Django in a
+# single place. Needed to run the app outside the
+# Django test web server
+RUN cd /pulsar/django && ./manage.py collectstatic --no-input
+
+ENV SERVICE_URL http://127.0.0.1:8080
+EXPOSE 80
+
+CMD ["supervisord", "-n"]
diff --git a/docker/pulsar-standalone/conf/nginx-app.conf b/docker/pulsar-standalone/conf/nginx-app.conf
new file mode 100644
index 0000000..6f57c10
--- /dev/null
+++ b/docker/pulsar-standalone/conf/nginx-app.conf
@@ -0,0 +1,37 @@
+#
+# 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.
+#
+
+upstream django {
+ server unix:/tmp/uwsgi.sock;
+}
+
+server {
+ listen 80 default_server;
+
+ charset utf-8;
+
+ location /static {
+ alias /pulsar/django/static;
+ }
+
+ location / {
+ uwsgi_pass django;
+ include /pulsar/conf/uwsgi_params;
+ }
+}
diff --git a/docker/pulsar-standalone/conf/postgresql.conf b/docker/pulsar-standalone/conf/postgresql.conf
new file mode 100644
index 0000000..201dbba
--- /dev/null
+++ b/docker/pulsar-standalone/conf/postgresql.conf
@@ -0,0 +1,38 @@
+#
+# 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.
+#
+
+# Relax durability to increase write throughput
+fsync = off
+full_page_writes = off
+synchronous_commit = off
+
+# Default configs
+data_directory = '/var/lib/postgresql/9.6/main'
+hba_file = '/etc/postgresql/9.6/main/pg_hba.conf'
+ident_file = '/etc/postgresql/9.6/main/pg_ident.conf'
+external_pid_file = '/var/run/postgresql/9.6-main.pid'
+
+port = 5432
+max_connections = 100
+
+datestyle = 'iso, mdy'
+default_text_search_config = 'pg_catalog.english'
+stats_temp_directory = '/var/run/postgresql/9.6-main.pg_stat_tmp'
+timezone = 'UTC'
+log_timezone = 'UTC'
diff --git a/docker/pulsar-standalone/conf/supervisor-app.conf b/docker/pulsar-standalone/conf/supervisor-app.conf
new file mode 100644
index 0000000..6a02245
--- /dev/null
+++ b/docker/pulsar-standalone/conf/supervisor-app.conf
@@ -0,0 +1,34 @@
+#
+# 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.
+#
+
+[program:postgres]
+command = /usr/lib/postgresql/9.6/bin/postgres -D /etc/postgresql/9.6/main
+user = postgres
+
+[program:uwsgi]
+command = /usr/local/bin/uwsgi --ini /pulsar/conf/uwsgi.ini
+
+[program:nginx]
+command = /usr/sbin/nginx
+
+[program:collector]
+command = /pulsar/django/collector.sh
+
+[program:pulsar]
+command = /pulsar/bin/pulsar standalone
diff --git a/docker/pulsar-standalone/conf/uwsgi.ini b/docker/pulsar-standalone/conf/uwsgi.ini
new file mode 100644
index 0000000..f027436
--- /dev/null
+++ b/docker/pulsar-standalone/conf/uwsgi.ini
@@ -0,0 +1,45 @@
+#
+# 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.
+#
+
+# django.ini file
+[uwsgi]
+
+uid = www-data
+gid = www-data
+
+# master
+master = true
+
+# maximum number of processes
+processes = 10
+
+# the socket (use the full path to be safe)
+socket = /tmp/uwsgi.sock
+
+# with appropriate permissions - *may* be needed
+# chmod-socket = 664
+
+# the base directory
+chdir = /pulsar/django
+
+# Django's wsgi file
+module = dashboard.wsgi
+
+# clear environment on exit
+vacuum = true
diff --git a/docker/pulsar-standalone/conf/uwsgi_params b/docker/pulsar-standalone/conf/uwsgi_params
new file mode 100644
index 0000000..f539451
--- /dev/null
+++ b/docker/pulsar-standalone/conf/uwsgi_params
@@ -0,0 +1,16 @@
+
+uwsgi_param QUERY_STRING $query_string;
+uwsgi_param REQUEST_METHOD $request_method;
+uwsgi_param CONTENT_TYPE $content_type;
+uwsgi_param CONTENT_LENGTH $content_length;
+
+uwsgi_param REQUEST_URI $request_uri;
+uwsgi_param PATH_INFO $document_uri;
+uwsgi_param DOCUMENT_ROOT $document_root;
+uwsgi_param SERVER_PROTOCOL $server_protocol;
+uwsgi_param HTTPS $https if_not_empty;
+
+uwsgi_param REMOTE_ADDR $remote_addr;
+uwsgi_param REMOTE_PORT $remote_port;
+uwsgi_param SERVER_PORT $server_port;
+uwsgi_param SERVER_NAME $server_name;
diff --git a/docker/pulsar-standalone/django/collector.py b/docker/pulsar-standalone/django/collector.py
new file mode 100755
index 0000000..76c0646
--- /dev/null
+++ b/docker/pulsar-standalone/django/collector.py
@@ -0,0 +1,345 @@
+#!/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.
+#
+
+
+import os
+import django
+import requests
+import pytz
+import multiprocessing
+import traceback
+import sys
+from django.utils import timezone
+from django.utils.dateparse import parse_datetime
+from django.db import connection
+import time
+import argparse
+
+current_milli_time = lambda: int(round(time.time() * 1000))
+
+def get(base_url, path):
+ if base_url.endswith('/'): path = path[1:]
+ return requests.get(base_url + path,
+ headers=http_headers,
+ proxies=http_proxyes,
+ ).json()
+
+def parse_date(d):
+ if d:
+ dt = parse_datetime(d)
+ if dt.tzinfo:
+ # There is already the timezone set
+ return dt
+ else:
+ # Assume UTC if no timezone
+ return pytz.timezone('UTC').localize(parse_datetime(d))
+ else: return None
+
+# Fetch the stats for a given broker
+def fetch_broker_stats(cluster, broker_url, timestamp):
+ try:
+ _fetch_broker_stats(cluster, broker_url, timestamp)
+ except Exception as e:
+ traceback.print_exc(file=sys.stderr)
+ raise e
+
+
+def _fetch_broker_stats(cluster, broker_host_port, timestamp):
+ broker_url = 'http://%s/' % broker_host_port
+ print ' Getting stats for %s' % broker_host_port
+
+ broker, _ = Broker.objects.get_or_create(
+ url = broker_host_port,
+ cluster = cluster
+ )
+ active_broker = ActiveBroker(broker=broker, timestamp=timestamp)
+ active_broker.save()
+
+ # Get topics stats
+ topics_stats = get(broker_url, '/admin/broker-stats/destinations')
+
+ clusters = dict( (cluster.name, cluster) for cluster in Cluster.objects.all() )
+
+ db_bundles = []
+ db_topics = []
+ db_subscriptions = []
+ db_consumers = []
+ db_replication = []
+
+ for namespace_name, bundles_stats in topics_stats.items():
+ property_name = namespace_name.split('/')[0]
+ property, _ = Property.objects.get_or_create(name=property_name)
+
+ namespace, _ = Namespace.objects.get_or_create(
+ name=namespace_name,
+ property=property)
+ namespace.clusters.add(cluster)
+ namespace.save()
+
+ for bundle_range, topics_stats in bundles_stats.items():
+ bundle = Bundle(
+ broker = broker,
+ namespace = namespace,
+ range = bundle_range,
+ cluster = cluster,
+ timestamp = timestamp)
+ db_bundles.append(bundle)
+
+ for topic_name, stats in topics_stats['persistent'].items():
+ topic = Topic(
+ broker = broker,
+ active_broker = active_broker,
+ name = topic_name,
+ namespace = namespace,
+ bundle = bundle,
+ cluster = cluster,
+ timestamp = timestamp,
+ averageMsgSize = stats['averageMsgSize'],
+ msgRateIn = stats['msgRateIn'],
+ msgRateOut = stats['msgRateOut'],
+ msgThroughputIn = stats['msgThroughputIn'],
+ msgThroughputOut = stats['msgThroughputOut'],
+ pendingAddEntriesCount = stats['pendingAddEntriesCount'],
+ producerCount = stats['producerCount'],
+ storageSize = stats['storageSize']
+ )
+
+ db_topics.append(topic)
+ totalBacklog = 0
+ numSubscriptions = 0
+ numConsumers = 0
+
+ for subscription_name, subStats in stats['subscriptions'].items():
+ numSubscriptions += 1
+ subscription = Subscription(
+ topic = topic,
+ name = subscription_name,
+ namespace = namespace,
+ timestamp = timestamp,
+ msgBacklog = subStats['msgBacklog'],
+ msgRateExpired = subStats['msgRateExpired'],
+ msgRateOut = subStats['msgRateOut'],
+ msgRateRedeliver = subStats.get('msgRateRedeliver', 0),
+ msgThroughputOut = subStats['msgThroughputOut'],
+ subscriptionType = subStats['type'][0],
+ unackedMessages = subStats.get('unackedMessages', 0),
+ )
+ db_subscriptions.append(subscription)
+
+ totalBacklog += subStats['msgBacklog']
+
+ for consStats in subStats['consumers']:
+ numConsumers += 1
+ consumer = Consumer(
+ subscription = subscription,
+ timestamp = timestamp,
+ address = consStats['address'],
+ availablePermits = consStats.get('availablePermits', 0),
+ connectedSince = parse_date(consStats.get('connectedSince')),
+ consumerName = consStats.get('consumerName'),
+ msgRateOut = consStats.get('msgRateOut', 0),
+ msgRateRedeliver = consStats.get('msgRateRedeliver', 0),
+ msgThroughputOut = consStats.get('msgThroughputOut', 0),
+ unackedMessages = consStats.get('unackedMessages', 0),
+ blockedConsumerOnUnackedMsgs = consStats.get('blockedConsumerOnUnackedMsgs', False)
+ )
+ db_consumers.append(consumer)
+
+ topic.backlog = totalBacklog
+ topic.subscriptionCount = numSubscriptions
+ topic.consumerCount = numConsumers
+
+ replicationMsgIn = 0
+ replicationMsgOut = 0
+ replicationThroughputIn = 0
+ replicationThroughputOut = 0
+ replicationBacklog = 0
+
+ for remote_cluster, replStats in stats['replication'].items():
+ replication = Replication(
+ timestamp = timestamp,
+ topic = topic,
+ local_cluster = cluster,
+ remote_cluster = clusters[remote_cluster],
+
+ msgRateIn = replStats['msgRateIn'],
+ msgRateOut = replStats['msgRateOut'],
+ msgThroughputIn = replStats['msgThroughputIn'],
+ msgThroughputOut = replStats['msgThroughputOut'],
+ replicationBacklog = replStats['replicationBacklog'],
+ connected = replStats['connected'],
+ replicationDelayInSeconds = replStats['replicationDelayInSeconds'],
+ msgRateExpired = replStats['msgRateExpired'],
+
+ inboundConnectedSince = parse_date(replStats.get('inboundConnectedSince')),
+ outboundConnectedSince = parse_date(replStats.get('outboundConnectedSince')),
+ )
+
+ db_replication.append(replication)
+
+ replicationMsgIn += replication.msgRateIn
+ replicationMsgOut += replication.msgRateOut
+ replicationThroughputIn += replication.msgThroughputIn
+ replicationThroughputOut += replication.msgThroughputOut
+ replicationBacklog += replication.replicationBacklog
+
+ topic.replicationRateIn = replicationMsgIn
+ topic.replicationRateOut = replicationMsgOut
+ topic.replicationThroughputIn = replicationThroughputIn
+ topic.replicationThroughputOut = replicationThroughputOut
+ topic.replicationBacklog = replicationBacklog
+ topic.localRateIn = topic.msgRateIn - replicationMsgIn
+ topic.localRateOut = topic.msgRateOut - replicationMsgOut
+ topic.localThroughputIn = topic.msgThroughputIn - replicationThroughputIn
+ topic.localThroughputOut = topic.msgThroughputIn - replicationThroughputOut
+
+
+ if connection.vendor == 'postgresql':
+ # Bulk insert into db
+ Bundle.objects.bulk_create(db_bundles, batch_size=10000)
+
+ # Trick to refresh primary keys after previous bulk import
+ for topic in db_topics: topic.bundle = topic.bundle
+ Topic.objects.bulk_create(db_topics, batch_size=10000)
+
+ for subscription in db_subscriptions: subscription.topic = subscription.topic
+ Subscription.objects.bulk_create(db_subscriptions, batch_size=10000)
+
+ for consumer in db_consumers: consumer.subscription = consumer.subscription
+ Consumer.objects.bulk_create(db_consumers, batch_size=10000)
+
+ for replication in db_replication: replication.topic = replication.topic
+ Replication.objects.bulk_create(db_replication, batch_size=10000)
+
+ else:
+ # For other DB providers we have to insert one by one
+ # to be able to retrieve the PK of the newly inserted records
+ for bundle in db_bundles:
+ bundle.save()
+
+ for topic in db_topics:
+ topic.bundle = topic.bundle
+ topic.save()
+
+ for subscription in db_subscriptions:
+ subscription.topic = subscription.topic
+ subscription.save()
+
+ for consumer in db_consumers:
+ consumer.subscription = consumer.subscription
+ consumer.save()
+
+ for replication in db_replication:
+ replication.topic = replication.topic
+ replication.save()
+
+
+def fetch_stats():
+ timestamp = current_milli_time()
+
+ pool = multiprocessing.Pool(args.workers)
+
+ futures = []
+
+ for cluster_name in get(args.serviceUrl, '/admin/clusters'):
+ if cluster_name == 'global': continue
+
+ cluster_url = get(args.serviceUrl, '/admin/clusters/' + cluster_name)['serviceUrl']
+ print 'Cluster:', cluster_name, '->', cluster_url
+ cluster, created = Cluster.objects.get_or_create(name=cluster_name)
+ if cluster_url != cluster.serviceUrl:
+ cluster.serviceUrl = cluster_url
+ cluster.save()
+
+ # Get the list of brokers for each cluster
+ for cluster in Cluster.objects.all():
+ try:
+ for broker_host_port in get(cluster.serviceUrl, '/admin/brokers/' + cluster.name):
+ f = pool.apply_async(fetch_broker_stats, (cluster, broker_host_port, timestamp))
+ futures.append(f)
+ except Exception as e:
+ print 'ERROR: ', e
+
+ pool.close()
+
+ for f in futures:
+ f.get()
+
+ pool.join()
+
+ # Update Timestamp in DB
+ latest, _ = LatestTimestamp.objects.get_or_create(name='latest')
+ latest.timestamp = timestamp
+ latest.save()
+
+def purge_db():
+ now = current_milli_time()
+ ttl_minutes = args.purge
+ threshold = now - (ttl_minutes * 60 * 1000)
+
+ Bundle.objects.filter(timestamp__lt = threshold).delete()
+ Topic.objects.filter(timestamp__lt = threshold).delete()
+ Subscription.objects.filter(timestamp__lt = threshold).delete()
+ Consumer.objects.filter(timestamp__lt = threshold).delete()
+
+def collect_and_purge():
+ print '-- Starting stats collection'
+ fetch_stats()
+ purge_db()
+ print '-- Finished collecting stats'
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard.settings")
+ django.setup()
+
+ from stats.models import *
+
+ parser = argparse.ArgumentParser(description='Pulsar Stats collector')
+ parser.add_argument(action="store", dest="serviceUrl", help='Service URL of one cluster in the Pulsar instance')
+
+ parser.add_argument('--proxy', action='store',
+ help="Connect using a HTTP proxy", dest="proxy")
+ parser.add_argument('--header', action="append", dest="header",
+ help='Add an additional HTTP header to all requests')
+ parser.add_argument('--purge', action="store", dest="purge", type=int, default=60,
+ help='Purge statistics older than PURGE minutes. (default: 60min)')
+
+ parser.add_argument('--workers', action="store", dest="workers", type=int, default=64,
+ help='Number of worker processes to be used to fetch the stats (default: 64)')
+
+ global args
+ args = parser.parse_args(sys.argv[1:])
+
+ global http_headers
+ http_headers = {}
+ if args.header:
+ http_headers = dict(x.split(': ') for x in args.header)
+
+ global http_proxyes
+ http_proxyes = {}
+ if args.proxy:
+ http_proxyes['http'] = args.proxy
+ http_proxyes['https'] = args.proxy
+
+ # Schedule a new collection every 1min
+ while True:
+ p = multiprocessing.Process(target=collect_and_purge)
+ p.start()
+ time.sleep(60)
diff --git a/docker/pulsar-standalone/django/collector.sh b/docker/pulsar-standalone/django/collector.sh
new file mode 100755
index 0000000..1c1f4d3
--- /dev/null
+++ b/docker/pulsar-standalone/django/collector.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+#
+# 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.
+#
+
+
+cd /pulsar/django
+./collector.py $SERVICE_URL
diff --git a/docker/pulsar-standalone/django/dashboard/__init__.py b/docker/pulsar-standalone/django/dashboard/__init__.py
new file mode 100644
index 0000000..d8a500d
--- /dev/null
+++ b/docker/pulsar-standalone/django/dashboard/__init__.py
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+
diff --git a/docker/pulsar-standalone/django/dashboard/settings.py b/docker/pulsar-standalone/django/dashboard/settings.py
new file mode 100644
index 0000000..37e8c2c
--- /dev/null
+++ b/docker/pulsar-standalone/django/dashboard/settings.py
@@ -0,0 +1,181 @@
+#
+# 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.
+#
+
+"""
+Django settings for dashboard project.
+
+Generated by 'django-admin startproject' using Django 1.10.3.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.10/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/1.10/ref/settings/
+"""
+
+import os
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'kxmt78byexqz$9m!9o3f7!h3d$lz@2fe9-+te7!=0rfwm2afcb'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = ['*']
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'stats.apps.StatsConfig',
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'django.contrib.humanize',
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'dashboard.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ 'django.template.context_processors.request',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'dashboard.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
+
+# DATABASES = {
+# 'default': {
+# 'ENGINE': 'django.db.backends.sqlite3',
+# 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+# }
+# }
+
+#
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.postgresql',
+ 'USER' : 'docker',
+ 'PASSWORD' : 'docker',
+ 'HOST' : 'localhost',
+ 'NAME' : 'pulsar_dashboard',
+ }
+}
+
+# DATABASES = {
+# 'default': {
+# 'ENGINE': 'django.db.backends.mysql',
+# 'USER' : 'root',
+# 'HOST' : 'localhost',
+# 'NAME' : 'pulsar_dashboard',
+# }
+# }
+
+
+# Password validation
+# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/1.10/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.10/howto/static-files/
+
+STATIC_URL = '/static/'
+
+STATIC_ROOT = os.path.join(BASE_DIR, "static/")
+
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'handlers': {
+ 'console': {
+ 'level': 'INFO',
+ 'class': 'logging.StreamHandler',
+ }
+ },
+ 'loggers': {
+ 'django.db.backends': {
+ 'handlers': ['console'],
+ 'level': 'INFO',
+ },
+ }
+}
diff --git a/docker/pulsar-standalone/django/dashboard/urls.py b/docker/pulsar-standalone/django/dashboard/urls.py
new file mode 100644
index 0000000..71b1f11
--- /dev/null
+++ b/docker/pulsar-standalone/django/dashboard/urls.py
@@ -0,0 +1,45 @@
+#
+# 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.
+#
+
+"""dashboard URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/1.10/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.conf.urls import url, include
+ 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
+"""
+from django.conf.urls import include, url
+from django.contrib import admin
+
+from stats import views
+
+
+urlpatterns = [
+ url(r'^admin/', admin.site.urls),
+ url(r'^stats/', include('stats.urls')),
+ url(r'^$', views.home, name='home'),
+]
diff --git a/docker/pulsar-standalone/django/dashboard/wsgi.py b/docker/pulsar-standalone/django/dashboard/wsgi.py
new file mode 100644
index 0000000..9bdcc64
--- /dev/null
+++ b/docker/pulsar-standalone/django/dashboard/wsgi.py
@@ -0,0 +1,35 @@
+#
+# 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.
+#
+
+"""
+WSGI config for dashboard project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard.settings")
+
+application = get_wsgi_application()
diff --git a/docker/pulsar-standalone/django/manage.py b/docker/pulsar-standalone/django/manage.py
new file mode 100755
index 0000000..44022a4
--- /dev/null
+++ b/docker/pulsar-standalone/django/manage.py
@@ -0,0 +1,41 @@
+#!/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.
+#
+
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard.settings")
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError:
+ # The above import may fail for some other reason. Ensure that the
+ # issue is really that Django is missing to avoid masking other
+ # exceptions on Python 2.
+ try:
+ import django
+ except ImportError:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ )
+ raise
+ execute_from_command_line(sys.argv)
diff --git a/docker/pulsar-standalone/django/stats/__init__.py b/docker/pulsar-standalone/django/stats/__init__.py
new file mode 100644
index 0000000..d8a500d
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/__init__.py
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+
diff --git a/docker/pulsar-standalone/django/stats/admin.py b/docker/pulsar-standalone/django/stats/admin.py
new file mode 100644
index 0000000..b1bb360
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/admin.py
@@ -0,0 +1,34 @@
+#
+# 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 django.contrib import admin
+
+# Register your models here.
+from .models import *
+admin.site.register(Cluster)
+admin.site.register(Property)
+admin.site.register(Namespace)
+admin.site.register(Bundle)
+
+
+class TopicAdmin(admin.ModelAdmin):
+ list_filter = ('cluster', 'namespace__property__name')
+
+
+admin.site.register(Topic, TopicAdmin)
diff --git a/docker/pulsar-standalone/django/stats/apps.py b/docker/pulsar-standalone/django/stats/apps.py
new file mode 100644
index 0000000..5941cbe
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/apps.py
@@ -0,0 +1,26 @@
+#
+# 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 django.apps import AppConfig
+
+
+class StatsConfig(AppConfig):
+ name = 'stats'
diff --git a/docker/pulsar-standalone/django/stats/migrations/0001_initial.py b/docker/pulsar-standalone/django/stats/migrations/0001_initial.py
new file mode 100644
index 0000000..1ce53fa
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/migrations/0001_initial.py
@@ -0,0 +1,221 @@
+#
+# 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.
+#
+
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-02-21 21:20
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ActiveBroker',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('timestamp', models.BigIntegerField(db_index=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Broker',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('url', models.URLField(db_index=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Bundle',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('timestamp', models.BigIntegerField(db_index=True)),
+ ('range', models.CharField(max_length=200)),
+ ('broker', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='stats.Broker')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Cluster',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=200, unique=True)),
+ ('serviceUrl', models.URLField()),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Consumer',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('timestamp', models.BigIntegerField(db_index=True)),
+ ('address', models.CharField(max_length=64, null=True)),
+ ('availablePermits', models.IntegerField(default=0)),
+ ('connectedSince', models.DateTimeField(null=True)),
+ ('consumerName', models.CharField(max_length=64, null=True)),
+ ('msgRateOut', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('msgRateRedeliver', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('msgThroughputOut', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('unackedMessages', models.BigIntegerField(default=0)),
+ ('blockedConsumerOnUnackedMsgs', models.BooleanField(default=False)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='LatestTimestamp',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=10, unique=True)),
+ ('timestamp', models.BigIntegerField(default=0)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Namespace',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=200, unique=True)),
+ ('clusters', models.ManyToManyField(to='stats.Cluster')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Property',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=200, unique=True)),
+ ],
+ options={
+ 'verbose_name_plural': 'properties',
+ },
+ ),
+ migrations.CreateModel(
+ name='Replication',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('timestamp', models.BigIntegerField(db_index=True)),
+ ('msgRateIn', models.DecimalField(decimal_places=1, max_digits=12)),
+ ('msgThroughputIn', models.DecimalField(decimal_places=1, max_digits=12)),
+ ('msgRateOut', models.DecimalField(decimal_places=1, max_digits=12)),
+ ('msgThroughputOut', models.DecimalField(decimal_places=1, max_digits=12)),
+ ('msgRateExpired', models.DecimalField(decimal_places=1, max_digits=12)),
+ ('replicationBacklog', models.BigIntegerField(default=0)),
+ ('connected', models.BooleanField(default=False)),
+ ('replicationDelayInSeconds', models.IntegerField(default=0)),
+ ('inboundConnectedSince', models.DateTimeField(null=True)),
+ ('outboundConnectedSince', models.DateTimeField(null=True)),
+ ('local_cluster', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='stats.Cluster')),
+ ('remote_cluster', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='remote_cluster', to='stats.Cluster')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Subscription',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=200)),
+ ('timestamp', models.BigIntegerField(db_index=True)),
+ ('msgBacklog', models.BigIntegerField(default=0)),
+ ('msgRateExpired', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('msgRateOut', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('msgRateRedeliver', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('msgThroughputOut', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('subscriptionType', models.CharField(choices=[('N', 'Not connected'), ('E', 'Exclusive'), ('S', 'Shared'), ('F', 'Failover')], default='N', max_length=1)),
+ ('unackedMessages', models.BigIntegerField(default=0)),
+ ('namespace', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='stats.Namespace')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Topic',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(db_index=True, max_length=1024)),
+ ('timestamp', models.BigIntegerField(db_index=True)),
+ ('averageMsgSize', models.IntegerField(default=0)),
+ ('msgRateIn', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('msgRateOut', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('msgThroughputIn', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('msgThroughputOut', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('pendingAddEntriesCount', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('producerCount', models.IntegerField(default=0)),
+ ('subscriptionCount', models.IntegerField(default=0)),
+ ('consumerCount', models.IntegerField(default=0)),
+ ('storageSize', models.BigIntegerField(default=0)),
+ ('backlog', models.BigIntegerField(default=0)),
+ ('localRateIn', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('localRateOut', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('localThroughputIn', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('localThroughputOut', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('replicationRateIn', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('replicationRateOut', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('replicationThroughputIn', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('replicationThroughputOut', models.DecimalField(decimal_places=1, default=0, max_digits=12)),
+ ('replicationBacklog', models.BigIntegerField(default=0)),
+ ('active_broker', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='stats.ActiveBroker')),
+ ('broker', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='stats.Broker')),
+ ('bundle', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='stats.Bundle')),
+ ('cluster', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='stats.Cluster')),
+ ('namespace', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='stats.Namespace')),
+ ],
+ ),
+ migrations.AddField(
+ model_name='subscription',
+ name='topic',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='stats.Topic'),
+ ),
+ migrations.AddField(
+ model_name='replication',
+ name='topic',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='stats.Topic'),
+ ),
+ migrations.AddField(
+ model_name='namespace',
+ name='property',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='stats.Property'),
+ ),
+ migrations.AddField(
+ model_name='consumer',
+ name='subscription',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='stats.Subscription'),
+ ),
+ migrations.AddField(
+ model_name='bundle',
+ name='cluster',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='stats.Cluster'),
+ ),
+ migrations.AddField(
+ model_name='bundle',
+ name='namespace',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='stats.Namespace'),
+ ),
+ migrations.AddField(
+ model_name='broker',
+ name='cluster',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='stats.Cluster'),
+ ),
+ migrations.AddField(
+ model_name='activebroker',
+ name='broker',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='stats.Broker'),
+ ),
+ migrations.AlterIndexTogether(
+ name='topic',
+ index_together=set([('name', 'cluster', 'timestamp')]),
+ ),
+ ]
diff --git a/docker/pulsar-standalone/django/stats/migrations/__init__.py b/docker/pulsar-standalone/django/stats/migrations/__init__.py
new file mode 100644
index 0000000..d321c94
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/migrations/__init__.py
@@ -0,0 +1,20 @@
+#
+# 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.
+#
+
+#
\ No newline at end of file
diff --git a/docker/pulsar-standalone/django/stats/models.py b/docker/pulsar-standalone/django/stats/models.py
new file mode 100644
index 0000000..11bd23d
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/models.py
@@ -0,0 +1,200 @@
+#
+# 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 django.utils.encoding import python_2_unicode_compatible
+from django.db.models import *
+from django.urls import reverse
+
+# Used to store the latest
+class LatestTimestamp(Model):
+ name = CharField(max_length=10, unique=True)
+ timestamp = BigIntegerField(default=0)
+
+@python_2_unicode_compatible
+class Cluster(Model):
+ name = CharField(max_length=200, unique=True)
+ serviceUrl = URLField()
+
+ def __str__(self):
+ return self.name
+
+@python_2_unicode_compatible
+class Broker(Model):
+ url = URLField(db_index=True)
+ cluster = ForeignKey(Cluster, on_delete=SET_NULL, db_index=True, null=True)
+
+ def __str__(self):
+ return self.url
+
+@python_2_unicode_compatible
+class ActiveBroker(Model):
+ broker = ForeignKey(Broker, on_delete=SET_NULL, db_index=True, null=True)
+ timestamp = BigIntegerField(db_index=True)
+
+ def __str__(self):
+ return self.url
+
+@python_2_unicode_compatible
+class Property(Model):
+ name = CharField(max_length=200, unique=True)
+
+ def __str__(self):
+ return self.name
+
+ class Meta:
+ verbose_name_plural = 'properties'
+
+@python_2_unicode_compatible
+class Namespace(Model):
+ name = CharField(max_length=200, unique=True)
+ property = ForeignKey(Property, on_delete=SET_NULL, db_index=True, null=True)
+ clusters = ManyToManyField(Cluster)
+
+ def is_global(self):
+ return self.name.split('/', 2)[1] == 'global'
+
+ def __str__(self):
+ return self.name
+
+@python_2_unicode_compatible
+class Bundle(Model):
+ timestamp = BigIntegerField(db_index=True)
+ broker = ForeignKey(Broker, on_delete=SET_NULL, db_index=True, null=True)
+ namespace = ForeignKey(Namespace, on_delete=SET_NULL, db_index=True, null=True)
+ cluster = ForeignKey(Cluster, on_delete=SET_NULL, db_index=True, null=True)
+ range = CharField(max_length=200)
+
+ def __str__(self):
+ return str(self.pk) + '--' + self.namespace.name + '/' + self.range
+
+@python_2_unicode_compatible
+class Topic(Model):
+ name = CharField(max_length=1024, db_index=True)
+ active_broker = ForeignKey(ActiveBroker, on_delete=SET_NULL, db_index=True, null=True)
+ broker = ForeignKey(Broker, on_delete=SET_NULL, db_index=True, null=True)
+ namespace = ForeignKey(Namespace, on_delete=SET_NULL, db_index=True, null=True)
+ cluster = ForeignKey(Cluster, on_delete=SET_NULL, db_index=True, null=True)
+ bundle = ForeignKey(Bundle, on_delete=SET_NULL, db_index=True, null=True)
+
+ timestamp = BigIntegerField(db_index=True)
+ averageMsgSize = IntegerField(default=0)
+ msgRateIn = DecimalField(max_digits = 12, decimal_places=1, default=0)
+ msgRateOut = DecimalField(max_digits = 12, decimal_places=1, default=0)
+ msgThroughputIn = DecimalField(max_digits = 12, decimal_places=1, default=0)
+ msgThroughputOut = DecimalField(max_digits = 12, decimal_places=1, default=0)
+ pendingAddEntriesCount = DecimalField(max_digits = 12, decimal_places=1, default=0)
+ producerCount = IntegerField(default=0)
+ subscriptionCount = IntegerField(default=0)
+ consumerCount = IntegerField(default=0)
+ storageSize = BigIntegerField(default=0)
+ backlog = BigIntegerField(default=0)
+
+ localRateIn = DecimalField(max_digits = 12, decimal_places=1, default=0)
+ localRateOut = DecimalField(max_digits = 12, decimal_places=1, default=0)
+ localThroughputIn = DecimalField(max_digits = 12, decimal_places=1, default=0)
+ localThroughputOut = DecimalField(max_digits = 12, decimal_places=1, default=0)
+
+ replicationRateIn = DecimalField(max_digits = 12, decimal_places=1, default=0)
+ replicationRateOut = DecimalField(max_digits = 12, decimal_places=1, default=0)
+ replicationThroughputIn = DecimalField(max_digits = 12, decimal_places=1, default=0)
+ replicationThroughputOut = DecimalField(max_digits = 12, decimal_places=1, default=0)
+ replicationBacklog = BigIntegerField(default=0)
+
+ def short_name(self):
+ return self.name.split('/', 5)[-1]
+
+ def is_global(self):
+ return self.namespace.is_global()
+
+ def url_name(self):
+ return '/'.join(self.name.split('://', 1))
+
+ def get_absolute_url(self):
+ url = reverse('topic', args=[self.url_name()])
+ if self.namespace.is_global():
+ url += '?cluster=' + self.cluster.name
+ return url
+
+ class Meta:
+ index_together = ('name', 'cluster', 'timestamp')
+
+ def __str__(self):
+ return self.name
+
+@python_2_unicode_compatible
+class Subscription(Model):
+ name = CharField(max_length=200)
+ topic = ForeignKey(Topic, on_delete=SET_NULL, null=True)
+ namespace = ForeignKey(Namespace, on_delete=SET_NULL, null=True, db_index=True)
+
+ timestamp = BigIntegerField(db_index=True)
+ msgBacklog = BigIntegerField(default=0)
+ msgRateExpired = DecimalField(max_digits = 12, decimal_places=1, default=0)
+ msgRateOut = DecimalField(max_digits = 12, decimal_places=1, default=0)
+ msgRateRedeliver = DecimalField(max_digits = 12, decimal_places=1, default=0)
+ msgThroughputOut = DecimalField(max_digits = 12, decimal_places=1, default=0)
+
+ SUBSCRIPTION_TYPES = (
+ ('N', 'Not connected'),
+ ('E', 'Exclusive'),
+ ('S', 'Shared'),
+ ('F', 'Failover'),
+ )
+ subscriptionType = CharField(max_length=1, choices=SUBSCRIPTION_TYPES, default='N')
+ unackedMessages = BigIntegerField(default=0)
+
+ def __str__(self):
+ return self.name
+
+
+class Consumer(Model):
+ timestamp = BigIntegerField(db_index=True)
+ subscription = ForeignKey(Subscription, on_delete=SET_NULL, db_index=True, null=True)
+
+ address = CharField(max_length=64, null=True)
+ availablePermits = IntegerField(default=0)
+ connectedSince = DateTimeField(null=True)
+ consumerName = CharField(max_length=64, null=True)
+ msgRateOut = DecimalField(max_digits = 12, decimal_places=1, default=0)
+ msgRateRedeliver = DecimalField(max_digits = 12, decimal_places=1, default=0)
+ msgThroughputOut = DecimalField(max_digits = 12, decimal_places=1, default=0)
+ unackedMessages = BigIntegerField(default=0)
+ blockedConsumerOnUnackedMsgs = BooleanField(default=False)
+
+
+class Replication(Model):
+ timestamp = BigIntegerField(db_index=True)
+ topic = ForeignKey(Topic, on_delete=SET_NULL, null=True)
+ local_cluster = ForeignKey(Cluster, on_delete=SET_NULL, null=True)
+ remote_cluster = ForeignKey(Cluster, on_delete=SET_NULL, null=True, related_name='remote_cluster')
+
+ msgRateIn = DecimalField(max_digits = 12, decimal_places=1)
+ msgThroughputIn = DecimalField(max_digits = 12, decimal_places=1)
+ msgRateOut = DecimalField(max_digits = 12, decimal_places=1)
+ msgThroughputOut = DecimalField(max_digits = 12, decimal_places=1)
+ msgRateExpired = DecimalField(max_digits = 12, decimal_places=1)
+ replicationBacklog = BigIntegerField(default=0)
+
+ connected = BooleanField(default=False)
+ replicationDelayInSeconds = IntegerField(default=0)
+
+ inboundConnectedSince = DateTimeField(null=True)
+ outboundConnectedSince = DateTimeField(null=True)
diff --git a/docker/pulsar-standalone/django/stats/templates/stats/base.html b/docker/pulsar-standalone/django/stats/templates/stats/base.html
new file mode 100644
index 0000000..1e5843f
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/templates/stats/base.html
@@ -0,0 +1,98 @@
+<!--
+
+ 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.
+
+-->
+{% load static %}<!DOCTYPE html>
+
+<html>
+<head>
+<title>Pulsar Dashboard</title>
+<link rel="stylesheet" type="text/css" href="{% static "admin/css/base.css" %}" />
+<link rel="stylesheet" type="text/css" href="{% static "admin/css/changelists.css" %}" />
+
+<script
+ src="https://code.jquery.com/jquery-3.1.1.slim.min.js"
+ integrity="sha256-/SIrNqv8h6QGKDuNoLGA4iret+kyesCkHGzVUUV0shc="
+ crossorigin="anonymous"></script>
+
+{% block extrastyle %}{% endblock %}
+{% block extrahead %}{% endblock %}
+{% block blockbots %}<meta name="robots" content="NONE,NOARCHIVE" />{% endblock %}
+</head>
+
+<body class="{% if is_popup %}popup {% endif %}{% block bodyclass %}{% endblock %}"
+ data-admin-utc-offset="{% now "Z" %}">
+
+<!-- Container -->
+<div id="container">
+
+ {% if not is_popup %}
+ <!-- Header -->
+ <div id="header">
+ <div id="branding">
+ {% block branding %}<h1 id="site-name"><a href="{% url 'home' %}">Pulsar Dashboard</a></h1>{% endblock %}
+ </div>
+ {% block nav-global %}
+ <h2>
+ <a href="{% url 'home' %}">Properties</a> |
+ <a href="{% url 'brokers' %}">Brokers</a> |
+ <a href="{% url 'topics' %}">Topics</a> |
+ <a href="{% url 'clusters' %}">Clusters</a>
+ </h2>
+ </div>
+ {% endblock %}
+ </div>
+ <!-- END Header -->
+
+
+ {% block breadcrumbs %}
+ <div class="breadcrumbs">
+ <a href="{% url 'home' %}">Home</a>
+ {% if title %} › {{ title }}{% endif %}
+ </div>
+ {% endblock %}
+ {% endif %}
+
+ {% block messages %}
+ {% if messages %}
+ <ul class="messagelist">{% for message in messages %}
+ <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message|capfirst }}</li>
+ {% endfor %}</ul>
+ {% endif %}
+ {% endblock messages %}
+
+ <!-- Content -->
+ <div id="content" class="{% block coltype %}colM{% endblock %}">
+ {% block pretitle %}{% endblock %}
+ {% block content_title %}{% if title %}<h1>{{ title }}</h1>{% endif %}{% endblock %}
+ {% block content %}
+ {% block object-tools %}{% endblock %}
+ {{ content }}
+ {% endblock %}
+ {% block sidebar %}{% endblock %}
+ <br class="clear" />
+ </div>
+ <!-- END Content -->
+
+ {% block footer %}<div id="footer"></div>{% endblock %}
+</div>
+<!-- END Container -->
+
+</body>
+</html>
diff --git a/docker/pulsar-standalone/django/stats/templates/stats/broker.html b/docker/pulsar-standalone/django/stats/templates/stats/broker.html
new file mode 100644
index 0000000..38f68a9
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/templates/stats/broker.html
@@ -0,0 +1,71 @@
+<!--
+
+ 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.
+
+-->
+{% extends "stats/base.html" %}
+{% load humanize %}
+{% load table %}
+
+{% block title %}Broker | {{property.name}}{% endblock %}
+
+{% block breadcrumbs %}
+<div class="breadcrumbs">
+ <a href="{% url 'home' %}">Home</a>
+ › <a href="{% url 'brokers' %}">Brokers</a>
+ › {{broker_url}}
+</div>
+{% endblock %}
+
+
+{% block content %}
+
+<table>
+<thead>
+ <tr>
+ {% column_header topics 'namespace__name' 'Namespace' %}
+ {% column_header topics 'name' 'Topic' %}
+ {% column_header topics 'msgRateIn' 'Msg/s in' %}
+ {% column_header topics 'msgRateOut' 'Msg/s out' %}
+ {% column_header topics 'msgThroughputIn' 'Bytes/s in' %}
+ {% column_header topics 'msgThroughputOut' 'Bytes/s out' %}
+ {% column_header topics 'backlog' 'Backlog' %}
+ </tr>
+</thead>
+<tbody>
+
+{% for topic in topics.results %}
+ <tr class="{% cycle 'row1' 'row2' %}">
+ <th><a href="{% url 'namespace' topic.namespace.name %}">{{topic.namespace}}</a></th>
+ <th><a href="{{topic.get_absolute_url}}">{{topic.short_name}}</a></th>
+ <td>{{topic.msgRateIn | intcomma}}</td>
+ <td>{{topic.msgRateOut | intcomma}}</td>
+ <td>{{topic.msgThroughputIn | intcomma}}</td>
+ <td>{{topic.msgThroughputOut | intcomma}}</td>
+ <td>{{topic.backlog | intcomma}}</td>
+ </tr>
+{% empty %}
+ <tr><td>No topics</td></tr>
+{% endfor %}
+</tbody>
+</table>
+
+{% table_footer topics %}
+
+
+{% endblock %}
diff --git a/docker/pulsar-standalone/django/stats/templates/stats/brokers.html b/docker/pulsar-standalone/django/stats/templates/stats/brokers.html
new file mode 100644
index 0000000..b022a87
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/templates/stats/brokers.html
@@ -0,0 +1,90 @@
+<!--
+
+ 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.
+
+-->
+{% extends "stats/base.html" %}
+
+{% load humanize %}
+{% load table %}
+{% load stats_extras %}
+
+{% block title %}Brokers{% endblock %}
+
+{% block content %}
+
+<div class="module filtered" id="changelist">
+<div id="changelist-filter">
+ <h2>Clusters</h2>
+<ul>
+ <li {% if not selectedCluster %}class="selected"{% endif %}>
+ <a href="{% url 'brokers' %}">All</a></li>
+
+ {% for cluster in clusters.all %}
+ <li {% if selectedCluster == cluster.name %}class="selected"{% endif %}
+ ><a href="{% url 'brokers_cluster' cluster %}">{{cluster}}</a></li>
+ {% endfor %}
+
+ <li>
+</ul>
+</div>
+</div>
+
+<table>
+<thead>
+ <tr>
+ {% column_header brokers 'url' 'Broker' %}
+ {% column_header brokers 'numBundles' 'Bundles' %}
+ {% column_header brokers 'numTopics' 'Topics' %}
+ {% column_header brokers 'numProducers' 'Producers' %}
+ {% column_header brokers 'numSubscriptions' 'Subscriptions' %}
+ {% column_header brokers 'numConsumers' 'Consumers' %}
+ {% column_header brokers 'rateIn' 'Rate In' %}
+ {% column_header brokers 'rateOut' 'Rate Out' %}
+ {% column_header brokers 'throughputIn' 'Mbps In' %}
+ {% column_header brokers 'throughputOut' 'Mbps Out' %}
+ {% column_header brokers 'backlog' 'Backlog' %}
+ </tr>
+</thead>
+<tbody>
+
+{% for broker in brokers.results %}
+ <tr class="{% cycle 'row1' 'row2' %}">
+ <th>
+ <a href="{% url 'broker' broker.url %}">{{broker.url}}</a>
+ </th>
+ <td>{{broker.numBundles | safe_intcomma}}</td>
+ <td>{{broker.numTopics | safe_intcomma}}</td>
+ <td>{{broker.numProducers | safe_intcomma}}</td>
+ <td>{{broker.numSubscriptions | safe_intcomma}}</td>
+ <td>{{broker.numConsumers | safe_intcomma}}</td>
+ <td>{{broker.rateIn | safe_intcomma}}</td>
+ <td>{{broker.rateOut | safe_intcomma}}</td>
+ <td>{{broker.throughputIn | mbps | floatformat | intcomma}}</td>
+ <td>{{broker.throughputOut | mbps | floatformat | intcomma}}</td>
+ <td>{{broker.backlog | safe_intcomma}}</td>
+ </tr>
+{% empty %}
+ <tr><td>No Brokers</td></tr>
+{% endfor %}
+</tbody>
+</table>
+
+{% table_footer brokers %}
+
+{% endblock %}
diff --git a/docker/pulsar-standalone/django/stats/templates/stats/clusters.html b/docker/pulsar-standalone/django/stats/templates/stats/clusters.html
new file mode 100644
index 0000000..afcf22e
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/templates/stats/clusters.html
@@ -0,0 +1,106 @@
+<!--
+
+ 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.
+
+-->
+{% extends "stats/base.html" %}
+
+{% load humanize %}
+{% load table %}
+{% load stats_extras %}
+
+{% block title %}Clusters{% endblock %}
+
+{% block content %}
+
+
+<table>
+<thead>
+ <tr>
+ {% column_header clusters 'name' 'Cluster' %}
+ {% column_header clusters 'numTopics' 'Topics' %}
+
+ {% column_header clusters 'localRateIn' 'Local Rate In' %}
+ {% column_header clusters 'localRateOut' 'Local Rate Out' %}
+ {% column_header clusters 'replicationRateIn' 'Replication Rate In' %}
+ {% column_header clusters 'replicationRateOut' 'Replication Rate Out' %}
+
+ {% column_header clusters 'localBacklog' 'Local Backlog' %}
+ {% column_header clusters 'replicationBacklog' 'Replication Backlog' %}
+ {% column_header clusters 'storage' 'Storage' %}
+ </tr>
+</thead>
+<tbody>
+
+{% for cluster in clusters.results %}
+ <tr class="{% cycle 'row1' 'row2' %}">
+ <th>{{cluster.name}}</th>
+
+ <td>{{cluster.numTopics | intcomma}}</td>
+ <td>{{cluster.localRateIn | intcomma}}</td>
+ <td>{{cluster.localRateOut | intcomma}}</td>
+ <td>{{cluster.replicationRateIn | intcomma}}</td>
+ <td>{{cluster.replicationRateOut | intcomma}}</td>
+
+ <td>{{cluster.localBacklog | intcomma}}</td>
+ <td>{{cluster.replicationBacklog | intcomma}}</td>
+ <td>{{cluster.storage | filesizeformat}}</td>
+ </tr>
+
+ <tr class="{% cycle 'row1' 'row2' %}">
+ <td></td>
+ <td colspan="6">
+ <table>
+ <thead>
+ <tr>
+ <th>Remote Cluster</th>
+ <th title="Msg/s">Rate in</th>
+ <th title="Msg/s">Rate out</th>
+ <th>Mbps in</th>
+ <th>Mbps out</th>
+ <th title="Messages">Replication backlog</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for peer in cluster.peers %}
+ <tr>
+ <td>{{peer.remote_cluster__name}}</td>
+ <td title="{{peer.remote_cluster__name}} ⟶ {{cluster.name}}">{{peer.msgRateIn__sum | intcomma}}</td>
+ <td title="{{cluster.name}} ⟶ {{peer.remote_cluster__name}}">{{peer.msgRateOut__sum | intcomma}}</td>
+ <td title="{{peer.remote_cluster__name}} ⟶ {{cluster.name}}">{{peer.msgThroughputIn__sum | mbps | floatformat | intcomma}}</td>
+ <td title="{{cluster.name}} ⟶ {{peer.remote_cluster__name}}">{{peer.msgThroughputOut__sum | mbps | floatformat | intcomma}}</td>
+ <td title="{{cluster.name}} ⟶ {{peer.remote_cluster__name}}">{{peer.replicationBacklog__sum | intcomma}}</td>
+ </tr>
+ {% empty %}
+ <tr><td>No replication</tr></td>
+ {% endfor %}
+ </tbody>
+ </table>
+ </td>
+ </tr>
+
+
+{% empty %}
+ <tr><td>No Clusters</td></tr>
+{% endfor %}
+</tbody>
+</table>
+
+{% table_footer clusters %}
+
+{% endblock %}
diff --git a/docker/pulsar-standalone/django/stats/templates/stats/home.html b/docker/pulsar-standalone/django/stats/templates/stats/home.html
new file mode 100644
index 0000000..828bfae
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/templates/stats/home.html
@@ -0,0 +1,74 @@
+<!--
+
+ 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.
+
+-->
+{% extends "stats/base.html" %}
+
+{% load table %}
+{% load humanize %}
+
+{% block title %}Home{% endblock %}
+
+{% block content %}
+
+<table>
+<thead>
+ <tr>
+ {% column_header properties 'name' 'Property' %}
+ {% column_header properties 'numNamespaces' 'Namespaces' %}
+ {% column_header properties 'numTopics' 'Topics' %}
+ {% column_header properties 'numProducers' 'Producers' %}
+ {% column_header properties 'numSubscriptions' 'Subscriptions' %}
+ {% column_header properties 'numConsumers' 'Consumers' %}
+ {% column_header properties 'rateIn' 'Rate In' %}
+ {% column_header properties 'rateOut' 'Rate Out' %}
+ {% column_header properties 'throughputIn' 'Throughput In' %}
+ {% column_header properties 'throughputOut' 'Throughput Out' %}
+ {% column_header properties 'backlog' 'Backlog' %}
+ {% column_header properties 'storage' 'Storage' %}
+ </tr>
+</thead>
+<tbody>
+
+{% for property in properties.results %}
+ <tr class="{% cycle 'row1' 'row2' %}">
+ <th>
+ <a href="{% url 'property' property.name %}">{{property.name}}</a>
+ </th>
+ <td>{{property.numNamespaces | intcomma}}</td>
+ <td>{{property.numTopics | intcomma}}</td>
+ <td>{{property.numProducers | intcomma}}</td>
+ <td>{{property.numSubscriptions | intcomma}}</td>
+ <td>{{property.numConsumers | intcomma}}</td>
+ <td>{{property.rateIn | intcomma}}</td>
+ <td>{{property.rateOut | intcomma}}</td>
+ <td>{{property.throughputIn | intcomma}}</td>
+ <td>{{property.throughputOut | intcomma}}</td>
+ <td>{{property.backlog | intcomma}}</td>
+ <td>{{property.storage | filesizeformat}}</td>
+ </tr>
+{% empty %}
+ <tr><td>No properties</td></tr>
+{% endfor %}
+</tbody>
+</table>
+
+
+
+{% endblock %}
diff --git a/docker/pulsar-standalone/django/stats/templates/stats/namespace.html b/docker/pulsar-standalone/django/stats/templates/stats/namespace.html
new file mode 100644
index 0000000..8af1c5f
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/templates/stats/namespace.html
@@ -0,0 +1,96 @@
+<!--
+
+ 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.
+
+-->
+{% extends "stats/base.html" %}
+{% load humanize %}
+{% load table %}
+
+{% block title %}Namespace | {{property.name}}{% endblock %}
+
+{% block breadcrumbs %}
+<div class="breadcrumbs">
+ <a href="{% url 'home' %}">Home</a>
+ › <a href="{% url 'property' namespace.property.name %}">{{ namespace.property }}</a>
+ › {{ namespace.name }}
+</div>
+{% endblock %}
+
+
+{% block content %}
+
+{% if namespace.is_global %}
+<div class="module filtered" id="changelist">
+<div id="changelist-filter">
+ <h2>Clusters</h2>
+<ul>
+ <li {% if not selectedCluster %}class="selected"{% endif %}><a href="?">All</a></li>
+
+ {% for cluster in namespace.clusters.all %}
+ <li {% if selectedCluster == cluster.name %}class="selected"{% endif %}
+ ><a href="?cluster={{cluster}}">{{cluster}}</a></li>
+ {% endfor %}
+
+ <li>
+</ul>
+</div>
+</div>
+{% endif %}
+
+<table>
+<thead>
+ <tr>
+ {% if namespace.is_global %}
+ {% column_header topics 'cluster__name' 'Cluster' %}
+ {% endif %}
+ {% column_header topics 'name' 'Topic' %}
+ {% column_header topics 'msgRateIn' 'Msg/s in' %}
+ {% column_header topics 'msgRateOut' 'Msg/s out' %}
+ {% column_header topics 'msgThroughputIn' 'Bytes/s in' %}
+ {% column_header topics 'msgThroughputOut' 'Bytes/s out' %}
+ {% column_header topics 'backlog' 'Backlog' %}
+ {% column_header topics 'broker' 'Broker' %}
+ </tr>
+</thead>
+<tbody>
+
+{% for topic in topics.results %}
+ <tr class="{% cycle 'row1' 'row2' %}">
+ {% if namespace.is_global %}
+ <th>{{topic.cluster}}</td>
+ {% endif %}
+
+ <th><a href="{{topic.get_absolute_url}}">{{topic.short_name}}</a></th>
+ <td>{{topic.msgRateIn | intcomma}}</td>
+ <td>{{topic.msgRateOut | intcomma}}</td>
+ <td>{{topic.msgThroughputIn | intcomma}}</td>
+ <td>{{topic.msgThroughputOut | intcomma}}</td>
+ <td>{{topic.backlog | intcomma}}</td>
+ <td><span title="{{topic.broker | escape}}">{{topic.broker | escape | truncatechars:20 }}</span></td>
+ </tr>
+{% empty %}
+ <tr><td>No topics</td></tr>
+{% endfor %}
+</tbody>
+</table>
+
+{% table_footer topics %}
+
+
+{% endblock %}
diff --git a/docker/pulsar-standalone/django/stats/templates/stats/property.html b/docker/pulsar-standalone/django/stats/templates/stats/property.html
new file mode 100644
index 0000000..7958919
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/templates/stats/property.html
@@ -0,0 +1,72 @@
+<!--
+
+ 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.
+
+-->
+{% extends "stats/base.html" %}
+
+{% load humanize %}
+{% load table %}
+
+{% block title %}Property | {{property.name}}{% endblock %}
+
+{% block content %}
+
+<table>
+<thead>
+ <tr>
+ {% column_header namespaces 'name' 'Namespace' %}
+ {% column_header namespaces 'numTopics' 'Topics' %}
+ {% column_header namespaces 'numProducers' 'Producers' %}
+ {% column_header namespaces 'numSubscriptions' 'Subscriptions' %}
+ {% column_header namespaces 'numConsumers' 'Consumers' %}
+ {% column_header namespaces 'rateIn' 'Rate In' %}
+ {% column_header namespaces 'rateOut' 'Rate Out' %}
+ {% column_header namespaces 'throughputIn' 'Throughput In' %}
+ {% column_header namespaces 'throughputOut' 'Throughput Out' %}
+ {% column_header namespaces 'backlog' 'Backlog' %}
+ {% column_header namespaces 'storage' 'Storage' %}
+ </tr>
+</thead>
+<tbody>
+
+{% for namespace in namespaces.results %}
+ <tr class="{% cycle 'row1' 'row2' %}">
+ <th>
+ <a href="{% url 'namespace' namespace.name %}">{{namespace.name}}</a>
+ </th>
+ <td>{{namespace.numTopics | intcomma}}</td>
+ <td>{{namespace.numProducers | intcomma}}</td>
+ <td>{{namespace.numSubscriptions | intcomma}}</td>
+ <td>{{namespace.numConsumers | intcomma}}</td>
+ <td>{{namespace.rateIn | intcomma}}</td>
+ <td>{{namespace.rateOut | intcomma}}</td>
+ <td>{{namespace.throughputIn | intcomma}}</td>
+ <td>{{namespace.throughputOut | intcomma}}</td>
+ <td>{{namespace.backlog | intcomma}}</td>
+ <td>{{namespace.storage | filesizeformat}}</td>
+ </tr>
+{% empty %}
+ <tr><td>No namespaces</td></tr>
+{% endfor %}
+</tbody>
+</table>
+
+{% table_footer namespaces %}
+
+{% endblock %}
diff --git a/docker/pulsar-standalone/django/stats/templates/stats/topic.html b/docker/pulsar-standalone/django/stats/templates/stats/topic.html
new file mode 100644
index 0000000..467b5a7
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/templates/stats/topic.html
@@ -0,0 +1,197 @@
+<!--
+
+ 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.
+
+-->
+{% extends "stats/base.html" %}
+
+{% load humanize %}
+{% load stats_extras %}
+
+{% block title %}Topic | {{topic.name}}{% endblock %}
+
+{% block breadcrumbs %}
+<div class="breadcrumbs">
+ <a href="{% url 'home' %}">Home</a>
+ › <a href="{% url 'property' topic.namespace.property.name %}">{{ topic.namespace.property }}</a>
+ › <a href="{% url 'namespace' topic.namespace.name %}">{{ topic.namespace.name }}</a>
+ › {{ topic.short_name }}
+</div>
+{% endblock %}
+
+{% block content %}
+
+{% if topic.is_global %}
+<div class="module filtered" id="changelist">
+<div id="changelist-filter">
+ <h2>Cluster</h2>
+<ul>
+ {% for cluster in clusters %}
+ <li {% if selectedCluster == cluster.name %}class="selected"{% endif %}
+ ><a href="{% url 'topic' topic.url_name %}?cluster={{cluster}}">{{cluster}}</a></li>
+ {% endfor %}
+
+ <li>
+</ul>
+</div>
+</div>
+{% endif %}
+
+
+<h2>Stats</h2>
+
+<table>
+ <tr class="row1"><th>Average msg size</th><td>{{topic.averageMsgSize | file_size_value}}</td>
+ <td>{{topic.averageMsgSize | file_size_unit}}</td></tr>
+ <tr class="row2"><th>Rate in</th><td>{{topic.msgRateIn | intcomma }}</td><td>msg/s</td></tr>
+ <tr class="row1"><th>Rate out</th><td>{{topic.msgRateOut | intcomma }}</td><td>msg/s</td></tr>
+
+ <tr class="row2"><th>Throughput in</th><td>{{topic.msgThroughputIn | file_size_value }}</td>
+ <td>{{topic.msgThroughputIn | file_size_unit }} / sec</td></tr>
+ <tr class="row1"><th>Throughput out</th><td>{{topic.msgThroughputOut | file_size_value }}</td>
+ <td>{{topic.msgThroughputOut | file_size_unit }} / sec</td></tr>
+
+ <tr class="row2"><th>Pending add entries</th><td>{{topic.pendingAddEntriesCount | intcomma }}</td><td></td></tr>
+ <tr class="row1"><th>Producer count</th><td>{{topic.producerCount | intcomma }}</td><td></td></tr>
+
+ <tr class="row2"><th>Storage size</th><td>{{topic.storageSize | file_size_value}}</td>
+ <td>{{topic.storageSize | file_size_unit}}</td></tr>
+ <tr class="row1">
+ <th>Broker</th>
+ <td colspan="2">
+ <a href="{% url 'broker' topic.broker.url %}">{{topic.broker | escape }}</a>
+ </td>
+ </tr>
+</table>
+
+<h2>Subscriptions</h2>
+
+<table>
+<thead>
+ <tr>
+ <th>Subscription</th>
+ <th>Type</th>
+ <th title="messages">Backlog</th>
+ <th title="Msg/s">Rate out</th>
+ <th title="Bytes/s">Throughput out</th>
+ <th title="Msg/s">Rate expired</th>
+ <th title="Msg/s">Unacked</th>
+ </tr>
+</thead>
+
+<tbody
+{% for sub, consumers in subscriptions %}
+ <tr class="{% cycle 'row1' 'row2' %}">
+ {% if consumers %}
+ <th><a data-toggle="#consumers-{{sub.id}}" href="#">{{sub.name}}</a></th>
+ {% else %}
+ <th>{{sub.name}}</th>
+ {% endif %}
+ <td>{{sub.get_subscriptionType_display}}</td>
+ <td>{{sub.msgBacklog | intcomma }}</td>
+ <td>{{sub.msgRateOut | intcomma}}</td>
+ <td>{{sub.msgThroughputOut | intcomma}}</td>
+ <td>{{sub.msgRateExpired | intcomma}}</td>
+ <td>{{sub.unackedMessages | intcomma}}</td>
+ </tr>
+
+ <tr class="{% cycle 'row1' 'row2' %}" id="consumers-{{sub.id}}"
+ style="display:none">
+ <td></td>
+ <td colspan="6">
+ <table>
+ <thead>
+ <tr>
+ <th>Consumer</th>
+ <th>Address</th>
+ <th title="Msg/s">Rate out</th>
+ <th title="bytes/s">Throughput out</th>
+ <th title="Msg/s">Redelivery rate</th>
+ <th>Connected since</th>
+ <th>Available permits</th>
+ <th title="Messages">Unacked</th>
+ <th title="Blocked for max unacked messages">Blocked</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for consumerStats in consumers %}
+ <tr>
+ <td>{{consumerStats.consumerName}}</td>
+ <td>{{consumerStats.address}}</td>
+ <td>{{consumerStats.msgRateOut | intcomma}}</td>
+ <td>{{consumerStats.msgThroughputOut | intcomma}}</td>
+ <td>{{consumerStats.msgRateRedeliver | intcomma}}</td>
+ <td title="{{consumerStats.connectedSince}} UTC">{{consumerStats.connectedSince | naturaltime}}</td>
+ <td>{{consumerStats.availablePermits | intcomma}}</td>
+ <td>{{consumerStats.unackedMessages | intcomma}}</td>
+ <td>{{consumerStats.blockedConsumerOnUnackedMsgs | yesno }}</td>
+ </tr>
+ {% empty %}
+ <tr><td>No consumers</tr></td>
+ {% endfor %}
+ </tbody>
+ </table>
+ </td>
+ </tr>
+{% empty %}
+ <tr><td>No subscriptions</tr></td>
+{% endfor %}
+</tbody>
+</table>
+
+{%if topic.is_global%}
+
+<h2>Replication from {{topic.cluster}}</h2>
+
+<table>
+<thead>
+ <tr>
+ <th>Remote Cluster</th>
+ <th title="Msg/s">Rate in</th>
+ <th title="Msg/s">Rate out</th>
+ <th>Mbps in</th>
+ <th>Mbps out</th>
+ <th title="Messages">Replication backlog</th>
+ </tr>
+</thead>
+<tbody>
+{% for peer in peers %}
+ <tr>
+ <td>{{peer.remote_cluster__name}}</td>
+ <td title="{{peer.remote_cluster__name}} ⟶ {{selectedCluster}}">{{peer.msgRateIn__sum | intcomma}}</td>
+ <td title="{{selectedCluster}} ⟶ {{peer.remote_cluster__name}}">{{peer.msgRateOut__sum | intcomma}}</td>
+ <td title="{{peer.remote_cluster__name}} ⟶ {{selectedCluster}}">{{peer.msgThroughputIn__sum | mbps | floatformat | intcomma}}</td>
+ <td title="{{selectedCluster}} ⟶ {{peer.remote_cluster__name}}">{{peer.msgThroughputOut__sum | mbps | floatformat | intcomma}}</td>
+ <td title="{{selectedCluster}} ⟶ {{peer.remote_cluster__name}}">{{peer.replicationBacklog__sum | intcomma}}</td>
+ </tr>
+{% empty %}
+ <tr><td>No replication</tr></td>
+{% endfor %}
+</tbody>
+</table>
+{% endif %}
+
+<script>
+$("a[data-toggle]").on("click", function(e) {
+ e.preventDefault(); // prevent navigating
+ var selector = $(this).data("toggle"); // get corresponding selector from data-toggle
+ $(selector).toggle( "slow", function() {
+ });
+});
+</script>
+{% endblock %}
diff --git a/docker/pulsar-standalone/django/stats/templates/stats/topics.html b/docker/pulsar-standalone/django/stats/templates/stats/topics.html
new file mode 100644
index 0000000..021443c
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/templates/stats/topics.html
@@ -0,0 +1,91 @@
+<!--
+
+ 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.
+
+-->
+{% extends "stats/base.html" %}
+{% load humanize %}
+{% load table %}
+
+{% block title %}Namespace | {{property.name}}{% endblock %}
+
+{% block breadcrumbs %}
+<div class="breadcrumbs">
+ <a href="{% url 'home' %}">Home</a>
+ › all topics
+</div>
+{% endblock %}
+
+
+{% block content %}
+
+<div class="module filtered" id="changelist">
+<div id="changelist-filter">
+ <h2>Clusters</h2>
+<ul>
+ <li {% if not selectedCluster %}class="selected"{% endif %}><a href="?">All</a></li>
+
+ {% for cluster in clusters %}
+ <li {% if selectedCluster == cluster.name %}class="selected"{% endif %}
+ ><a href="?cluster={{cluster}}">{{cluster}}</a></li>
+ {% endfor %}
+
+ <li>
+</ul>
+</div>
+</div>
+
+<table>
+<thead>
+ <tr>
+ {% column_header topics 'cluster__name' 'Cluster' %}
+ {% column_header topics 'namespace__name' 'Namespace' %}
+ {% column_header topics 'name' 'Topic' %}
+ {% column_header topics 'msgRateIn' 'Msg/s in' %}
+ {% column_header topics 'msgRateOut' 'Msg/s out' %}
+ {% column_header topics 'msgThroughputIn' 'Bytes/s in' %}
+ {% column_header topics 'msgThroughputOut' 'Bytes/s out' %}
+ {% column_header topics 'backlog' 'Backlog' %}
+ {% column_header topics 'broker' 'Broker' %}
+ </tr>
+</thead>
+<tbody>
+
+{% for topic in topics.results %}
+ <tr class="{% cycle 'row1' 'row2' %}">
+ <th>{{topic.cluster}}</td>
+ <th><a href="{% url 'namespace' topic.namespace.name %}">{{topic.namespace}}</a></th>
+ <th><a href="{{topic.get_absolute_url}}">{{topic.short_name}}</a></th>
+ <td>{{topic.msgRateIn | intcomma}}</td>
+ <td>{{topic.msgRateOut | intcomma}}</td>
+ <td>{{topic.msgThroughputIn | intcomma}}</td>
+ <td>{{topic.msgThroughputOut | intcomma}}</td>
+ <td>{{topic.backlog | intcomma}}</td>
+ <td title="{{topic.broker | escape}}"><a href="{% url 'broker' topic.broker %}">
+ {{topic.broker | escape | truncatechars:20 }}</a></td>
+ </tr>
+{% empty %}
+ <tr><td>No topics</td></tr>
+{% endfor %}
+</tbody>
+</table>
+
+{% table_footer topics %}
+
+
+{% endblock %}
diff --git a/docker/pulsar-standalone/django/stats/templatetags/__init__.py b/docker/pulsar-standalone/django/stats/templatetags/__init__.py
new file mode 100644
index 0000000..d8a500d
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/templatetags/__init__.py
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+
diff --git a/docker/pulsar-standalone/django/stats/templatetags/stats_extras.py b/docker/pulsar-standalone/django/stats/templatetags/stats_extras.py
new file mode 100644
index 0000000..78f7427
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/templatetags/stats_extras.py
@@ -0,0 +1,64 @@
+#
+# 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 django import template
+from django.utils import formats
+from django.contrib.humanize.templatetags.humanize import intcomma
+
+register = template.Library()
+
+KB = 1 << 10
+MB = 1 << 20
+GB = 1 << 30
+TB = 1 << 40
+PB = 1 << 50
+
+def fmt(x):
+ return str(formats.number_format(round(x, 1), 1))
+
+@register.filter(name='file_size_value')
+def file_size_value(bytes_):
+ bytes_ = float(bytes_)
+ if bytes_ < KB: return str(bytes_)
+ elif bytes_ < MB: return fmt(bytes_ / KB)
+ elif bytes_ < GB: return fmt(bytes_ / MB)
+ elif bytes_ < TB: return fmt(bytes_ / GB)
+ elif bytes_ < PB: return fmt(bytes_ / TB)
+ else: return fmt(bytes_ / PB)
+
+@register.filter(name='file_size_unit')
+def file_size_unit(bytes_):
+ if bytes_ < KB: return 'bytes'
+ elif bytes_ < MB: return 'KB'
+ elif bytes_ < GB: return 'MB'
+ elif bytes_ < TB: return 'GB'
+ elif bytes_ < PB: return 'TB'
+ else: return 'PB'
+
+
+@register.filter(name='mbps')
+def mbps(bytes_per_seconds):
+ if not bytes_per_seconds: return 0.0
+ else: return float(bytes_per_seconds) * 8 / 1024 / 1024
+
+@register.filter(name='safe_intcomma')
+def safe_intcomma(n):
+ if not n: return 0
+ else: return intcomma(n)
diff --git a/docker/pulsar-standalone/django/stats/templatetags/table.py b/docker/pulsar-standalone/django/stats/templatetags/table.py
new file mode 100644
index 0000000..2613092
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/templatetags/table.py
@@ -0,0 +1,91 @@
+#
+# 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 django import template
+from django.utils.html import format_html
+from django.utils.safestring import mark_safe
+from django.core.paginator import Paginator
+
+register = template.Library()
+
+@register.simple_tag
+def column_header(table, column, text):
+ selected = 'style="color: #D84A24"'
+ if table.sort == column:
+ sort = '-' + column
+ arrow = '↓'
+ elif table.sort == ('-' + column):
+ sort = column
+ arrow = '↑'
+ else:
+ sort = '-' + column
+ arrow = ''
+ selected = ''
+
+ params = dict(table.request.GET)
+ params['sort'] = [sort]
+ params_str = '&'.join( (k + '=' + v[0]) for k,v in params.items())
+
+ return format_html('<th><a href="?{}"><span {}>{} {}</span></a></th>\n',
+ params_str,
+ mark_safe(selected),
+ mark_safe(arrow),
+ text
+ )
+
+@register.simple_tag
+def table_footer(table):
+ if table.show_all or table.paginator.num_pages == 1:
+ return ''
+
+ params = dict(table.request.GET)
+
+ footer = '<p class="paginator">'
+ for page in table.paginator.page_range:
+ if page == table.page:
+ footer += '<span class="this-page">%d</span>\n' % page
+ else:
+ params['page'] = [str(page)]
+ params_str = '&'.join( (k + '=' + v[0]) for k,v in params.items())
+ footer += '<a href="?%s">%d</a>\n' % (params_str, page)
+
+ footer += ' Total: %d\n' % table.paginator.count
+
+ del params['page']
+ params['show-all'] = '1'
+ params_str = '&'.join( (k + '=' + v[0]) for k,v in params.items())
+ footer += ' | <a href="?%s">Show all</a>' % params_str
+ footer += '</p>'
+ return mark_safe(footer)
+
+
+class Table:
+ def __init__(self, request, queryset, default_sort, default_page_size=25):
+ self.request = request
+ self.sort = request.GET.get('sort', default_sort)
+ self.page_size = int(request.GET.get('page-size', default_page_size))
+ self.page = int(request.GET.get('page', 1))
+ self.results = queryset.order_by(self.sort)
+ self.show_all = request.GET.get('show-all', False)
+
+ if not self.show_all:
+ # Paginate results unless explicitely turned off
+ self.paginator = Paginator(self.results, self.page_size)
+ self.results = self.paginator.page(self.page)
diff --git a/docker/pulsar-standalone/django/stats/tests.py b/docker/pulsar-standalone/django/stats/tests.py
new file mode 100644
index 0000000..8cdbd9c
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/tests.py
@@ -0,0 +1,22 @@
+#
+# 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 django.test import TestCase
+
+# Create your tests here.
diff --git a/docker/pulsar-standalone/django/stats/urls.py b/docker/pulsar-standalone/django/stats/urls.py
new file mode 100644
index 0000000..0c488bd
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/urls.py
@@ -0,0 +1,37 @@
+#
+# 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 django.conf.urls import url
+
+from . import views
+
+urlpatterns = [
+ url(r'^property/(?P<property_name>.+)/$', views.property, name='property'),
+ url(r'^namespace/(?P<namespace_name>.+)/$', views.namespace, name='namespace'),
+
+
+ url(r'^brokers/$', views.brokers, name='brokers'),
+ url(r'^brokers/(?P<cluster_name>.+)/$', views.brokers_cluster, name='brokers_cluster'),
+ url(r'^broker/(?P<broker_url>.+)/$', views.broker, name='broker'),
+
+ url(r'^topics/$', views.topics, name='topics'),
+ url(r'^topic/(?P<topic_name>.+)/$', views.topic, name='topic'),
+
+ url(r'^clusters/$', views.clusters, name='clusters'),
+]
diff --git a/docker/pulsar-standalone/django/stats/views.py b/docker/pulsar-standalone/django/stats/views.py
new file mode 100644
index 0000000..426e9b1
--- /dev/null
+++ b/docker/pulsar-standalone/django/stats/views.py
@@ -0,0 +1,283 @@
+#
+# 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 django.shortcuts import render, get_object_or_404
+from django.template import loader
+from django.urls import reverse
+from django.views import generic
+from django.db.models import Q
+
+from django.http import HttpResponseRedirect, HttpResponse
+from .models import *
+
+from stats.templatetags.table import Table
+
+def get_timestamp():
+ try:
+ return LatestTimestamp.objects.get(name='latest').timestamp
+ except:
+ return 0
+
+class HomeView(generic.ListView):
+ model = Property
+ template_name = 'stats/home.html'
+
+def home(request):
+ ts = get_timestamp()
+ properties = Property.objects.filter(
+ namespace__topic__timestamp = ts,
+ ).annotate(
+ numNamespaces = Count('namespace__name', distinct=True),
+ numTopics = Count('namespace__topic__name', distinct=True),
+ numProducers = Sum('namespace__topic__producerCount'),
+ numSubscriptions = Sum('namespace__topic__subscriptionCount'),
+ numConsumers = Sum('namespace__topic__consumerCount'),
+ backlog = Sum('namespace__topic__backlog'),
+ storage = Sum('namespace__topic__storageSize'),
+ rateIn = Sum('namespace__topic__msgRateIn'),
+ rateOut = Sum('namespace__topic__msgRateOut'),
+ throughputIn = Sum('namespace__topic__msgThroughputIn'),
+ throughputOut = Sum('namespace__topic__msgThroughputOut'),
+ )
+
+ print properties.query
+
+ properties = Table(request, properties, default_sort='name')
+
+ return render(request, 'stats/home.html', {
+ 'properties': properties,
+ 'title' : 'Properties',
+ })
+
+def property(request, property_name):
+ property = get_object_or_404(Property, name=property_name)
+ ts = get_timestamp()
+ namespaces = Namespace.objects.filter(
+ property = property,
+ topic__timestamp = ts,
+ ).annotate(
+ numTopics = Count('topic'),
+ numProducers = Sum('topic__producerCount'),
+ numSubscriptions = Sum('topic__subscriptionCount'),
+ numConsumers = Sum('topic__consumerCount'),
+ backlog = Sum('topic__backlog'),
+ storage = Sum('topic__storageSize'),
+ rateIn = Sum('topic__msgRateIn'),
+ rateOut = Sum('topic__msgRateOut'),
+ throughputIn = Sum('topic__msgThroughputIn'),
+ throughputOut = Sum('topic__msgThroughputOut'),
+ )
+
+ namespaces = Table(request, namespaces, default_sort='name')
+
+ return render(request, 'stats/property.html', {
+ 'property': property,
+ 'namespaces' : namespaces,
+ 'title' : property.name,
+ })
+
+
+def namespace(request, namespace_name):
+ selectedClusterName = request.GET.get('cluster')
+
+ namespace = get_object_or_404(Namespace, name=namespace_name)
+ topics = Topic.objects.select_related('broker', 'namespace', 'cluster')
+ if selectedClusterName:
+ topics = topics.filter(
+ namespace = namespace,
+ timestamp = get_timestamp(),
+ cluster__name = selectedClusterName
+ )
+ else:
+ topics = topics.filter(
+ namespace = namespace,
+ timestamp = get_timestamp()
+ )
+
+ topics = Table(request, topics, default_sort='name')
+ return render(request, 'stats/namespace.html', {
+ 'namespace' : namespace,
+ 'topics' : topics,
+ 'title' : namespace.name,
+ 'selectedCluster' : selectedClusterName,
+ })
+
+
+def topic(request, topic_name):
+ timestamp = get_timestamp()
+ topic_name = 'persistent://' + topic_name.split('persistent/', 1)[1]
+ cluster_name = request.GET.get('cluster')
+ clusters = []
+
+ if cluster_name:
+ topic = get_object_or_404(Topic, name=topic_name, cluster__name=cluster_name, timestamp=timestamp)
+ clusters = [x.cluster for x in Topic.objects.filter(name=topic_name, timestamp=timestamp).order_by('cluster__name')]
+ else:
+ topic = get_object_or_404(Topic, name=topic_name, timestamp=timestamp)
+ subscriptions = Subscription.objects.filter(topic=topic).order_by('name')
+
+ subs = []
+
+ for sub in subscriptions:
+ consumers = Consumer.objects.filter(subscription=sub).order_by('address')
+ subs.append((sub, consumers))
+
+ if topic.is_global():
+ peers_clusters = Replication.objects.filter(
+ timestamp = timestamp,
+ local_cluster__name = cluster_name
+ ).values('remote_cluster__name'
+ ).annotate(
+ Sum('msgRateIn'),
+ Sum('msgThroughputIn'),
+ Sum('msgRateOut'),
+ Sum('msgThroughputOut'),
+ Sum('replicationBacklog')
+ )
+ else:
+ peers_clusters = []
+
+ return render(request, 'stats/topic.html', {
+ 'topic' : topic,
+ 'subscriptions' : subs,
+ 'title' : topic.name,
+ 'selectedCluster' : cluster_name,
+ 'clusters' : clusters,
+ 'peers' : peers_clusters,
+ })
+
+def topics(request):
+ selectedClusterName = request.GET.get('cluster')
+
+ topics = Topic.objects.select_related('broker', 'namespace', 'cluster')
+ if selectedClusterName:
+ topics = topics.filter(
+ timestamp = get_timestamp(),
+ cluster__name = selectedClusterName
+ )
+ else:
+ topics = topics.filter(
+ timestamp = get_timestamp()
+ )
+
+ topics = Table(request, topics, default_sort='cluster__name')
+ return render(request, 'stats/topics.html', {
+ 'clusters' : Cluster.objects.all(),
+ 'topics' : topics,
+ 'title' : 'Topics',
+ 'selectedCluster' : selectedClusterName,
+ })
+
+
+def brokers(request):
+ return brokers_cluster(request, None)
+
+def brokers_cluster(request, cluster_name):
+ ts = get_timestamp()
+
+ brokers = Broker.objects
+ if cluster_name:
+ brokers = brokers.filter(
+ Q(topic__timestamp=ts) | Q(topic__timestamp__isnull=True),
+ activebroker__timestamp = ts,
+ cluster__name = cluster_name,
+
+ )
+ else:
+ brokers = brokers.filter(
+ Q(topic__timestamp=ts) | Q(topic__timestamp__isnull=True),
+ activebroker__timestamp = ts
+ )
+
+ brokers = brokers.annotate(
+ numBundles = Count('topic__bundle'),
+ numTopics = Count('topic'),
+ numProducers = Sum('topic__producerCount'),
+ numSubscriptions = Sum('topic__subscriptionCount'),
+ numConsumers = Sum('topic__consumerCount'),
+ backlog = Sum('topic__backlog'),
+ storage = Sum('topic__storageSize'),
+ rateIn = Sum('topic__msgRateIn'),
+ rateOut = Sum('topic__msgRateOut'),
+ throughputIn = Sum('topic__msgThroughputIn'),
+ throughputOut = Sum('topic__msgThroughputOut'),
+ )
+
+ brokers = Table(request, brokers, default_sort='url')
+
+ return render(request, 'stats/brokers.html', {
+ 'clusters' : Cluster.objects.all(),
+ 'brokers' : brokers,
+ 'selectedCluster' : cluster_name,
+ })
+
+def broker(request, broker_url):
+ broker = Broker.objects.get(url = broker_url)
+ topics = Topic.objects.filter(
+ timestamp = get_timestamp(),
+ broker__url = broker_url
+ )
+
+ topics = Table(request, topics, default_sort='namespace__name')
+ return render(request, 'stats/broker.html', {
+ 'topics' : topics,
+ 'title' : 'Broker - %s - %s' % (broker.cluster, broker_url),
+ 'broker_url' : broker_url
+ })
+
+
+def clusters(request):
+ ts = get_timestamp()
+
+ clusters = Cluster.objects.filter(
+ topic__timestamp = ts
+ )
+
+ clusters = clusters.annotate(
+ numTopics = Count('topic'),
+ localBacklog = Sum('topic__backlog'),
+ replicationBacklog = Sum('topic__replicationBacklog'),
+ storage = Sum('topic__storageSize'),
+ localRateIn = Sum('topic__localRateIn'),
+ localRateOut = Sum('topic__localRateOut'),
+ replicationRateIn = Sum('topic__replicationRateIn'),
+ replicationRateOut = Sum('topic__replicationRateOut'),
+ )
+
+ clusters = Table(request, clusters, default_sort='name')
+
+ for cluster in clusters.results:
+ # Fetch per-remote peer stats
+ peers = Replication.objects.filter(
+ timestamp = ts,
+ local_cluster = cluster,
+ ).values('remote_cluster__name')
+
+ peers = peers.annotate(
+ Sum('msgRateIn'),
+ Sum('msgThroughputIn'),
+ Sum('msgRateOut'),
+ Sum('msgThroughputOut'),
+ Sum('replicationBacklog')
+ )
+ cluster.peers = peers
+
+ return render(request, 'stats/clusters.html', {
+ 'clusters' : clusters,
+ })
diff --git a/docker/pulsar-standalone/pom.xml b/docker/pulsar-standalone/pom.xml
new file mode 100644
index 0000000..716c141
--- /dev/null
+++ b/docker/pulsar-standalone/pom.xml
@@ -0,0 +1,81 @@
+<!--
+
+ 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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.apache.pulsar</groupId>
+ <artifactId>docker-images</artifactId>
+ <version>2.2.0-incubating-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.apache.pulsar</groupId>
+ <artifactId>pulsar-standalone-docker-image</artifactId>
+ <name>Apache Pulsar :: Docker Images :: Pulsar Standalone</name>
+ <packaging>pom</packaging>
+
+ <profiles>
+ <profile>
+ <id>docker</id>
+ <!-- include the docker image only when docker profile is active -->
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.pulsar</groupId>
+ <artifactId>pulsar-all-docker-image</artifactId>
+ <version>${project.parent.version}</version>
+ <classifier>docker-info</classifier>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.spotify</groupId>
+ <artifactId>dockerfile-maven-plugin</artifactId>
+ <version>${dockerfile-maven.version}</version>
+ <executions>
+ <execution>
+ <id>default</id>
+ <goals>
+ <goal>build</goal>
+ </goals>
+ </execution>
+ <execution>
+ <id>tag-and-push-latest</id>
+ <goals>
+ <goal>tag</goal>
+ <goal>push</goal>
+ </goals>
+ <configuration>
+ <repository>${docker.organization}/pulsar-standalone</repository>
+ <tag>latest</tag>
+ </configuration>
+ </execution>
+ </executions>
+ <configuration>
+ <repository>${docker.organization}/pulsar-standalone</repository>
+ <pullNewerImage>false</pullNewerImage>
+ <tag>${project.version}</tag>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
diff --git a/site2/docs/getting-started-standalone.md b/site2/docs/getting-started-standalone.md
index 3f95cdd..efafb22 100644
--- a/site2/docs/getting-started-standalone.md
+++ b/site2/docs/getting-started-standalone.md
@@ -9,12 +9,14 @@ For the purposes of local development and testing, you can run Pulsar in standal
> #### Pulsar in production?
> If you're looking to run a full production Pulsar installation, see the [Deploying a Pulsar instance](deploy-bare-metal.md) guide.
-## System requirements
+## Run Pulsar Standalone Manually
+
+### System requirements
Pulsar is currently available for **MacOS** and **Linux**. In order to use Pulsar, you'll need to install [Java 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html).
-## Installing Pulsar
+### Installing Pulsar
To get started running Pulsar, download a binary tarball release in one of the following ways:
@@ -37,7 +39,7 @@ $ tar xvfz apache-pulsar-{{pulsar:version}}-bin.tar.gz
$ cd apache-pulsar-{{pulsar:version}}
```
-## What your package contains
+### What your package contains
The Pulsar binary package initially contains the following directories:
@@ -58,7 +60,7 @@ Directory | Contains
`logs` | Logs created by the installation
-## Installing Builtin Connectors
+### Installing Builtin Connectors
Since release `2.1.0-incubating`, Pulsar releases a separate binary distribution, containing all the `builtin` connectors.
If you would like to enable those `builtin` connectors, you can download the connectors tarball release in one of the following ways:
@@ -104,7 +106,7 @@ pulsar-io-twitter-{{pulsar:version}}.nar
> If you are [running Pulsar in Docker](getting-started-docker.md) or deploying Pulsar using a docker image (e.g. [K8S](deploy-kubernetes.md) or [DCOS](deploy-dcos.md)),
> you can use `apachepulsar/pulsar-all` image instead of `apachepulsar/pulsar` image. `apachepulsar/pulsar-all` image has already bundled [all builtin connectors](io-overview.md#working-with-connectors).
-## Starting the cluster
+### Starting the cluster
Once you have an up-to-date local copy of the release, you can start up a local cluster using the [`pulsar`](reference-cli-tools.md#pulsar) command, which is stored in the `bin` directory, and specifying that you want to start up Pulsar in standalone mode:
@@ -123,6 +125,21 @@ If Pulsar has been successfully started, you should see `INFO`-level log message
> #### Automatically created namespace
> When you start a local standalone cluster, Pulsar will automatically create a `public/default` [namespace](concepts-messaging.md#namespaces) that you can use for development purposes. All Pulsar topics are managed within namespaces. For more info, see [Topics](concepts-messaging.md#topics).
+## Run Pulsar Standalone in Docker
+
+Alternatively, you can run pulsar standalone locally in docker.
+
+```bash
+docker run -it -p 80:80 -p 8080:8080 -p 6650:6650 apachepulsar/pulsar-standalone
+```
+
+The command forwards following port to localhost:
+
+- 80: the port for pulsar dashboard
+- 8080: the http service url for pulsar service
+- 6650: the binary protocol service url for pulsar service
+
+After the docker container is running, you can access the dashboard under http://localhost .
## Testing your cluster setup
diff --git a/site2/website/versioned_docs/version-2.1.0-incubating/getting-started-standalone.md b/site2/website/versioned_docs/version-2.1.0-incubating/getting-started-standalone.md
index 29cf000..38d5e02 100644
--- a/site2/website/versioned_docs/version-2.1.0-incubating/getting-started-standalone.md
+++ b/site2/website/versioned_docs/version-2.1.0-incubating/getting-started-standalone.md
@@ -10,12 +10,15 @@ For the purposes of local development and testing, you can run Pulsar in standal
> #### Pulsar in production?
> If you're looking to run a full production Pulsar installation, see the [Deploying a Pulsar instance](deploy-bare-metal.md) guide.
-## System requirements
+
+## Run Pulsar Standalone Manually
+
+### System requirements
Pulsar is currently available for **MacOS** and **Linux**. In order to use Pulsar, you'll need to install [Java 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html).
-## Installing Pulsar
+### Installing Pulsar
To get started running Pulsar, download a binary tarball release in one of the following ways:
@@ -38,7 +41,7 @@ $ tar xvfz apache-pulsar-{{pulsar:version}}-bin.tar.gz
$ cd apache-pulsar-{{pulsar:version}}
```
-## What your package contains
+### What your package contains
The Pulsar binary package initially contains the following directories:
@@ -59,7 +62,7 @@ Directory | Contains
`logs` | Logs created by the installation
-## Installing Builtin Connectors
+### Installing Builtin Connectors
Since release `2.1.0-incubating`, Pulsar releases a separate binary distribution, containing all the `builtin` connectors.
If you would like to enable those `builtin` connectors, you can download the connectors tarball release in one of the following ways:
@@ -105,7 +108,7 @@ pulsar-io-twitter-{{pulsar:version}}.nar
> If you are [running Pulsar in Docker](getting-started-docker.md) or deploying Pulsar using a docker image (e.g. [K8S](deploy-kubernetes.md) or [DCOS](deploy-dcos.md)),
> you can use `apachepulsar/pulsar-all` image instead of `apachepulsar/pulsar` image. `apachepulsar/pulsar-all` image has already bundled [all builtin connectors](io-overview.md#working-with-connectors).
-## Starting the cluster
+### Starting the cluster
Once you have an up-to-date local copy of the release, you can start up a local cluster using the [`pulsar`](reference-cli-tools.md#pulsar) command, which is stored in the `bin` directory, and specifying that you want to start up Pulsar in standalone mode:
@@ -124,7 +127,6 @@ If Pulsar has been successfully started, you should see `INFO`-level log message
> #### Automatically created namespace
> When you start a local standalone cluster, Pulsar will automatically create a `public/default` [namespace](concepts-messaging.md#namespaces) that you can use for development purposes. All Pulsar topics are managed within namespaces. For more info, see [Topics](concepts-messaging.md#topics).
-
## Testing your cluster setup
Pulsar provides a CLI tool called [`pulsar-client`](reference-cli-tools.md#pulsar-client) that enables you to do things like send messages to a Pulsar topic in a running cluster. This command will send a simple message saying `hello-pulsar` to the `my-topic` topic: