fnordmetric 0.5.1 → 0.5.2

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.
Files changed (65) hide show
  1. data/.travis.yml +1 -0
  2. data/VERSION +1 -1
  3. data/doc/preview1.png +0 -0
  4. data/doc/preview2.png +0 -0
  5. data/doc/ulm_stats.rb +622 -0
  6. data/doc/version +1 -0
  7. data/fnordmetric.gemspec +16 -38
  8. data/haml/app.haml +12 -5
  9. data/lib/fnordmetric.rb +3 -0
  10. data/lib/fnordmetric/app.rb +19 -10
  11. data/lib/fnordmetric/bars_widget.rb +26 -0
  12. data/lib/fnordmetric/context.rb +3 -3
  13. data/lib/fnordmetric/gauge.rb +20 -0
  14. data/lib/fnordmetric/gauge_calculations.rb +28 -4
  15. data/lib/fnordmetric/gauge_modifiers.rb +39 -6
  16. data/lib/fnordmetric/logger.rb +19 -0
  17. data/lib/fnordmetric/numbers_widget.rb +5 -15
  18. data/lib/fnordmetric/pie_widget.rb +23 -0
  19. data/lib/fnordmetric/standalone.rb +1 -1
  20. data/lib/fnordmetric/timeline_widget.rb +16 -23
  21. data/lib/fnordmetric/toplist_widget.rb +25 -0
  22. data/lib/fnordmetric/widget.rb +3 -3
  23. data/pub/{fnordmetric/fnordmetric.css → fnordmetric.css} +46 -36
  24. data/pub/fnordmetric.js +1069 -0
  25. data/pub/loader.gif +0 -0
  26. data/pub/{highcharts → vendor}/highcharts.js +0 -0
  27. data/pub/{jquery-1.6.1.min.js → vendor/jquery-1.6.1.min.js} +0 -0
  28. data/readme.rdoc +228 -311
  29. data/spec/app_spec.rb +63 -3
  30. data/spec/gauge_modifiers_spec.rb +157 -2
  31. data/spec/gauge_spec.rb +143 -12
  32. data/spec/widget_spec.rb +18 -18
  33. metadata +33 -58
  34. data/.document +0 -5
  35. data/_spec/app_spec.rb +0 -178
  36. data/_spec/cache_spec.rb +0 -53
  37. data/_spec/combine_metric_spec.rb +0 -19
  38. data/_spec/core_spec.rb +0 -50
  39. data/_spec/count_metric_spec.rb +0 -32
  40. data/_spec/dashboard_spec.rb +0 -67
  41. data/_spec/event_spec.rb +0 -46
  42. data/_spec/metric_spec.rb +0 -118
  43. data/_spec/report_spec.rb +0 -87
  44. data/_spec/sum_metric_spec.rb +0 -33
  45. data/_spec/widget_spec.rb +0 -107
  46. data/doc/example_server.rb +0 -56
  47. data/doc/import_dump.rb +0 -26
  48. data/pub/fnordmetric/fnordmetric.js +0 -543
  49. data/pub/fnordmetric/widget_numbers.js +0 -71
  50. data/pub/fnordmetric/widget_timeline.css +0 -0
  51. data/pub/fnordmetric/widget_timeline.js +0 -110
  52. data/pub/highcharts/adapters/mootools-adapter.js +0 -12
  53. data/pub/highcharts/adapters/mootools-adapter.src.js +0 -243
  54. data/pub/highcharts/adapters/prototype-adapter.js +0 -14
  55. data/pub/highcharts/adapters/prototype-adapter.src.js +0 -284
  56. data/pub/highcharts/highcharts.src.js +0 -11103
  57. data/pub/highcharts/modules/exporting.js +0 -22
  58. data/pub/highcharts/modules/exporting.src.js +0 -703
  59. data/pub/highcharts/themes/dark-blue.js +0 -268
  60. data/pub/highcharts/themes/dark-green.js +0 -268
  61. data/pub/highcharts/themes/gray.js +0 -262
  62. data/pub/highcharts/themes/grid.js +0 -97
  63. data/pub/raphael-min.js +0 -8
  64. data/pub/raphael-utils.js +0 -221
  65. data/ulm_stats.rb +0 -198
