Tatsumaki是Plack作者的一个小框架,亮点是很好的利用了psgi.streaming的接口可以async的完成响应。不过因为缺少周边支持,所以除了几个webchat的example,似乎没看到什么应用。笔者之前纯为练手,却用tatsumaki写了个sync响应的小demo,算是展示一下用tatsuamki做普通web应用的基础步骤吧:
(代码本来是作为一个ElasticSearch数据分析的平台,不过后来发现社区有人开始做纯js的内嵌进ElasticSearch的plugin了,所以撤了repo,这里贴下代码)
所有的psgi/plack应用都一样有自己的app.psgi文件:
```perl
our $VERSION = 0.01;
### app.psgi
use Tatsumaki::Error;
use Tatsumaki::Application;
use Tatsumaki::HTTPClient;
use Tatsumaki::Server;
### read config
use File::Basename;
use YAML::Syck;
my $config = LoadFile(dirname(FILE) . ‘/config.yml’);
### elasticsearch init
use ElasticSearch;
#这里yml的写法借鉴Dancer::Plugin::ElasticSearch了
my $elsearch = ElasticSearch->new( $config->{‘options’} );
### index init
use POSIX qw(strftime);
my $index = join ‘-‘, ( (+split( ‘-‘, $config->{‘index’} ))[0], strftime( (+split( ‘-‘, $config->{‘index’} ))[1], localtime ) );
my $type = $config->{‘type’};
#首页类,调用了模板
package MainHandler;
use parent qw(Tatsumaki::Handler);
sub get {
my $self = shift;
$self->render(‘index.html’);
};
#具体的API类
package ListHandler;
use parent qw(Tatsumaki::Handler);
sub get {
#这里自动把urlpath切分好了
my ( $self, $group, $order, $interval ) = @_;
return ‘Not valid order’ unless $order eq ‘count’ or $order eq ‘mean’;
return ‘Not valid interval’ unless $interval =~ m#\d+(h|m|s)#;
my ($key_field, $value_field);
if ( $group eq ‘url’ ) {
$key_field = ‘url’;
$value_field = ‘responsetime’;
} elsif ( $group eq ‘ip’ ) {
$key_field = ‘oh’;
$value_field = ‘upstreamtime’;
} else {
return ‘Not valid group field’;
};
# get index mapping and sort into array
my $mapping = $elsearch->mapping(
index => “$index”,
type => “$type”,
);
my @res_map;
for my $property ( sort keys %{ $mapping->{$type}->{‘properties’} } ) {
if ($property eq ‘@fields’ ) {
my @fields;
push @fields, { name => $, type => $mapping->{$type}->{‘properties’}->{$property}->{‘properties’}->{$}->{‘type’} }
for sort keys %{ $mapping->{$type}->{‘properties’}->{$property}->{‘properties’} };
push @res_map, \@fields;
} else {
push @res_map, { name => $property, type => $mapping->{$type}->{‘properties’}->{$property}->{‘type’} };
}
}
# get value stat group by key field
my $data = $elsearch->search(
index => “$index”,
type => “$type”,
size => 0,
query => {
“range” => {
‘@timestamp’ => {
from => “now-$interval”,
to => “now”
},
},
},
facets => {
“$group” => {
“terms_stats” => {
“value_field” => “$value_field”,
“key_field” => “$key_field”,
“order” => “$order”,
“size” => 20,
}
},
}
);
my @res_tbl;
for ( @{$data->{facets}->{“$group”}->{terms}} ) {
my $key = $->{term};
my $mean = sprintf “%.03f”, $->{mean};
my $code_count = code_count($key_field, $key, $interval);
push @res_tbl, {
key => $key,
min => $->{min},
max => $->{max},
mean => $mean,
code => $code_count,
count => $_->{count},
};
};
$self->render('index.html', { table => \@res_tbl, mapping => \@res_map }); };
sub code_count {
my ($key_field, $key, $interval) = @;
my $result;
my $data = $elsearch->search(
index => “$index”,
type => “$type”,
size => 0,
query => {
range => {
‘@timestamp’ => {
from => “now-$interval”,
to => “now”
},
},
},
facets => {
“code” => {
facet_filter => {
term => {
$key_field => “$key”
}
},
terms => {
field => “status”,
}
}
}
);
for ( @{$data->{facets}->{code}->{terms}} ) {
$result->{$->{term}} = $_->{count};
};
return $result;
};
#画图数据API类,因为响应的是Ajax请求,所以这里开启了async,不过其实没意义了。因为这个ElasticSearch代码不是async格式的。应该改造用ElasticSearch::Transport::AEHTTP才能做到全程async。
package ChartHandler;
use parent qw(Tatsumaki::Handler);
PACKAGE->asynchronous(1);
use JSON;
sub post {
my $self = shift;
my $api = $self->request->param(‘api’) || ‘term’;
my $key = $self->request->param(‘key’) || ‘oh’;
my $value = $self->request->param(‘value’);
my $status = $self->request->param(‘status’) || ‘200’;
my $field = $key eq 'oh' ? 'upstreamtime' : 'responsetime';
my $data = $elsearch->search(
index => "$index",
type => "$type",
size => 0,
query => {
match_all => { }
},
facets => {
"chart" => {
facet_filter => {
and => [
{
term => {
status => $status,
},
},
{
$api => {
$key => $value,
},
},
]
},
date_histogram => {
value_field => $field,
key_field => '@timestamp',
interval => "1m"
}
},
},
);
my @result;
for ( @{$data->{'facets'}->{'chart'}->{'entries'}} ) {
push @result, {
time => $_->{'time'},
count => $_->{'count'},
mean => sprintf "%.3f", $_->{'mean'} * 1000,
};
};
header('Content-Type' => 'application/json');
to_json(\@result); }; #主函数 package main; use File::Basename; #通过Tatsumaki::Application绑定urlpath到不同的类上。注意下面listhandler那里用的正则捕获。对,上面类里传参就是这么来的。注意最多不超过$9。 my $app = Tatsumaki::Application->new([
'/' => 'MainHandler',
'/api/chartdata' => 'ChartHandler',
'/api/(\w+)/(\w+)/(\w+)' => 'ListHandler', ]); #指定template和static的路径。类似Dancer里的views和public $app->template_path(dirname(__FILE__) . '/templates'); $app->static_path(dirname(__FILE__) . '/static'); #psgi app组建完成 return $app->psgi_app; true; ``` static里都是bootstrap的东西就不贴了。然后说说template。目前Tatsumaki只支持Text::MicroTemplate::File一种template,当然自己在handler里调用其他的template然后返回字符串也行。不过其实Text::MicroTemplate也蛮强大的。下面上例子: ```html %# 这就是Text::MicroTemplate强大的地方了,行首加个百分号就可以直接使用perl而不像TT那样尽量搞自己的语法 %# 配合render传来的handler(前面说了是类$self),整个环境全可以任意调用。 % my $mapping = $_[0]->{'mapping'}; % my $table = $_[0]->{'table'}; %# 比如这里其实就是通过handler调用request了。 % my $group = $_[0]->{handler}->args->[0]; % my @codes = qw(200 206 302 304 400 403 404 499 502 503 504); <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<%= $group %> | 平均响应时间 | 最大响应时间 | 下载数 | % for ( @codes ) {<%= $_ %> | % }|
---|---|---|---|---|---|
<%= $list->{'key'} %> | <%= $list->{'mean'} %> | <%= $list->{'max'} %> | <%= $list->{'count'} %> | % for ( @codes ) { % if ( $list->{'code'}->{$_} ) {<%= $list->{'code'}->{$_} %> | % } else {% } % } |
```
效果如下:
2012 年 12 月 30 日附注:
更好的纯 js 版本已经作为独立的 elasticsearch-plugin 项目发布在 github 上。地址:https://github.com/chenryn/elasticsearch-logstash-faceter 。欢迎大家试用!!