fluent-plugin-onlineuser 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +2 -0
- data/Gemfile +4 -0
- data/README.md +2 -0
- data/Rakefile +11 -0
- data/fluent-plugin-onlineuser.gemspec +23 -0
- data/lib/fluent/plugin/out_onlineuser.rb +186 -0
- data/test/fluentd.conf +51 -0
- data/test/plugin/test_out_onlineuser.rb +42 -0
- metadata +136 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
Gem::Specification.new do |gem|
|
3
|
+
gem.name = "fluent-plugin-onlineuser"
|
4
|
+
gem.version = "0.0.1"
|
5
|
+
gem.authors = ["Yuyang Lan"]
|
6
|
+
gem.email = ["lanyuyang@gree.net"]
|
7
|
+
gem.summary = %q{Online Users Counter}
|
8
|
+
gem.description = %q{Fluentd plugin to count online users. It's based on Redis and the sorted set data type.}
|
9
|
+
gem.homepage = "https://github.com/lanyuyang/fluent-plugin-onlineuser"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
gem.has_rdoc = false
|
16
|
+
|
17
|
+
gem.add_development_dependency "rake"
|
18
|
+
gem.add_development_dependency "fluentd"
|
19
|
+
gem.add_development_dependency "redis"
|
20
|
+
|
21
|
+
gem.add_runtime_dependency "fluentd"
|
22
|
+
gem.add_runtime_dependency "redis"
|
23
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module Fluent
|
4
|
+
class OnlineuserOutput < BufferedOutput
|
5
|
+
Fluent::Plugin.register_output('online_user', self)
|
6
|
+
|
7
|
+
config_param :host, :string, :default => 'localhost'
|
8
|
+
config_param :port, :integer, :default => 6379
|
9
|
+
config_param :db_index, :integer, :default => 0
|
10
|
+
config_param :redis_retry, :integer, :default => 3
|
11
|
+
|
12
|
+
config_param :session_timeout, :integer, :default => 1800
|
13
|
+
|
14
|
+
config_param :redis_key_prefix, :string, :default => 'counter:online_user:'
|
15
|
+
config_param :user_identify, :string, :default => 'uid'
|
16
|
+
config_param :segment, :string, :default => nil
|
17
|
+
|
18
|
+
config_param :tag, :string, :default => "online_user"
|
19
|
+
config_param :silent, :bool, :default => false
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
super
|
23
|
+
require 'redis'
|
24
|
+
require 'msgpack'
|
25
|
+
require 'rubygems'
|
26
|
+
end
|
27
|
+
|
28
|
+
def configure(conf)
|
29
|
+
super
|
30
|
+
@host = conf.has_key?('host') ? conf['host'] : 'localhost'
|
31
|
+
@port = conf.has_key?('port') ? conf['port'].to_i : 6379
|
32
|
+
@db_number = conf.has_key?('db_number') ? conf['db_number'].to_i : nil
|
33
|
+
|
34
|
+
@user_identify = @user_identify.split '|'
|
35
|
+
|
36
|
+
if @segment
|
37
|
+
s = @segment.split /\s+/
|
38
|
+
case s.first
|
39
|
+
when 'tag'
|
40
|
+
@cal_segment = lambda { |tag, record| 'tag:' + tag }
|
41
|
+
when 'capture'
|
42
|
+
if s.length == 3
|
43
|
+
format = s[2]
|
44
|
+
if format[0] == ?/ and format[-1] == ?/
|
45
|
+
format = format[1..-2]
|
46
|
+
end
|
47
|
+
regexp = Regexp.new(format)
|
48
|
+
@cal_segment = lambda { |tag, record|
|
49
|
+
if record[s[1]] and record[s[1]].is_a? String
|
50
|
+
if c = record[s[1]].match(regexp)
|
51
|
+
'captured_' + s[1] + ':' + (c.captures.join '_')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
}
|
55
|
+
else
|
56
|
+
raise Fluent::ConfigError, "wrong segment capture, specify it like 'capture __FIELD_NAME__ __CAPTURE_REGEX__'"
|
57
|
+
end
|
58
|
+
else
|
59
|
+
@cal_segment = lambda { |tag, record|
|
60
|
+
if record[@segment]
|
61
|
+
@segment + ':' + record[@segment]
|
62
|
+
end
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def start
|
69
|
+
super
|
70
|
+
|
71
|
+
begin
|
72
|
+
gem "hiredis"
|
73
|
+
@redis = Redis.new(
|
74
|
+
:host => @host, :port => @port, :driver => :hiredis,
|
75
|
+
:thread_safe => true, :db => @db_index
|
76
|
+
)
|
77
|
+
rescue LoadError
|
78
|
+
@redis = Redis.new(
|
79
|
+
:host => @host, :port => @port,
|
80
|
+
:thread_safe => true, :db => @db_index
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
@segments = {}
|
85
|
+
|
86
|
+
start_watch
|
87
|
+
end
|
88
|
+
|
89
|
+
def shutdown
|
90
|
+
@redis.quit
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_redis_key(segment_str)
|
94
|
+
@redis_key_prefix + segment_str
|
95
|
+
end
|
96
|
+
|
97
|
+
def tryOnRedis(method, *args)
|
98
|
+
tries = 0
|
99
|
+
begin
|
100
|
+
@redis.send(method, *args) if @redis.respond_to? method
|
101
|
+
rescue Redis::CommandError => e
|
102
|
+
tries += 1
|
103
|
+
# retry 3 times
|
104
|
+
retry if tries <= @redis_retry
|
105
|
+
$log.warn %Q[redis command retry failed : #{method}(#{args.join(', ')})]
|
106
|
+
raise e.message
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def start_watch
|
111
|
+
@watcher = Thread.new(&method(:watch))
|
112
|
+
end
|
113
|
+
|
114
|
+
def watch
|
115
|
+
@last_checked = Fluent::Engine.now
|
116
|
+
tick = 60
|
117
|
+
while true
|
118
|
+
sleep 0.5
|
119
|
+
if Fluent::Engine.now - @last_checked >= tick
|
120
|
+
now = Fluent::Engine.now
|
121
|
+
|
122
|
+
@segments.each_key do |segment|
|
123
|
+
user_num = user_count segment
|
124
|
+
|
125
|
+
if user_num == 0
|
126
|
+
@segments.delete segment
|
127
|
+
elsif not @silent
|
128
|
+
Fluent::Engine.emit @tag + '.' + segment, Fluent::Engine.now, {"online_user" => user_num}
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
@last_checked = now
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def user_count(segment)
|
138
|
+
to_expire = Fluent::Engine.now - @session_timeout
|
139
|
+
key = to_redis_key segment
|
140
|
+
tryOnRedis 'zremrangebyscore', key, '-inf', to_expire
|
141
|
+
tryOnRedis 'zcard', key
|
142
|
+
end
|
143
|
+
|
144
|
+
def extract_uid(record)
|
145
|
+
i = @user_identify.detect { |id|
|
146
|
+
v = record[id]
|
147
|
+
(v.is_a?(String) and !v.empty?) or (v.is_a?(Numeric) and v != 0)
|
148
|
+
}
|
149
|
+
i ? record[i] : nil
|
150
|
+
end
|
151
|
+
|
152
|
+
def format(tag, time, record)
|
153
|
+
[tag, time, record].to_msgpack
|
154
|
+
end
|
155
|
+
|
156
|
+
def write(chunk)
|
157
|
+
|
158
|
+
online_user = Hash.new { |hash, key| hash[key] = Hash.new }
|
159
|
+
|
160
|
+
begin
|
161
|
+
chunk.msgpack_each do |(tag, time, record)|
|
162
|
+
|
163
|
+
if (uid = extract_uid record)
|
164
|
+
online_user['all'][uid] = time
|
165
|
+
|
166
|
+
if @cal_segment and @cal_segment != '' and segment = @cal_segment.call(tag, record)
|
167
|
+
online_user[segment][uid] = time
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
## write to redis
|
173
|
+
online_user.each { |segment, users|
|
174
|
+
@segments[segment] = true
|
175
|
+
|
176
|
+
key = to_redis_key segment
|
177
|
+
|
178
|
+
users.each { |uid, ts|
|
179
|
+
tryOnRedis 'zadd', key, ts, uid
|
180
|
+
}
|
181
|
+
}
|
182
|
+
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
data/test/fluentd.conf
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
<source>
|
2
|
+
type forward
|
3
|
+
</source>
|
4
|
+
|
5
|
+
<match access_log.**>
|
6
|
+
type online_user
|
7
|
+
flush_interval 10s
|
8
|
+
|
9
|
+
#redis configuration
|
10
|
+
#host localhost
|
11
|
+
#port 6379
|
12
|
+
#db_index 0
|
13
|
+
#redis_try 3
|
14
|
+
|
15
|
+
#inactive timeout, default 30 minutes
|
16
|
+
#session_timeout 1800
|
17
|
+
|
18
|
+
# prefix of the keys stored into Redis
|
19
|
+
#redis_key_prefix counter:online_user:
|
20
|
+
|
21
|
+
# how do you define unique user ?
|
22
|
+
# the default is to find 'uid' field in record
|
23
|
+
|
24
|
+
# for support both members and non-members, try:
|
25
|
+
#user_identify uid|ip
|
26
|
+
|
27
|
+
#emit online user count event periodically or not
|
28
|
+
#silent false
|
29
|
+
|
30
|
+
#the tag prefix for emitted event
|
31
|
+
#tag online_user
|
32
|
+
|
33
|
+
#besides counting the overall online users
|
34
|
+
#also wanna count online users for each segment ?
|
35
|
+
#if so, use the segment directive
|
36
|
+
|
37
|
+
#segmented by tag
|
38
|
+
#segment tag
|
39
|
+
|
40
|
+
#segmented by a specified field
|
41
|
+
#segment service
|
42
|
+
|
43
|
+
#segmented by regexp-captured string
|
44
|
+
#the format is: capture __FIELD__ __REGEXP__
|
45
|
+
#segment capture name /^\w+:(\w+):/
|
46
|
+
|
47
|
+
</match>
|
48
|
+
|
49
|
+
<match **>
|
50
|
+
type stdout
|
51
|
+
</match>
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require 'fluent/test'
|
5
|
+
require 'fluent/plugin/out_onlineuser'
|
6
|
+
require 'redis'
|
7
|
+
|
8
|
+
class OnlineuserOutputTest < Test::Unit::TestCase
|
9
|
+
CONFIG = %[
|
10
|
+
host localhost
|
11
|
+
port 6379
|
12
|
+
db_number 0
|
13
|
+
]
|
14
|
+
|
15
|
+
@redis
|
16
|
+
|
17
|
+
def setup
|
18
|
+
Fluent::Test.setup
|
19
|
+
@redis = Redis.new(:host => 'localhost', :port => 6379, :thread_safe => true, :db => 0)
|
20
|
+
end
|
21
|
+
|
22
|
+
def teardown
|
23
|
+
@redis.quit
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_driver(conf, tag='test')
|
27
|
+
Fluent::Test::BufferedOutputTestDriver.new(Fluent::OnlineuserOutput, tag).configure(conf)
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_event_counter
|
31
|
+
d = create_driver CONFIG
|
32
|
+
t = Time.now
|
33
|
+
time = t.to_i
|
34
|
+
d.emit({"name" => "test", "uid" => 1}, time)
|
35
|
+
d.emit({"name" => "test", "uid" => 2}, time)
|
36
|
+
d.emit({"name" => "test", "uid" => 3}, time)
|
37
|
+
d.run
|
38
|
+
|
39
|
+
assert_equal 3, @redis.zcard('counter:online_user:all')
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fluent-plugin-onlineuser
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Yuyang Lan
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-16 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: fluentd
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: redis
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: fluentd
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: redis
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
description: Fluentd plugin to count online users. It's based on Redis and the sorted
|
95
|
+
set data type.
|
96
|
+
email:
|
97
|
+
- lanyuyang@gree.net
|
98
|
+
executables: []
|
99
|
+
extensions: []
|
100
|
+
extra_rdoc_files: []
|
101
|
+
files:
|
102
|
+
- .gitignore
|
103
|
+
- Gemfile
|
104
|
+
- README.md
|
105
|
+
- Rakefile
|
106
|
+
- fluent-plugin-onlineuser.gemspec
|
107
|
+
- lib/fluent/plugin/out_onlineuser.rb
|
108
|
+
- test/fluentd.conf
|
109
|
+
- test/plugin/test_out_onlineuser.rb
|
110
|
+
homepage: https://github.com/lanyuyang/fluent-plugin-onlineuser
|
111
|
+
licenses: []
|
112
|
+
post_install_message:
|
113
|
+
rdoc_options: []
|
114
|
+
require_paths:
|
115
|
+
- lib
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
117
|
+
none: false
|
118
|
+
requirements:
|
119
|
+
- - ! '>='
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
none: false
|
124
|
+
requirements:
|
125
|
+
- - ! '>='
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
requirements: []
|
129
|
+
rubyforge_project:
|
130
|
+
rubygems_version: 1.8.24
|
131
|
+
signing_key:
|
132
|
+
specification_version: 3
|
133
|
+
summary: Online Users Counter
|
134
|
+
test_files:
|
135
|
+
- test/fluentd.conf
|
136
|
+
- test/plugin/test_out_onlineuser.rb
|