@@ -21,6 +21,10 @@ describe "app" do
21
21
  @app ||= FnordMetric::App.new({
22
22
  :foospace => proc{
23
23
  widget 'Blubb', nil
24
+
25
+ gauge :testgauge, :tick => 1.hour.to_i, :progressive => true
26
+ gauge :test3gauge, :tick => 1.hour.to_i, :three_dimensional => true
27
+
24
28
  }
25
29
  }, @opts)
26
30
  end
@@ -407,14 +411,70 @@ describe "app" do
407
411
  #FnordMetric::Event.last.blubb.should == 42.23
408
412
  end
409
413
 
414
+ end
410
415
 
416
+ describe "gauges api" do
411
417
 
418
+ before(:all) do
419
+ @redis.keys("fnordmetric-foospace*").each { |k| @redis.del(k) }
420
+ gauge_key = "fnordmetric-foospace-gauge-testgauge-#{1.hour.to_i}"
421
+ @redis.hset(gauge_key, 1323687600, "18")
422
+ @redis.hset(gauge_key, 1323691200, "23")
423
+ end
412
424
 
413
- end
425
+ it "should return the right answer for: /metric/:name?at=timestamp" do
426
+ get "/foospace/gauge/testgauge?at=1323691205"
427
+ JSON.parse(last_response.body).first.last.to_i.should == 23
428
+ end
429
+
430
+ it "should return the right answer for: /metric/:name?at=timestamp" do
431
+ get "/foospace/gauge/testgauge?at=1323691200"
432
+ JSON.parse(last_response.body).first.last.to_i.should == 23
433
+ end
434
+
435
+ it "should return the right answer for: /metric/:name?at=timestamp-timstamp" do
436
+ get "/foospace/gauge/testgauge?at=1323691200-1323691205"
437
+ JSON.parse(last_response.body).first.last.to_i.should == 18
438
+ end
439
+
440
+ it "should return the right answer for: /metric/:name?at=timestamp-timstamp" do
441
+ get "/foospace/gauge/testgauge?at=1323691201-1323695205"
442
+ JSON.parse(last_response.body).keys.length
443
+ end
444
+
445
+ it "should return the right answer for: /metric/:name?at=timestamp-timstamp" do
446
+ get "/foospace/gauge/testgauge?at=1323691199-1323701201"
447
+ JSON.parse(last_response.body).keys.length.should == 4
448
+ JSON.parse(last_response.body)["1323687600"].to_i.should == 18
449
+ JSON.parse(last_response.body)["1323691200"].to_i.should == 23
450
+ end
451
+
452
+ it "should return the right answer for: /metric/:name?at=timestamp-timstamp&sum=true" do
453
+ get "/foospace/gauge/testgauge?at=1323691199-1323701201&sum=true"
454
+ JSON.parse(last_response.body).keys.length.should == 1
455
+ JSON.parse(last_response.body)["sum"].to_i.should == 18+23
456
+ end
414
457
 
415
- describe "metrics api" do
416
- # copy from _spec/app_spec.rb ?
417
458
  end
418
459
 
460
+ describe "three-dim gauges api" do
461
+
462
+ before(:all) do
463
+ @redis.keys("fnordmetric-foospace*").each { |k| @redis.del(k) }
464
+ gauge_key = "fnordmetric-foospace-gauge-test3gauge-#{1.hour.to_i}-1323691200"
465
+ @redis.zadd(gauge_key, 18, "fnordyblubb")
466
+ @redis.zadd(gauge_key, 23, "uberfoo")
467
+ @redis.set(gauge_key+"-count", 41)
468
+ end
469
+
470
+ it "should return the right answer for: /metric/:name?at=timestamp" do
471
+ get "/foospace/gauge/test3gauge?at=1323691205"
472
+ JSON.parse(last_response.body)["count"].to_i.should == 41
473
+ JSON.parse(last_response.body)["values"].length.should == 2
474
+ JSON.parse(last_response.body)["values"][0].should == ["uberfoo", "23"]
475
+ JSON.parse(last_response.body)["values"][1].should == ["fnordyblubb", "18"]
476
+ end
477
+
478
+ end
419
479
 
420
480
  end
@@ -153,6 +153,28 @@ describe FnordMetric::GaugeModifiers do
153
153
 
154
154
  end
155
155
 
