Commit f591bea6 by Torkel Ödegaard

Merge branch 'graphite-series-by-tags'

parents fe42f814 26577ab6
FROM phusion/baseimage:0.9.22
MAINTAINER Denys Zhdanov <denis.zhdanov@gmail.com>
RUN apt-get -y update \
&& apt-get -y upgrade \
&& apt-get -y install vim \
nginx \
python-dev \
python-flup \
python-pip \
python-ldap \
expect \
git \
memcached \
sqlite3 \
libffi-dev \
libcairo2 \
libcairo2-dev \
python-cairo \
python-rrdtool \
pkg-config \
nodejs \
&& rm -rf /var/lib/apt/lists/*
# choose a timezone at build-time
# use `--build-arg CONTAINER_TIMEZONE=Europe/Brussels` in `docker build`
ARG CONTAINER_TIMEZONE
ENV DEBIAN_FRONTEND noninteractive
RUN if [ ! -z "${CONTAINER_TIMEZONE}" ]; \
then ln -sf /usr/share/zoneinfo/$CONTAINER_TIMEZONE /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata; \
fi
# fix python dependencies (LTS Django and newer memcached/txAMQP)
RUN pip install --upgrade pip && \
pip install django==1.8.18 \
python-memcached==1.53 \
txAMQP==0.6.2
ARG version=1.0.2
ARG whisper_version=${version}
ARG carbon_version=${version}
ARG graphite_version=${version}
ARG statsd_version=v0.7.2
# install whisper
RUN git clone -b ${whisper_version} --depth 1 https://github.com/graphite-project/whisper.git /usr/local/src/whisper
WORKDIR /usr/local/src/whisper
RUN python ./setup.py install
# install carbon
RUN git clone -b ${carbon_version} --depth 1 https://github.com/graphite-project/carbon.git /usr/local/src/carbon
WORKDIR /usr/local/src/carbon
RUN pip install -r requirements.txt \
&& python ./setup.py install
# install graphite
RUN git clone -b ${graphite_version} --depth 1 https://github.com/graphite-project/graphite-web.git /usr/local/src/graphite-web
WORKDIR /usr/local/src/graphite-web
RUN pip install -r requirements.txt \
&& python ./setup.py install
ADD conf/opt/graphite/conf/*.conf /opt/graphite/conf/
ADD conf/opt/graphite/webapp/graphite/local_settings.py /opt/graphite/webapp/graphite/local_settings.py
# ADD conf/opt/graphite/webapp/graphite/app_settings.py /opt/graphite/webapp/graphite/app_settings.py
WORKDIR /opt/graphite/webapp
RUN mkdir -p /var/log/graphite/ \
&& PYTHONPATH=/opt/graphite/webapp django-admin.py collectstatic --noinput --settings=graphite.settings
# install statsd
RUN git clone -b ${statsd_version} https://github.com/etsy/statsd.git /opt/statsd
ADD conf/opt/statsd/config.js /opt/statsd/config.js
# config nginx
RUN rm /etc/nginx/sites-enabled/default
ADD conf/etc/nginx/nginx.conf /etc/nginx/nginx.conf
ADD conf/etc/nginx/sites-enabled/graphite-statsd.conf /etc/nginx/sites-enabled/graphite-statsd.conf
# init django admin
ADD conf/usr/local/bin/django_admin_init.exp /usr/local/bin/django_admin_init.exp
ADD conf/usr/local/bin/manage.sh /usr/local/bin/manage.sh
RUN chmod +x /usr/local/bin/manage.sh && /usr/local/bin/django_admin_init.exp
# logging support
RUN mkdir -p /var/log/carbon /var/log/graphite /var/log/nginx
ADD conf/etc/logrotate.d/graphite-statsd /etc/logrotate.d/graphite-statsd
# daemons
ADD conf/etc/service/carbon/run /etc/service/carbon/run
ADD conf/etc/service/carbon-aggregator/run /etc/service/carbon-aggregator/run
ADD conf/etc/service/graphite/run /etc/service/graphite/run
ADD conf/etc/service/statsd/run /etc/service/statsd/run
ADD conf/etc/service/nginx/run /etc/service/nginx/run
# default conf setup
ADD conf /etc/graphite-statsd/conf
ADD conf/etc/my_init.d/01_conf_init.sh /etc/my_init.d/01_conf_init.sh
# cleanup
RUN apt-get clean\
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# defaults
EXPOSE 80 2003-2004 2023-2024 8125/udp 8126
VOLUME ["/opt/graphite/conf", "/opt/graphite/storage", "/etc/nginx", "/opt/statsd", "/etc/logrotate.d", "/var/log"]
WORKDIR /
ENV HOME /root
CMD ["/sbin/my_init"]
Copyright (c) 2013-2016 Nathan Hopkins
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
FROM phusion/baseimage:0.9.22
LABEL maintainer="Denys Zhdanov <denis.zhdanov@gmail.com>"
MAINTAINER Denys Zhdanov <denis.zhdanov@gmail.com>
RUN apt-get -y update \
&& apt-get -y upgrade \
&& apt-get -y --force-yes install vim \
&& apt-get -y install vim \
nginx \
python-dev \
python-flup \
......@@ -22,38 +23,67 @@ RUN apt-get -y update \
nodejs \
&& rm -rf /var/lib/apt/lists/*
# choose a timezone at build-time
# use `--build-arg CONTAINER_TIMEZONE=Europe/Brussels` in `docker build`
ARG CONTAINER_TIMEZONE
ENV DEBIAN_FRONTEND noninteractive
RUN if [ ! -z "${CONTAINER_TIMEZONE}" ]; \
then ln -sf /usr/share/zoneinfo/$CONTAINER_TIMEZONE /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata; \
fi
# fix python dependencies (LTS Django and newer memcached/txAMQP)
RUN pip install django==1.8.18 \
RUN pip install --upgrade pip && \
pip install django==1.8.18 \
python-memcached==1.53 \
txAMQP==0.6.2 \
&& pip install --upgrade pip
txAMQP==0.6.2
ARG version=1.0.2
ARG whisper_version=${version}
ARG carbon_version=${version}
ARG graphite_version=${version}
RUN echo "Building Version: $version"
ARG whisper_repo=https://github.com/graphite-project/whisper.git
ARG carbon_repo=https://github.com/graphite-project/carbon.git
ARG graphite_repo=https://github.com/graphite-project/graphite-web.git
ARG statsd_version=v0.8.0
ARG statsd_repo=https://github.com/etsy/statsd.git
# install whisper
RUN git clone -b 1.0.2 --depth 1 https://github.com/graphite-project/whisper.git /usr/local/src/whisper
RUN git clone -b ${whisper_version} --depth 1 ${whisper_repo} /usr/local/src/whisper
WORKDIR /usr/local/src/whisper
RUN python ./setup.py install
# install carbon
RUN git clone -b 1.0.2 --depth 1 https://github.com/graphite-project/carbon.git /usr/local/src/carbon
RUN git clone -b ${carbon_version} --depth 1 ${carbon_repo} /usr/local/src/carbon
WORKDIR /usr/local/src/carbon
RUN pip install -r requirements.txt \
&& python ./setup.py install
# install graphite
RUN git clone -b 1.0.2 --depth 1 https://github.com/graphite-project/graphite-web.git /usr/local/src/graphite-web
RUN git clone -b ${graphite_version} --depth 1 ${graphite_repo} /usr/local/src/graphite-web
WORKDIR /usr/local/src/graphite-web
RUN pip install -r requirements.txt \
&& python ./setup.py install
# install statsd
RUN git clone -b ${statsd_version} ${statsd_repo} /opt/statsd
# config graphite
ADD conf/opt/graphite/conf/*.conf /opt/graphite/conf/
ADD conf/opt/graphite/webapp/graphite/local_settings.py /opt/graphite/webapp/graphite/local_settings.py
ADD conf/opt/graphite/webapp/graphite/app_settings.py /opt/graphite/webapp/graphite/app_settings.py
# ADD conf/opt/graphite/webapp/graphite/app_settings.py /opt/graphite/webapp/graphite/app_settings.py
WORKDIR /opt/graphite/webapp
RUN mkdir -p /var/log/graphite/ \
&& PYTHONPATH=/opt/graphite/webapp django-admin.py collectstatic --noinput --settings=graphite.settings
# install statsd
RUN git clone -b v0.7.2 https://github.com/etsy/statsd.git /opt/statsd
ADD conf/opt/statsd/config.js /opt/statsd/config.js
# config statsd
ADD conf/opt/statsd/config.js /opt/statsd/
# config nginx
RUN rm /etc/nginx/sites-enabled/default
......@@ -63,8 +93,7 @@ ADD conf/etc/nginx/sites-enabled/graphite-statsd.conf /etc/nginx/sites-enabled/g
# init django admin
ADD conf/usr/local/bin/django_admin_init.exp /usr/local/bin/django_admin_init.exp
ADD conf/usr/local/bin/manage.sh /usr/local/bin/manage.sh
RUN chmod +x /usr/local/bin/manage.sh \
&& /usr/local/bin/django_admin_init.exp
RUN chmod +x /usr/local/bin/manage.sh && /usr/local/bin/django_admin_init.exp
# logging support
RUN mkdir -p /var/log/carbon /var/log/graphite /var/log/nginx
......@@ -86,9 +115,10 @@ RUN apt-get clean\
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# defaults
EXPOSE 80 2003-2004 2023-2024 8125/udp 8126
EXPOSE 80 2003-2004 2023-2024 8125 8125/udp 8126
VOLUME ["/opt/graphite/conf", "/opt/graphite/storage", "/etc/nginx", "/opt/statsd", "/etc/logrotate.d", "/var/log"]
WORKDIR /
ENV HOME /root
ENV STATSD_INTERFACE udp
CMD ["/sbin/my_init"]
......@@ -12,7 +12,7 @@ graphite_conf_dir_contents=$(find /opt/graphite/conf -mindepth 1 -print -quit)
graphite_webapp_dir_contents=$(find /opt/graphite/webapp/graphite -mindepth 1 -print -quit)
graphite_storage_dir_contents=$(find /opt/graphite/storage -mindepth 1 -print -quit)
if [[ -z $graphite_dir_contents ]]; then
git clone -b 1.0.2 --depth 1 https://github.com/graphite-project/graphite-web.git /usr/local/src/graphite-web
# git clone -b 1.0.2 --depth 1 https://github.com/graphite-project/graphite-web.git /usr/local/src/graphite-web
cd /usr/local/src/graphite-web && python ./setup.py install
fi
if [[ -z $graphite_storage_dir_contents ]]; then
......
......@@ -40,4 +40,3 @@ aggregationMethod = sum
pattern = .*
xFilesFactor = 0.3
aggregationMethod = average
# Schema definitions for Whisper files. Entries are scanned in order,
# and first match wins. This file is scanned for changes every 60 seconds.
#
# Definition Syntax:
#
# [name]
# pattern = regex
# retentions = timePerPoint:timeToStore, timePerPoint:timeToStore, ...
#
# Remember: To support accurate aggregation from higher to lower resolution
# archives, the precision of a longer retention archive must be
# cleanly divisible by precision of next lower retention archive.
#
# Valid: 60s:7d,300s:30d (300/60 = 5)
# Invalid: 180s:7d,300s:30d (300/180 = 3.333)
#
# Carbon's internal metrics. This entry should match what is specified in
# CARBON_METRIC_PREFIX and CARBON_METRIC_INTERVAL settings
[carbon]
pattern = ^carbon\..*
retentions = 1m:31d,10m:1y,1h:5y
......
#!/bin/bash
PYTHONPATH=/opt/graphite/webapp django-admin.py syncdb --settings=graphite.settings
PYTHONPATH=/opt/graphite/webapp django-admin.py update_users --settings=graphite.settings
\ No newline at end of file
# PYTHONPATH=/opt/graphite/webapp django-admin.py update_users --settings=graphite.settings
\ No newline at end of file
graphite:
build: blocks/graphite1
build:
context: blocks/graphite1
args:
version: master
ports:
- "8080:80"
- "2003:2003"
......
[cache]
LOCAL_DATA_DIR = /opt/graphite/storage/whisper/
# Specify the user to drop privileges to
# If this is blank carbon runs as the user that invokes it
# This user must have write access to the local data directory
USER =
# Limit the size of the cache to avoid swapping or becoming CPU bound.
# Sorts and serving cache queries gets more expensive as the cache grows.
# Use the value "inf" (infinity) for an unlimited cache size.
MAX_CACHE_SIZE = inf
# Limits the number of whisper update_many() calls per second, which effectively
# means the number of write requests sent to the disk. This is intended to
# prevent over-utilizing the disk and thus starving the rest of the system.
# When the rate of required updates exceeds this, then carbon's caching will
# take effect and increase the overall throughput accordingly.
MAX_UPDATES_PER_SECOND = 1000
# Softly limits the number of whisper files that get created each minute.
# Setting this value low (like at 50) is a good way to ensure your graphite
# system will not be adversely impacted when a bunch of new metrics are
# sent to it. The trade off is that it will take much longer for those metrics'
# database files to all get created and thus longer until the data becomes usable.
# Setting this value high (like "inf" for infinity) will cause graphite to create
# the files quickly but at the risk of slowing I/O down considerably for a while.
MAX_CREATES_PER_MINUTE = inf
LINE_RECEIVER_INTERFACE = 0.0.0.0
LINE_RECEIVER_PORT = 2003
PICKLE_RECEIVER_INTERFACE = 0.0.0.0
PICKLE_RECEIVER_PORT = 2004
CACHE_QUERY_INTERFACE = 0.0.0.0
CACHE_QUERY_PORT = 7002
LOG_UPDATES = False
# Enable AMQP if you want to receve metrics using an amqp broker
# ENABLE_AMQP = False
# Verbose means a line will be logged for every metric received
# useful for testing
# AMQP_VERBOSE = False
# AMQP_HOST = localhost
# AMQP_PORT = 5672
# AMQP_VHOST = /
# AMQP_USER = guest
# AMQP_PASSWORD = guest
# AMQP_EXCHANGE = graphite
# Patterns for all of the metrics this machine will store. Read more at
# http://en.wikipedia.org/wiki/Advanced_Message_Queuing_Protocol#Bindings
#
# Example: store all sales, linux servers, and utilization metrics
# BIND_PATTERNS = sales.#, servers.linux.#, #.utilization
#
# Example: store everything
# BIND_PATTERNS = #
# NOTE: you cannot run both a cache and a relay on the same server
# with the default configuration, you have to specify a distinict
# interfaces and ports for the listeners.
[relay]
LINE_RECEIVER_INTERFACE = 0.0.0.0
LINE_RECEIVER_PORT = 2003
PICKLE_RECEIVER_INTERFACE = 0.0.0.0
PICKLE_RECEIVER_PORT = 2004
CACHE_SERVERS = server1, server2, server3
MAX_QUEUE_SIZE = 10000
import datetime
import time
from django.utils.timezone import get_current_timezone
from django.core.urlresolvers import get_script_prefix
from django.http import HttpResponse
from django.shortcuts import render_to_response, get_object_or_404
from pytz import timezone
from graphite.util import json
from graphite.events import models
from graphite.render.attime import parseATTime
def to_timestamp(dt):
return time.mktime(dt.timetuple())
class EventEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime.datetime):
return to_timestamp(obj)
return json.JSONEncoder.default(self, obj)
def view_events(request):
if request.method == "GET":
context = { 'events' : fetch(request),
'slash' : get_script_prefix()
}
return render_to_response("events.html", context)
else:
return post_event(request)
def detail(request, event_id):
e = get_object_or_404(models.Event, pk=event_id)
context = { 'event' : e,
'slash' : get_script_prefix()
}
return render_to_response("event.html", context)
def post_event(request):
if request.method == 'POST':
event = json.loads(request.body)
assert isinstance(event, dict)
values = {}
values["what"] = event["what"]
values["tags"] = event.get("tags", None)
values["when"] = datetime.datetime.fromtimestamp(
event.get("when", time.time()))
if "data" in event:
values["data"] = event["data"]
e = models.Event(**values)
e.save()
return HttpResponse(status=200)
else:
return HttpResponse(status=405)
def get_data(request):
if 'jsonp' in request.REQUEST:
response = HttpResponse(
"%s(%s)" % (request.REQUEST.get('jsonp'),
json.dumps(fetch(request), cls=EventEncoder)),
mimetype='text/javascript')
else:
response = HttpResponse(
json.dumps(fetch(request), cls=EventEncoder),
mimetype="application/json")
return response
def fetch(request):
#XXX we need to move to USE_TZ=True to get rid of naive-time conversions
def make_naive(dt):
if 'tz' in request.GET:
tz = timezone(request.GET['tz'])
else:
tz = get_current_timezone()
local_dt = dt.astimezone(tz)
if hasattr(local_dt, 'normalize'):
local_dt = local_dt.normalize()
return local_dt.replace(tzinfo=None)
if request.GET.get("from", None) is not None:
time_from = make_naive(parseATTime(request.GET["from"]))
else:
time_from = datetime.datetime.fromtimestamp(0)
if request.GET.get("until", None) is not None:
time_until = make_naive(parseATTime(request.GET["until"]))
else:
time_until = datetime.datetime.now()
tags = request.GET.get("tags", None)
if tags is not None:
tags = request.GET.get("tags").split(" ")
return [x.as_dict() for x in
models.Event.find_events(time_from, time_until, tags=tags)]
[
{
"pk": 1,
"model": "auth.user",
"fields": {
"username": "admin",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": true,
"is_staff": true,
"last_login": "2011-09-20 17:02:14",
"groups": [],
"user_permissions": [],
"password": "sha1$1b11b$edeb0a67a9622f1f2cfeabf9188a711f5ac7d236",
"email": "root@example.com",
"date_joined": "2011-09-20 17:02:14"
}
}
]
# Edit this file to override the default graphite settings, do not edit settings.py
# Turn on debugging and restart apache if you ever see an "Internal Server Error" page
#DEBUG = True
# Set your local timezone (django will try to figure this out automatically)
TIME_ZONE = 'UTC'
# Setting MEMCACHE_HOSTS to be empty will turn off use of memcached entirely
#MEMCACHE_HOSTS = ['127.0.0.1:11211']
# Sometimes you need to do a lot of rendering work but cannot share your storage mount
#REMOTE_RENDERING = True
#RENDERING_HOSTS = ['fastserver01','fastserver02']
#LOG_RENDERING_PERFORMANCE = True
#LOG_CACHE_PERFORMANCE = True
# If you've got more than one backend server they should all be listed here
#CLUSTER_SERVERS = []
# Override this if you need to provide documentation specific to your graphite deployment
#DOCUMENTATION_URL = "http://wiki.mycompany.com/graphite"
# Enable email-related features
#SMTP_SERVER = "mail.mycompany.com"
# LDAP / ActiveDirectory authentication setup
#USE_LDAP_AUTH = True
#LDAP_SERVER = "ldap.mycompany.com"
#LDAP_PORT = 389
#LDAP_SEARCH_BASE = "OU=users,DC=mycompany,DC=com"
#LDAP_BASE_USER = "CN=some_readonly_account,DC=mycompany,DC=com"
#LDAP_BASE_PASS = "readonly_account_password"
#LDAP_USER_QUERY = "(username=%s)" #For Active Directory use "(sAMAccountName=%s)"
# If sqlite won't cut it, configure your real database here (don't forget to run manage.py syncdb!)
#DATABASE_ENGINE = 'mysql' # or 'postgres'
#DATABASE_NAME = 'graphite'
#DATABASE_USER = 'graphite'
#DATABASE_PASSWORD = 'graphite-is-awesome'
#DATABASE_HOST = 'mysql.mycompany.com'
#DATABASE_PORT = '3306'
grafana:$apr1$4R/20xhC$8t37jPP5dbcLr48btdkU//
daemon off;
user www-data;
worker_processes 1;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
server_names_hash_bucket_size 32;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
gzip_disable "msie6";
server {
listen 80 default_server;
server_name _;
open_log_file_cache max=1000 inactive=20s min_uses=2 valid=1m;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Host $host;
client_max_body_size 10m;
client_body_buffer_size 128k;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
}
add_header Access-Control-Allow-Origin "*";
add_header Access-Control-Allow-Methods "GET, OPTIONS";
add_header Access-Control-Allow-Headers "origin, authorization, accept";
location /content {
alias /opt/graphite/webapp/content;
}
location /media {
alias /usr/share/pyshared/django/contrib/admin/media;
}
}
}
{
graphitePort: 2003,
graphiteHost: "127.0.0.1",
port: 8125,
mgmt_port: 8126,
backends: ['./backends/graphite'],
debug: true
}
[min]
pattern = \.min$
xFilesFactor = 0.1
aggregationMethod = min
[max]
pattern = \.max$
xFilesFactor = 0.1
aggregationMethod = max
[sum]
pattern = \.count$
xFilesFactor = 0
aggregationMethod = sum
[default_average]
pattern = .*
xFilesFactor = 0.5
aggregationMethod = average
[carbon]
pattern = ^carbon\..*
retentions = 1m:31d,10m:1y,1h:5y
[highres]
pattern = ^highres.*
retentions = 1s:1d,1m:7d
[statsd]
pattern = ^statsd.*
retentions = 1m:7d,10m:1y
[default]
pattern = .*
retentions = 10s:1d,1m:7d,10m:1y
[supervisord]
nodaemon = true
environment = GRAPHITE_STORAGE_DIR='/opt/graphite/storage',GRAPHITE_CONF_DIR='/opt/graphite/conf'
[program:nginx]
command = /usr/sbin/nginx
stdout_logfile = /var/log/supervisor/%(program_name)s.log
stderr_logfile = /var/log/supervisor/%(program_name)s.log
autorestart = true
[program:carbon-cache]
;user = www-data
command = /opt/graphite/bin/carbon-cache.py --debug start
stdout_logfile = /var/log/supervisor/%(program_name)s.log
stderr_logfile = /var/log/supervisor/%(program_name)s.log
autorestart = true
[program:graphite-webapp]
;user = www-data
directory = /opt/graphite/webapp
environment = PYTHONPATH='/opt/graphite/webapp'
command = /usr/bin/gunicorn_django -b127.0.0.1:8000 -w2 graphite/settings.py
stdout_logfile = /var/log/supervisor/%(program_name)s.log
stderr_logfile = /var/log/supervisor/%(program_name)s.log
autorestart = true
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
import $ from 'jquery';
import coreModule from '../../core_module';
......@@ -201,9 +199,9 @@ export class FormDropdownCtrl {
}
open() {
this.inputElement.show();
this.inputElement.css('width', (Math.max(this.linkElement.width(), 80) + 16) + 'px');
this.inputElement.show();
this.inputElement.focus();
this.linkElement.hide();
......
......@@ -48,6 +48,7 @@ function (_, $, coreModule) {
segment.html = selected.html || selected.value;
segment.fake = false;
segment.expandable = selected.expandable;
segment.type = selected.type;
}
else if (segment.custom !== 'false') {
segment.value = value;
......
......@@ -103,7 +103,7 @@ function (angular, _, coreModule) {
};
this.newPlusButton = function() {
return new MetricSegment({fake: true, html: '<i class="fa fa-plus "></i>', type: 'plus-button' });
return new MetricSegment({fake: true, html: '<i class="fa fa-plus "></i>', type: 'plus-button', cssClass: 'query-part' });
};
this.newSelectTagValue = function() {
......
import _ from 'lodash';
const versionPattern = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z\.]+))?/;
export class SemVersion {
major: number;
minor: number;
patch: number;
meta: string;
constructor(version: string) {
let match = versionPattern.exec(version);
if (match) {
this.major = Number(match[1]);
this.minor = Number(match[2] || 0);
this.patch = Number(match[3] || 0);
this.meta = match[4];
}
}
isGtOrEq(version: string): boolean {
let compared = new SemVersion(version);
return !(this.major < compared.major || this.minor < compared.minor || this.patch < compared.patch);
}
isValid(): boolean {
return _.isNumber(this.major);
}
}
export function isVersionGtOrEq(a: string, b: string): boolean {
let a_semver = new SemVersion(a);
return a_semver.isGtOrEq(b);
}
......@@ -2,12 +2,26 @@
export class GraphiteConfigCtrl {
static templateUrl = 'public/app/plugins/datasource/graphite/partials/config.html';
datasourceSrv: any;
current: any;
/** @ngInject */
constructor($scope) {
constructor($scope, datasourceSrv) {
this.datasourceSrv = datasourceSrv;
this.current.jsonData = this.current.jsonData || {};
this.current.jsonData.graphiteVersion = this.current.jsonData.graphiteVersion || '0.9';
this.autoDetectGraphiteVersion();
}
autoDetectGraphiteVersion() {
this.datasourceSrv.loadDatasource(this.current.name)
.then((ds) => {
return ds.getVersion();
}).then((version) => {
this.graphiteVersions.push({name: version, value: version});
this.current.jsonData.graphiteVersion = version;
});
}
graphiteVersions = [
......
......@@ -2,6 +2,7 @@
import _ from 'lodash';
import * as dateMath from 'app/core/utils/datemath';
import {isVersionGtOrEq, SemVersion} from 'app/core/utils/version';
/** @ngInject */
export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
......@@ -9,6 +10,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
this.url = instanceSettings.url;
this.name = instanceSettings.name;
this.graphiteVersion = instanceSettings.jsonData.graphiteVersion || '0.9';
this.supportsTags = supportsTags(this.graphiteVersion);
this.cacheTimeout = instanceSettings.cacheTimeout;
this.withCredentials = instanceSettings.withCredentials;
this.render_method = instanceSettings.render_method || 'POST';
......@@ -217,6 +219,126 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
});
};
this.getTags = function(optionalOptions) {
let options = optionalOptions || {};
let httpOptions: any = {
method: 'GET',
url: '/tags',
// for cancellations
requestId: options.requestId,
};
if (options && options.range) {
httpOptions.params.from = this.translateTime(options.range.from, false);
httpOptions.params.until = this.translateTime(options.range.to, true);
}
return this.doGraphiteRequest(httpOptions).then(results => {
return _.map(results.data, tag => {
return {
text: tag.tag,
id: tag.id
};
});
});
};
this.getTagValues = function(tag, optionalOptions) {
let options = optionalOptions || {};
let httpOptions: any = {
method: 'GET',
url: '/tags/' + tag,
// for cancellations
requestId: options.requestId,
};
if (options && options.range) {
httpOptions.params.from = this.translateTime(options.range.from, false);
httpOptions.params.until = this.translateTime(options.range.to, true);
}
return this.doGraphiteRequest(httpOptions).then(results => {
if (results.data && results.data.values) {
return _.map(results.data.values, value => {
return {
text: value.value,
id: value.id
};
});
} else {
return [];
}
});
};
this.getTagsAutoComplete = (expression, tagPrefix) => {
let httpOptions: any = {
method: 'GET',
url: '/tags/autoComplete/tags',
params: {
expr: expression
}
};
if (tagPrefix) {
httpOptions.params.tagPrefix = tagPrefix;
}
return this.doGraphiteRequest(httpOptions).then(results => {
if (results.data) {
return _.map(results.data, (tag) => {
return { text: tag };
});
} else {
return [];
}
});
};
this.getTagValuesAutoComplete = (expression, tag, valuePrefix) => {
let httpOptions: any = {
method: 'GET',
url: '/tags/autoComplete/values',
params: {
expr: expression,
tag: tag
}
};
if (valuePrefix) {
httpOptions.params.valuePrefix = valuePrefix;
}
return this.doGraphiteRequest(httpOptions).then(results => {
if (results.data) {
return _.map(results.data, (value) => {
return { text: value };
});
} else {
return [];
}
});
};
this.getVersion = function() {
let httpOptions = {
method: 'GET',
url: '/version/_', // Prevent last / trimming
};
return this.doGraphiteRequest(httpOptions).then(results => {
if (results.data) {
let semver = new SemVersion(results.data);
return semver.isValid() ? results.data : '';
}
return '';
}).catch(() => {
return '';
});
};
this.testDatasource = function() {
return this.metricFindQuery('*').then(function () {
return { status: "success", message: "Data source is working"};
......@@ -303,3 +425,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
return clean_options;
};
}
function supportsTags(version: string): boolean {
return isVersionGtOrEq(version, '1.1');
}
import _ from 'lodash';
import {isVersionGtOrEq} from 'app/core/utils/version';
var index = [];
var categories = {
......@@ -968,13 +970,7 @@ FuncInstance.prototype.updateText = function() {
};
function isVersionRelatedFunction(func, graphiteVersion) {
return isVersionGreaterOrEqual(graphiteVersion, func.version) || !func.version;
}
function isVersionGreaterOrEqual(a, b) {
var a_num = Number(a);
var b_num = Number(b);
return a_num >= b_num;
return isVersionGtOrEq(graphiteVersion, func.version) || !func.version;
}
export default {
......
import _ from 'lodash';
import gfunc from './gfunc';
import {Parser} from './parser';
export default class GraphiteQuery {
target: any;
functions: any[];
segments: any[];
tags: any[];
error: any;
seriesByTagUsed: boolean;
checkOtherSegmentsIndex: number;
removeTagValue: string;
templateSrv: any;
scopedVars: any;
/** @ngInject */
constructor(target, templateSrv?, scopedVars?) {
this.target = target;
this.parseTarget();
this.removeTagValue = '-- remove tag --';
}
parseTarget() {
this.functions = [];
this.segments = [];
this.tags = [];
this.error = null;
if (this.target.textEditor) {
return;
}
var parser = new Parser(this.target.target);
var astNode = parser.getAst();
if (astNode === null) {
this.checkOtherSegmentsIndex = 0;
return;
}
if (astNode.type === 'error') {
this.error = astNode.message + " at position: " + astNode.pos;
this.target.textEditor = true;
return;
}
try {
this.parseTargetRecursive(astNode, null, 0);
} catch (err) {
console.log('error parsing target:', err.message);
this.error = err.message;
this.target.textEditor = true;
}
this.checkOtherSegmentsIndex = this.segments.length - 1;
this.checkForSeriesByTag();
}
checkForSeriesByTag() {
let seriesByTagFunc = _.find(this.functions, (func) => func.def.name === 'seriesByTag');
if (seriesByTagFunc) {
this.seriesByTagUsed = true;
seriesByTagFunc.hidden = true;
let tags = this.splitSeriesByTagParams(seriesByTagFunc);
this.tags = tags;
}
}
getSegmentPathUpTo(index) {
var arr = this.segments.slice(0, index);
return _.reduce(arr, function(result, segment) {
return result ? (result + "." + segment.value) : segment.value;
}, "");
}
parseTargetRecursive(astNode, func, index) {
if (astNode === null) {
return null;
}
switch (astNode.type) {
case 'function':
var innerFunc = gfunc.createFuncInstance(astNode.name, { withDefaultParams: false });
_.each(astNode.params, (param, index) => {
this.parseTargetRecursive(param, innerFunc, index);
});
innerFunc.updateText();
this.functions.push(innerFunc);
break;
case 'series-ref':
this.addFunctionParameter(func, astNode.value, index, this.segments.length > 0);
break;
case 'bool':
case 'string':
case 'number':
if ((index-1) >= func.def.params.length) {
throw { message: 'invalid number of parameters to method ' + func.def.name };
}
var shiftBack = this.isShiftParamsBack(func);
this.addFunctionParameter(func, astNode.value, index, shiftBack);
break;
case 'metric':
if (this.segments.length > 0) {
if (astNode.segments.length !== 1) {
throw { message: 'Multiple metric params not supported, use text editor.' };
}
this.addFunctionParameter(func, astNode.segments[0].value, index, true);
break;
}
this.segments = astNode.segments;
}
}
isShiftParamsBack(func) {
return func.def.name !== 'seriesByTag';
}
updateSegmentValue(segment, index) {
this.segments[index].value = segment.value;
}
addSelectMetricSegment() {
this.segments.push({value: "select metric"});
}
addFunction(newFunc) {
this.functions.push(newFunc);
this.moveAliasFuncLast();
}
moveAliasFuncLast() {
var aliasFunc = _.find(this.functions, function(func) {
return func.def.name === 'alias' ||
func.def.name === 'aliasByNode' ||
func.def.name === 'aliasByMetric';
});
if (aliasFunc) {
this.functions = _.without(this.functions, aliasFunc);
this.functions.push(aliasFunc);
}
}
addFunctionParameter(func, value, index, shiftBack) {
if (shiftBack) {
index = Math.max(index - 1, 0);
}
func.params[index] = value;
}
removeFunction(func) {
this.functions = _.without(this.functions, func);
}
updateModelTarget(targets) {
// render query
if (!this.target.textEditor) {
var metricPath = this.getSegmentPathUpTo(this.segments.length);
this.target.target = _.reduce(this.functions, wrapFunction, metricPath);
}
this.updateRenderedTarget(this.target, targets);
// loop through other queries and update targetFull as needed
for (const target of targets || []) {
if (target.refId !== this.target.refId) {
this.updateRenderedTarget(target, targets);
}
}
}
updateRenderedTarget(target, targets) {
// render nested query
var targetsByRefId = _.keyBy(targets, 'refId');
// no references to self
delete targetsByRefId[target.refId];
var nestedSeriesRefRegex = /\#([A-Z])/g;
var targetWithNestedQueries = target.target;
// Keep interpolating until there are no query references
// The reason for the loop is that the referenced query might contain another reference to another query
while (targetWithNestedQueries.match(nestedSeriesRefRegex)) {
var updated = targetWithNestedQueries.replace(nestedSeriesRefRegex, (match, g1) => {
var t = targetsByRefId[g1];
if (!t) {
return match;
}
// no circular references
delete targetsByRefId[g1];
return t.target;
});
if (updated === targetWithNestedQueries) {
break;
}
targetWithNestedQueries = updated;
}
delete target.targetFull;
if (target.target !== targetWithNestedQueries) {
target.targetFull = targetWithNestedQueries;
}
}
splitSeriesByTagParams(func) {
const tagPattern = /([^\!=~]+)([\!=~]+)([^\!=~]+)/;
return _.flatten(_.map(func.params, (param: string) => {
let matches = tagPattern.exec(param);
if (matches) {
let tag = matches.slice(1);
if (tag.length === 3) {
return {
key: tag[0],
operator: tag[1],
value: tag[2]
};
}
}
return [];
}));
}
getSeriesByTagFuncIndex() {
return _.findIndex(this.functions, (func) => func.def.name === 'seriesByTag');
}
getSeriesByTagFunc() {
let seriesByTagFuncIndex = this.getSeriesByTagFuncIndex();
if (seriesByTagFuncIndex >= 0) {
return this.functions[seriesByTagFuncIndex];
} else {
return undefined;
}
}
addTag(tag) {
let newTagParam = renderTagString(tag);
this.getSeriesByTagFunc().params.push(newTagParam);
this.tags.push(tag);
}
removeTag(index) {
this.getSeriesByTagFunc().params.splice(index, 1);
this.tags.splice(index, 1);
}
updateTag(tag, tagIndex) {
this.error = null;
if (tag.key === this.removeTagValue) {
this.removeTag(tagIndex);
return;
}
let newTagParam = renderTagString(tag);
this.getSeriesByTagFunc().params[tagIndex] = newTagParam;
this.tags[tagIndex] = tag;
}
renderTagExpressions(excludeIndex = -1) {
return _.compact(_.map(this.tags, (tagExpr, index) => {
// Don't render tag that we want to lookup
if (index !== excludeIndex) {
return tagExpr.key + tagExpr.operator + tagExpr.value;
}
}));
}
}
function wrapFunction(target, func) {
return func.render(target);
}
function renderTagString(tag) {
return tag.key + tag.operator + tag.value;
}
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
<div class="gf-form" ng-show="ctrl.target.textEditor">
<input type="text" class="gf-form-input" ng-model="ctrl.target.target" spellcheck="false" ng-blur="ctrl.targetTextChanged()"></input>
</div>
<div class="gf-form" ng-show="ctrl.target.textEditor">
<input type="text" class="gf-form-input" ng-model="ctrl.target.target" spellcheck="false" ng-blur="ctrl.targetTextChanged()"></input>
</div>
<div ng-hide="ctrl.target.textEditor">
<div class="gf-form-inline">
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label width-6 query-keyword">Series</label>
</div>
<div ng-repeat="tag in ctrl.queryModel.tags" class="gf-form">
<gf-form-dropdown model="tag.key" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-key"
get-options="ctrl.getTags($index, $query)"
on-change="ctrl.tagChanged(tag, $index)">
</gf-form-dropdown>
<gf-form-dropdown model="tag.operator" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-operator"
get-options="ctrl.getTagOperators()"
on-change="ctrl.tagChanged(tag, $index)"
min-input-width="30">
</gf-form-dropdown>
<gf-form-dropdown model="tag.value" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-value"
get-options="ctrl.getTagValues(tag, $index, $query)"
on-change="ctrl.tagChanged(tag, $index)">
</gf-form-dropdown>
<label class="gf-form-label query-keyword" ng-if="ctrl.showDelimiter($index)">AND</label>
</div>
<div ng-repeat="segment in ctrl.segments" role="menuitem" class="gf-form">
<metric-segment segment="segment" get-options="ctrl.getAltSegments($index)" on-change="ctrl.segmentValueChanged(segment, $index)"></metric-segment>
</div>
<div ng-repeat="func in ctrl.functions" class="gf-form">
<span graphite-func-editor class="gf-form-label query-part"></span>
<div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.addTagSegments" role="menuitem" class="gf-form">
<metric-segment segment="segment" get-options="ctrl.getTagsAsSegments()" on-change="ctrl.addNewTag(segment)">
</metric-segment>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label width-6 query-keyword">Functions</label>
</div>
<div ng-repeat="func in ctrl.queryModel.functions" class="gf-form">
<span graphite-func-editor class="gf-form-label query-part" ng-hide="func.hidden"></span>
</div>
<div class="gf-form dropdown">
......@@ -19,8 +55,8 @@
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
</div>
......
......@@ -48,7 +48,7 @@ describe('GraphiteQueryCtrl', function() {
});
it('should parse expression and build function model', function() {
expect(ctx.ctrl.functions.length).to.be(2);
expect(ctx.ctrl.queryModel.functions.length).to.be(2);
});
});
......@@ -61,7 +61,7 @@ describe('GraphiteQueryCtrl', function() {
});
it('should add function with correct node number', function() {
expect(ctx.ctrl.functions[0].params[0]).to.be(2);
expect(ctx.ctrl.queryModel.functions[0].params[0]).to.be(2);
});
it('should update target', function() {
......@@ -99,7 +99,7 @@ describe('GraphiteQueryCtrl', function() {
});
it('should add both series refs as params', function() {
expect(ctx.ctrl.functions[0].params.length).to.be(2);
expect(ctx.ctrl.queryModel.functions[0].params.length).to.be(2);
});
});
......@@ -115,7 +115,7 @@ describe('GraphiteQueryCtrl', function() {
});
it('should add function param', function() {
expect(ctx.ctrl.functions[0].params.length).to.be(1);
expect(ctx.ctrl.queryModel.functions[0].params.length).to.be(1);
});
});
......@@ -131,7 +131,7 @@ describe('GraphiteQueryCtrl', function() {
});
it('should have correct func params', function() {
expect(ctx.ctrl.functions[0].params.length).to.be(1);
expect(ctx.ctrl.queryModel.functions[0].params.length).to.be(1);
});
});
......@@ -210,4 +210,113 @@ describe('GraphiteQueryCtrl', function() {
});
});
describe('when adding seriesByTag function', function() {
beforeEach(function() {
ctx.ctrl.target.target = '';
ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
ctx.ctrl.parseTarget();
ctx.ctrl.addFunction(gfunc.getFuncDef('seriesByTag'));
});
it('should update functions', function() {
expect(ctx.ctrl.queryModel.getSeriesByTagFuncIndex()).to.be(0);
});
it('should update seriesByTagUsed flag', function() {
expect(ctx.ctrl.queryModel.seriesByTagUsed).to.be(true);
});
it('should update target', function() {
expect(ctx.ctrl.target.target).to.be('seriesByTag()');
});
it('should call refresh', function() {
expect(ctx.panelCtrl.refresh.called).to.be(true);
});
});
describe('when parsing seriesByTag function', function() {
beforeEach(function() {
ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
ctx.ctrl.parseTarget();
});
it('should add tags', function() {
const expected = [
{key: 'tag1', operator: '=', value: 'value1'},
{key: 'tag2', operator: '!=~', value: 'value2'}
];
expect(ctx.ctrl.queryModel.tags).to.eql(expected);
});
it('should add plus button', function() {
expect(ctx.ctrl.addTagSegments.length).to.be(1);
});
});
describe('when tag added', function() {
beforeEach(function() {
ctx.ctrl.target.target = "seriesByTag()";
ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
ctx.ctrl.parseTarget();
ctx.ctrl.addNewTag({value: 'tag1'});
});
it('should update tags with default value', function() {
const expected = [
{key: 'tag1', operator: '=', value: 'select tag value'}
];
expect(ctx.ctrl.queryModel.tags).to.eql(expected);
});
it('should update target', function() {
const expected = "seriesByTag('tag1=select tag value')";
expect(ctx.ctrl.target.target).to.eql(expected);
});
});
describe('when tag changed', function() {
beforeEach(function() {
ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
ctx.ctrl.parseTarget();
ctx.ctrl.tagChanged({key: 'tag1', operator: '=', value: 'new_value'}, 0);
});
it('should update tags', function() {
const expected = [
{key: 'tag1', operator: '=', value: 'new_value'},
{key: 'tag2', operator: '!=~', value: 'value2'}
];
expect(ctx.ctrl.queryModel.tags).to.eql(expected);
});
it('should update target', function() {
const expected = "seriesByTag('tag1=new_value', 'tag2!=~value2')";
expect(ctx.ctrl.target.target).to.eql(expected);
});
});
describe('when tag removed', function() {
beforeEach(function() {
ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
ctx.ctrl.parseTarget();
ctx.ctrl.removeTag(0);
});
it('should update tags', function() {
const expected = [
{key: 'tag2', operator: '!=~', value: 'value2'}
];
expect(ctx.ctrl.queryModel.tags).to.eql(expected);
});
it('should update target', function() {
const expected = "seriesByTag('tag2!=~value2')";
expect(ctx.ctrl.target.target).to.eql(expected);
});
});
});
import {describe, beforeEach, it, expect} from 'test/lib/common';
import {SemVersion, isVersionGtOrEq} from 'app/core/utils/version';
describe("SemVersion", () => {
let version = '1.0.0-alpha.1';
describe('parsing', () => {
it('should parse version properly', () => {
let semver = new SemVersion(version);
expect(semver.major).to.be(1);
expect(semver.minor).to.be(0);
expect(semver.patch).to.be(0);
expect(semver.meta).to.be('alpha.1');
});
});
describe('comparing', () => {
beforeEach(() => {
version = '3.4.5';
});
it('should detect greater version properly', () => {
let semver = new SemVersion(version);
let cases = [
{value: '3.4.5', expected: true},
{value: '3.4.4', expected: true},
{value: '3.4.6', expected: false},
{value: '4', expected: false},
{value: '3.5', expected: false},
];
cases.forEach((testCase) => {
expect(semver.isGtOrEq(testCase.value)).to.be(testCase.expected);
});
});
});
describe('isVersionGtOrEq', () => {
it('should compare versions properly (a >= b)', () => {
let cases = [
{values: ['3.4.5', '3.4.5'], expected: true},
{values: ['3.4.5', '3.4.4'] , expected: true},
{values: ['3.4.5', '3.4.6'], expected: false},
{values: ['3.4', '3.4.0'], expected: true},
{values: ['3', '3.0.0'], expected: true},
{values: ['3.1.1-beta1', '3.1'], expected: true},
{values: ['3.4.5', '4'], expected: false},
{values: ['3.4.5', '3.5'], expected: false},
];
cases.forEach((testCase) => {
expect(isVersionGtOrEq(testCase.values[0], testCase.values[1])).to.be(testCase.expected);
});
});
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment