diff --git a/README.md b/README.md index 3941de0..ac00f09 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,25 @@ A Redis-backed statistics storage and querying library written in Ruby. -Redistat was originally created to replace a small hacked together statistics collection solution which was MySQL-based. When I started I had a short list of requirements: +Redistat was originally created to replace a small hacked together statistics +collection solution which was MySQL-based. When I started I had a short list +of requirements: * Store and increment/decrement integer values (counters, etc) * Up to the second statistics available at all times * Screamingly fast -Redis fits perfectly with all of these requirements. It has atomic operations like increment, and it's lightning fast, meaning if the data is structured well, the initial stats reporting call will store data in a format that's instantly retrievable just as fast. +Redis fits perfectly with all of these requirements. It has atomic operations +like increment, and it's lightning fast, meaning if the data is structured +well, the initial stats reporting call will store data in a format that's +instantly retrievable just as fast. ## Installation ## gem install redistat -If you are using Ruby 1.8.x, it's recommended you also install the `SystemTimer` gem, as the Redis gem will otherwise complain. +If you are using Ruby 1.8.x, it's recommended you also install the +`SystemTimer` gem, as the Redis gem will otherwise complain. ## Usage (Crash Course) ## @@ -129,39 +135,71 @@ finder.children.map { |child| child.total } ### Scope ### -A type of global-namespace for storing data. When using the `Redistat::Model` wrapper, the scope is automatically set to the class name. In the examples above, the scope is `ViewStats`. Can be overridden by calling the `#scope` class method on your model class. +A type of global-namespace for storing data. When using the `Redistat::Model` +wrapper, the scope is automatically set to the class name. In the examples +above, the scope is `ViewStats`. Can be overridden by calling the `#scope` +class method on your model class. ### Label ### -Identifier string to separate different types and groups of statistics from each other. The first argument of the `#store`, `#find`, and `#fetch` methods is the label that you're storing to, or fetching from. +Identifier string to separate different types and groups of statistics from +each other. The first argument of the `#store`, `#find`, and `#fetch` methods +is the label that you're storing to, or fetching from. -Labels support multiple grouping levels by splitting the label string with `/` and storing the same stats for each level. For example, when storing data to a label called `views/product/44`, the data is stored for the label you specify, and also for `views/product` and `views`. +Labels support multiple grouping levels by splitting the label string with `/` +and storing the same stats for each level. For example, when storing data to a +label called `views/product/44`, the data is stored for the label you specify, +and also for `views/product` and `views`. -A word of caution: Don't use a crazy number of group levels. As two levels causes twice as many `hincrby` calls to Redis as not using the grouping feature. Hence using 10 grouping levels, causes 10 times as many write calls to Redis. +A word of caution: Don't use a crazy number of group levels. As two levels +causes twice as many `hincrby` calls to Redis as not using the grouping +feature. Hence using 10 grouping levels, causes 10 times as many write calls +to Redis. ### Input Statistics Data ### -You provide Redistat with the data you want to store using a Ruby Hash. This data is then stored in a corresponding Redis hash with identical key/field names. +You provide Redistat with the data you want to store using a Ruby Hash. This +data is then stored in a corresponding Redis hash with identical key/field +names. -Key names in the hash also support grouping features similar to those available for Labels. Again, the more levels you use, the more write calls to Redis, so avoid using 10-15 levels. +Key names in the hash also support grouping features similar to those +available for Labels. Again, the more levels you use, the more write calls to +Redis, so avoid using 10-15 levels. ### Depth (Storage Accuracy) ### -Define how accurately data should be stored, and how accurately it's looked up when fetching it again. By default Redistat uses a depth value of `:hour`, which means it's impossible to separate two events which were stored at 10:18 and 10:23. In Redis they are both stored within a date key of `2011031610`. +Define how accurately data should be stored, and how accurately it's looked up +when fetching it again. By default Redistat uses a depth value of `:hour`, +which means it's impossible to separate two events which were stored at 10:18 +and 10:23. In Redis they are both stored within a date key of `2011031610`. -You can set depth within your model using the `#depth` class method. Available depths are: `:year`, `:month`, `:day`, `:hour`, `:min`, `:sec` +You can set depth within your model using the `#depth` class method. Available +depths are: `:year`, `:month`, `:day`, `:hour`, `:min`, `:sec` ### Time Ranges ### -When you fetch data, you need to specify a start and an end time. The selection behavior can seem a bit weird at first when, but makes sense when you understand how Redistat works internally. +When you fetch data, you need to specify a start and an end time. The +selection behavior can seem a bit weird at first when, but makes sense when +you understand how Redistat works internally. -For example, if we are using a Depth value of `:hour`, and we trigger a fetch call starting at `1.hour.ago` (13:34), till `Time.now` (14:34), only stats from 13:00:00 till 13:59:59 are returned, as they were all stored within the key for the 13th hour. If both 13:00 and 14:00 was returned, you would get results from two whole hours. Hence if you want up to the second data, use an end time of `1.hour.from_now`. +For example, if we are using a Depth value of `:hour`, and we trigger a fetch +call starting at `1.hour.ago` (13:34), till `Time.now` (14:34), only stats +from 13:00:00 till 13:59:59 are returned, as they were all stored within the +key for the 13th hour. If both 13:00 and 14:00 was returned, you would get +results from two whole hours. Hence if you want up to the second data, use an +end time of `1.hour.from_now`. ### The Finder Object ### -Calling the `#find` method on a Redistat model class returns a `Redistat::Finder` object. The finder is a lazy-loaded gateway to your data. Meaning you can create a new finder, and modify instantiated finder's label, scope, dates, and more. It does not call Redis and fetch the data until you call `#total`, `#all`, `#map`, `#each`, or `#each_with_index` on the finder. +Calling the `#find` method on a Redistat model class returns a +`Redistat::Finder` object. The finder is a lazy-loaded gateway to your +data. Meaning you can create a new finder, and modify instantiated finder's +label, scope, dates, and more. It does not call Redis and fetch the data until +you call `#total`, `#all`, `#map`, `#each`, or `#each_with_index` on the +finder. -This section does need further expanding as there's a lot to cover when it comes to the finder. +This section does need further expanding as there's a lot to cover when it +comes to the finder. @@ -169,7 +207,8 @@ This section does need further expanding as there's a lot to cover when it comes ### Storing / Writing ### -Redistat stores all data into a Redis hash keys. The Redis key name the used consists of three parts. The scope, label, and datetime: +Redistat stores all data into a Redis hash keys. The Redis key name the used +consists of three parts. The scope, label, and datetime: {scope}/{label}:{datetime} @@ -200,28 +239,52 @@ ViewStats.store('views/product/44', {'count/chrome/11' => 1}) ViewStats/views/product/44:20110315 ViewStats/views/product/44:2011031510 -...by creating the Redis key, and/or hash field if needed, otherwise it simply increments the already existing data. +...by creating the Redis key, and/or hash field if needed, otherwise it simply +increments the already existing data. -It would also create the following Redis sets to keep track of which child labels are available: +It would also create the following Redis sets to keep track of which child +labels are available: ViewStats.label_index: ViewStats.label_index:views ViewStats.label_index:views/product -It should now be more obvious to you why you should think about how you use the grouping capabilities so you don't go crazy and use 10-15 levels. Storing is done through Redis' `hincrby` call, which only supports a single key/field combo. Meaning the above example would call `hincrby` a total of 36 times to store the data, and `sadd` a total of 3 times to ensure the label index is accurate. 39 calls is however not a problem for Redis, most calls happen in less than 0.15ms (0.00015 seconds) on my local machine. +It should now be more obvious to you why you should think about how you use +the grouping capabilities so you don't go crazy and use 10-15 levels. Storing +is done through Redis' `hincrby` call, which only supports a single key/field +combo. Meaning the above example would call `hincrby` a total of 36 times to +store the data, and `sadd` a total of 3 times to ensure the label index is +accurate. 39 calls is however not a problem for Redis, most calls happen in +less than 0.15ms (0.00015 seconds) on my local machine. ### Fetching / Reading ### -By default when fetching statistics, Redistat will figure out how to do the least number of reads from Redis. First it checks how long range you're fetching. If whole days, months or years for example fit within the start and end dates specified, it will fetch the one key for the day/month/year in question. It further drills down to the smaller units. +By default when fetching statistics, Redistat will figure out how to do the +least number of reads from Redis. First it checks how long range you're +fetching. If whole days, months or years for example fit within the start and +end dates specified, it will fetch the one key for the day/month/year in +question. It further drills down to the smaller units. -It is also intelligent enough to not fetch each day from 3-31 of a month, instead it would fetch the data for the whole month and the first two days, which are then removed from the summary of the whole month. This means three calls to `hgetall` instead of 29 if each whole day was fetched. +It is also intelligent enough to not fetch each day from 3-31 of a month, +instead it would fetch the data for the whole month and the first two days, +which are then removed from the summary of the whole month. This means three +calls to `hgetall` instead of 29 if each whole day was fetched. ### Buffer ### -The buffer is a new, still semi-beta, feature aimed to reduce the number of Redis `hincrby` that Redistat sends. This should only really be useful when you're hitting north of 30,000 Redis requests per second, if your Redis server has limited resources, or against my recommendation you've opted to use 10, 20, or more label grouping levels. +The buffer is a new, still semi-beta, feature aimed to reduce the number of +Redis `hincrby` that Redistat sends. This should only really be useful when +you're hitting north of 30,000 Redis requests per second, if your Redis server +has limited resources, or against my recommendation you've opted to use 10, +20, or more label grouping levels. -Buffering tries to fold together multiple `store` calls into as few as possible by merging the statistics hashes from all calls and groups them based on scope, label, date depth, and more. You configure the the buffer by setting `Redistat.buffer_size` to an integer higher than 1. This basically tells Redistat how many `store` calls to buffer in memory before writing all data to Redis. +Buffering tries to fold together multiple `store` calls into as few as +possible by merging the statistics hashes from all calls and groups them based +on scope, label, date depth, and more. You configure the the buffer by setting +`Redistat.buffer_size` to an integer higher than 1. This basically tells +Redistat how many `store` calls to buffer in memory before writing all data to +Redis. ## Todo ## @@ -233,7 +296,9 @@ Buffering tries to fold together multiple `store` calls into as few as possible ## Credits ## -[Global Personals](http://globalpersonals.co.uk/) deserves a thank you. Currently the primary user of Redistat, they've allowed me to spend some company time to further develop the project. +[Global Personals](http://globalpersonals.co.uk/) deserves a thank +you. Currently the primary user of Redistat, they've allowed me to spend some +company time to further develop the project. ## Note on Patches/Pull Requests ## @@ -242,8 +307,9 @@ Buffering tries to fold together multiple `store` calls into as few as possible * Make your feature addition or bug fix. * Add tests for it. This is important so I don't break it in a future version unintentionally. -* Commit, do not mess with rakefile, version, or history. - (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) +* Commit, do not mess with rakefile, version, or history. (if you want to + have your own version, that is fine but bump version in a commit by itself I + can ignore when I pull) * Send me a pull request. Bonus points for topic branches.