156
+ describe "increment an average-gauge" do
157
+
158
+ it "should increment_unique a non-progressive gauge" do
159
+ gauge_key = "fnordmetrics-myns-gauge-mygauge_917-10"
160
+ @redis.hset(gauge_key, "695280200", "54")
161
+ @redis.set(gauge_key+"-695280200-value-count", 5)
162
+ create_gauge_context({
163
+ :key => "mygauge_917",
164
+ :average => true,
165
+ :tick => 10
166
+ }, proc{
167
+ incr(:mygauge_917, 30)
168
+ }).tap do |context|
169
+ event = { :_time => @now, :_session_key => "mysesskey" }
170
+ context.call(event, @redis_wrap)
171
+ end
172
+ @redis.hget(gauge_key, "695280200").should == "84"
173
+ @redis.get(gauge_key+"-695280200-value-count").should == "6"
174
+ end
175
+
176
+ end
177
+
156
178
  describe "increment uniquely by session_key" do
157
179
 
158
180
  it "should increment_unique a non-progressive gauge" do
@@ -257,8 +279,141 @@ describe FnordMetric::GaugeModifiers do
257
279
 
258
280
  end
259
281
 
260
- it "should increment a field on a three-dimensional gauge by 1"
261
- it "should increment a field on a three-dimensional gauge by 5"
282
+ describe "increment three-dimensional gagues" do
283
+
284
+ it "should increment a three-dim gauge by 1" do
285
+ gauge_key = "fnordmetrics-myns-gauge-mygauge_434-10-695280200"
286
+ @redis.zadd(gauge_key, 12, "whoopwhoop")
287
+ create_gauge_context({
288
+ :key => "mygauge_434",
289
+ :three_dimensional => true,
290
+ :tick => 10,
291
+ }, proc{
292
+ incr_field(:mygauge_434, "whoopwhoop", 1)
293
+ }).tap do |context|
294
+ event = { :_time => @now }
295
+ context.call(event, @redis_wrap)
296
+ end
297
+ @redis.zscore(gauge_key, "whoopwhoop").should == "13"
298
+ @redis.get(gauge_key+"-count").should == "1"
299
+ end
300
+
301
+ it "should increment a three-dim gauge by 5 on an empty field" do
302
+ gauge_key = "fnordmetrics-myns-gauge-mygauge_634-10-695280200"
303
+ @redis.set(gauge_key+"-count", 6)
304
+ create_gauge_context({
305
+ :key => "mygauge_634",
306
+ :three_dimensional => true,
307
+ :tick => 10,
308
+ }, proc{
309
+ incr_field(:mygauge_634, "whoopwhoop", 5)
310
+ }).tap do |context|
311
+ event = { :_time => @now }
312
+ context.call(event, @redis_wrap)
313
+ end
314
+ @redis.zscore(gauge_key, "whoopwhoop").should == "5"
315
+ @redis.get(gauge_key+"-count").should == "7"
316
+ end
317
+
318
+ it "should increment a three-dim gauge by event supplied field" do
319
+ gauge_key = "fnordmetrics-myns-gauge-mygauge_634-10-695280200"
320
+ @redis.zadd(gauge_key, 11, "fnordybar")
321
+ create_gauge_context({
322
+ :key => "mygauge_634",
323
+ :three_dimensional => true,
324
+ :tick => 10,
325
+ }, proc{
326
+ incr_field(:mygauge_634, data[:myfield], 5)
327
+ }).tap do |context|
328
+ event = { :_time => @now, :myfield => "fnordybar" }
329
+ context.call(event, @redis_wrap)
330
+ end
331
+ @redis.zscore(gauge_key, "fnordybar").should == "16"
332
+ end
333
+
334
+ it "should increment_unique a three-dim gauge" do
335
+ gauge_key = "fnordmetrics-myns-gauge-mygauge_1263-10-695280200"
336
+ @redis.zadd(gauge_key, 54, "mykey")
337
+ @redis.set(gauge_key+"-sessions-count", 5)
338
+ create_gauge_context({
339
+ :key => "mygauge_1263",
340
+ :tick => 10,
341
+ :unique => true,
342
+ :three_dimensional => true
343
+ }, proc{
344
+ incr_field(:mygauge_1263, "mykey", 30)
345
+ }).tap do |context|
346
+ event = { :_time => @now, :_session_key => "mysesskey" }
347
+ context.call(event, @redis_wrap)
348
+ end
349
+ @redis.zscore(gauge_key, "mykey").should == "84"
350
+ @redis.get(gauge_key+"-sessions-count").should == "6"
351
+ @redis.smembers(gauge_key+"-sessions").should == ["mysesskey"]
352
+ end
353
+
354
+ it "should not increment_unique a non-progressive gauge if session is known" do
355
+ gauge_key = "fnordmetrics-myns-gauge-mygauge_1266-695280200"
356
+ @redis.zadd(gauge_key, 54, "otherkey")
357
+ @redis.set(gauge_key+"-sessions-count", 5)
358
+ @redis.sadd(gauge_key+"-sessions", "mysesskey")
359
+ create_gauge_context({
360
+ :key => "mygauge_1266",
361
+ :tick => 10,
362
+ :unique => true,
363
+ :three_dimensional => true
364
+ }, proc{
365
+ incr_field(:mygauge_1266, "otherkey", 30)
366
+ }).tap do |context|
367
+ event = { :_time => @now, :_session_key => "mysesskey" }
368
+ context.call(event, @redis_wrap)
369
+ end
370
+ @redis.zscore(gauge_key, "otherkey").should == "54"
371
+ @redis.get(gauge_key+"-sessions-count").should == "5"
372
+ @redis.smembers(gauge_key+"-sessions").should == ["mysesskey"]
373
+ end
374
+
375
+ end
376
+
377
+ describe "set value on two/three-dim gauge" do
378
+
379
+ it "should set a value on a two-dim gauge" do
380
+ gauge_key = "fnordmetrics-myns-gauge-mygauge_5463-10"
381
+ @redis.hset(gauge_key, "695280200", "54")
382
+ @redis.set(gauge_key+"-695280200-sessions-count", 5)
383
+ @redis.hget(gauge_key, "695280200").should == "54"
384
+ create_gauge_context({
385
+ :key => "mygauge_5463",
386
+ :tick => 10
387
+ }, proc{
388
+ set_value(:mygauge_5463, 17)
389
+ }).tap do |context|
390
+ event = { :_time => @now, :_session_key => "mysesskey" }
391
+ context.call(event, @redis_wrap)
392
+ end
393
+ @redis.hget(gauge_key, "695280200").should == "17"
394
+ end
395
+
396
+
397
+ it "should set a value on a two-dim gauge" do
398
+ gauge_key = "fnordmetrics-myns-gauge-mygauge_1463-10-695280200"
399
+ @redis.zadd(gauge_key, 65, "asdasdkey")
400
+ @redis.zscore(gauge_key, "asdasdkey").should == "65"
401
+ create_gauge_context({
402
+ :key => "mygauge_1463",
403
+ :three_dimensional => true,
404
+ :tick => 10
405
+ }, proc{
406
+ set_field(:mygauge_1463, "asdasdkey", 23)
407
+ }).tap do |context|
408
+ event = { :_time => @now, :_session_key => "mysesskey" }
409
+ context.call(event, @redis_wrap)
410
+ end
411
+ @redis.zscore(gauge_key, "asdasdkey").should == "23"
412
+ end
413
+
414
+ end
415
+
416
+
262
417
  it "should raise an error if incr_field is called on a 2d gauge"
263
418
 
264
419
  private
@@ -12,11 +12,21 @@ describe FnordMetric::Gauge do
12
12
  @redis.keys("fnordmetric-myns*").each { |k| @redis.del(k) }
13
13
  end
14
14
 
15
- it "should remember it's own name" do
15
+ it "should remember its own name" do
16
16
  gauge = FnordMetric::Gauge.new({:key_prefix => "foo", :key => "fnordgauge"})
17
17
  gauge.name.should == "fnordgauge"
18
18
  end
19
19
 
20
+ it "should return its key as title if none specified" do
21
+ gauge = FnordMetric::Gauge.new({:key_prefix => "foo", :key => "fnordgauge"})
22
+ gauge.title.should == "fnordgauge"
23
+ end
24
+
25
+ it "should return its title as title if none specified" do
26
+ gauge = FnordMetric::Gauge.new({:key_prefix => "foo", :key => "fnordgauge", :title => "Fnord Gauge"})
27
+ gauge.title.should == "Fnord Gauge"
28
+ end
29
+
20
30
  it "should raise an error when initialize without key" do
21
31
  lambda{
22
32
  FnordMetric::Gauge.new({:key_prefix => "foo"})
@@ -41,9 +51,15 @@ describe FnordMetric::Gauge do
41
51
 
42
52
  describe "ticks" do
43
53
 
44
- it "should return the correct tick if configured"
45
- it "should return the default tick if none configured"
54
+ it "should return the correct tick if configured" do
55
+ gauge = FnordMetric::Gauge.new({:tick => 23, :key_prefix => "fnordmetrics-myns", :key => "mygauge"})
56
+ gauge.tick.should == 23
57
+ end
46
58
 
59
+ it "should return the default tick if none configured" do
60
+ gauge = FnordMetric::Gauge.new({:key_prefix => "fnordmetrics-myns", :key => "mygauge"})
61
+ gauge.tick.should == 3600
62
+ end
47
63
 
48
64
  it "should return the correct tick_at" do
49
65
  gauge = FnordMetric::Gauge.new({:tick => 10, :key_prefix => "fnordmetrics-myns", :key => "mygauge"})
@@ -81,8 +97,30 @@ describe FnordMetric::Gauge do
81
97
  end
82
98
 
83
99
  it "should return the correct value_at per session" do
100
+ @redis.hset(@gauge_key, "695280200", "76")
84
101
  @redis.set(@gauge_key+"-695280200-sessions-count", "23")
85
- @gauge.value_at(@now, :avg_per_session => 1).should == (54.0/23.0)
102
+ _gauge = FnordMetric::Gauge.new({
103
+ :tick => 10,
104
+ :key_prefix => "fnordmetric-myns",
105
+ :key => "mygauge_966",
106
+ :unique => true,
107
+ :redis => @redis
108
+ })
109
+ _gauge.value_at(@now).should == "76"
110
+ end
111
+
112
+ it "should return the correct value_at per session with avg" do
113
+ @redis.hset(@gauge_key, "695280200", "76")
114
+ @redis.set(@gauge_key+"-695280200-sessions-count", "23")
115
+ _gauge = FnordMetric::Gauge.new({
116
+ :tick => 10,
117
+ :key_prefix => "fnordmetric-myns",
118
+ :key => "mygauge_966",
119
+ :unique => true,
120
+ :average => true,
121
+ :redis => @redis
122
+ })
123
+ _gauge.value_at(@now).should == (76.0/23.0)
86
124
  end
87
125
 
88
126
  it "should receive gauge values for multiple ticks" do
@@ -95,7 +133,33 @@ describe FnordMetric::Gauge do
95
133
  it "should receive gauge values per session for multiple ticks" do
96
134
  @redis.set(@gauge_key+"-695280200-sessions-count", "23")
97
135
  @redis.set(@gauge_key+"-695280210-sessions-count", "8")
98
- @gauge.values_at([@now, @now+8], :avg_per_session => 1).should == {
136
+ @redis.hset(@gauge_key, "695280200", "76")
137
+ @redis.hset(@gauge_key, "695280210", "56")
138
+ _gauge = FnordMetric::Gauge.new({
139
+ :tick => 10,
140
+ :key_prefix => "fnordmetric-myns",
141
+ :key => "mygauge_966",
142
+ :unique => true,
143
+ :redis => @redis
144
+ })
145
+ _gauge.values_at([@now, @now+8]).should == {
146
+ 695280200 => "76",
147
+ 695280210 => "56"
148
+ }
149
+ end
150
+
151
+ it "should receive gauge values per session for multiple ticks with avg" do
152
+ @redis.set(@gauge_key+"-695280200-sessions-count", "23")
153
+ @redis.set(@gauge_key+"-695280210-sessions-count", "8")
154
+ _gauge = FnordMetric::Gauge.new({
155
+ :tick => 10,
156
+ :key_prefix => "fnordmetric-myns",
157
+ :key => "mygauge_966",
158
+ :unique => true,
159
+ :average => true,
160
+ :redis => @redis
161
+ })
162
+ _gauge.values_at([@now, @now+8]).should == {
99
163
  695280200 => (54.0/23.0),
100
164
  695280210 => (123.0/8.0)
101
165
  }
@@ -111,18 +175,85 @@ describe FnordMetric::Gauge do
111
175
  end
112
176
 
113
177
  it "should receive gauge values for all ticks in a given range" do
114
- @gauge.values_in(@now..@now+8).should == {
178
+ @gauge.values_in(@now+10..@now+18).should == {
115
179
  695280200 => "54",
116
180
  695280210 => "123"
117
181
  }
118
- @gauge.values_in(@now..@now+6).should == {
119
- 695280200 => "54"
120
- }
121
- @gauge.values_in(@now+8..@now+10).should == {
122
- 695280210 => "123"
123
- }
124
182
  end
125
183
 
126
184
  end
127
185
 
186
+ describe "three-dim value retrival" do
187
+
188
+ before(:each) do
189
+ @gauge = FnordMetric::Gauge.new({
190
+ :tick => 10,
191
+ :key_prefix => "fnordmetric-myns",
192
+ :three_dimensional => true,
193
+ :key => "mygauge_966",
194
+ :redis => @redis
195
+ })
196
+ @redis.keys("fnordmetric-myns*").each { |k| @redis.del(k) }
197
+ @gauge_key = "fnordmetric-myns-gauge-mygauge_966-10-1323691200"
198
+ @redis.zadd(@gauge_key, 18, "fnordyblubb")
199
+ @redis.zadd(@gauge_key, 23, "uberfoo")
200
+ @redis.set(@gauge_key+"-count", 41)
201
+ end
202
+
203
+ it "should retrieve field_values at a given time" do
204
+ @gauge.field_values_at(1323691200).should be_a(Array)
205
+ @gauge.field_values_at(1323691200).length.should == 2
206
+ @gauge.field_values_at(1323691200)[0].should == ["uberfoo", "23"]
207
+ @gauge.field_values_at(1323691200)[1].should == ["fnordyblubb", "18"]
208
+ end
209
+
210
+ it "should retrieve the correct total count" do
211
+ @gauge.field_values_total(1323691200).should == 41
212
+ end
213
+
214
+ it "should retrieve max 50 fields per default" do
215
+ 70.times{ |n| @redis.zadd(@gauge_key, 23, "field#{n}") }
216
+ @gauge.field_values_at(1323691200).should be_a(Array)
217
+ @gauge.field_values_at(1323691200).length.should == 50
218
+ end
219
+
220
+ it "should retrieve more than 50 fields if requested" do
221
+ 70.times{ |n| @redis.zadd(@gauge_key, 23, "field#{n}") }
222
+ @gauge.field_values_at(1323691200, :max_fields => 60).should be_a(Array)
223
+ @gauge.field_values_at(1323691200, :max_fields => 60).length.should == 60
224
+ end
225
+
226
+ it "should retrieve all fields if requested" do
227
+ 70.times{ |n| @redis.zadd(@gauge_key, 23, "field#{n}") }
228
+ @gauge.field_values_at(1323691200, :max_fields => 0).should be_a(Array)
229
+ @gauge.field_values_at(1323691200, :max_fields => 0).length.should == 72
230
+ end
231
+
232
+ it "should call the value calculation block and return the result" do
233
+ vals = @gauge.field_values_at(1323691200){ |v| v.to_i + 123 }
234
+ vals.should be_a(Array)
235
+ vals.length.should == 2
236
+ vals[0].should == ["uberfoo", 146]
237
+ vals[1].should == ["fnordyblubb", 141]
238
+ end
239
+
240
+ it "should return the correct field_values per session with avg" do
241
+ @redis.set(@gauge_key+"-sessions-count", "3")
242
+ @gauge = FnordMetric::Gauge.new({
243
+ :tick => 10,
244
+ :key_prefix => "fnordmetric-myns",
245
+ :three_dimensional => true,
246
+ :unique => true,
247
+ :average => true,
248
+ :key => "mygauge_966",
249
+ :redis => @redis
250
+ })
251
+ vals = @gauge.field_values_at(1323691200)
252
+ vals.should be_a(Array)
253
+ vals.length.should == 2
254
+ vals[0].should == ["uberfoo", 23/3.0]
255
+ vals[1].should == ["fnordyblubb", 18/3.0]
256
+ end
257
+
258
+ end
128
259
  end