Skip to content

Commit 3c4845a

Browse files
authored
feat: add event summarizer (#19)
* Added event summaries to the EventProcessor
1 parent d2cbf8e commit 3c4845a

16 files changed

+759
-162
lines changed

.clang-tidy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
---
22
CheckOptions:
33
- { key: readability-identifier-length.IgnoredParameterNames, value: 'i|j|k|c|os|it' }
4+
- { key: readability-identifier-length.IgnoredVariableNames, value: 'ec' }
45
- { key: readability-identifier-length.IgnoredLoopCounterNames, value: 'i|j|k|c|os|it' }

libs/common/include/events/client_events.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ struct FeatureEventParams {
1919
std::string key;
2020
Context context;
2121
EvaluationResult eval_result;
22+
Value default_;
2223
};
2324

2425
struct FeatureEventBase {
@@ -29,6 +30,8 @@ struct FeatureEventBase {
2930
Value value;
3031
std::optional<Reason> reason;
3132
Value default_;
33+
34+
explicit FeatureEventBase(FeatureEventParams const& params);
3235
};
3336

3437
struct FeatureEvent : public FeatureEventBase {

libs/common/include/events/common_events.hpp

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,6 @@ struct Date {
2424
std::chrono::system_clock::time_point t;
2525
};
2626

27-
struct VariationSummary {
28-
std::size_t count;
29-
Value value;
30-
};
31-
32-
struct VariationKey {
33-
Version version;
34-
std::optional<VariationIndex> variation;
35-
36-
struct Hash {
37-
auto operator()(VariationKey const& p) const -> size_t {
38-
if (p.variation) {
39-
return std::hash<Version>{}(p.version) ^
40-
std::hash<VariationIndex>{}(*p.variation);
41-
} else {
42-
return std::hash<Version>{}(p.version);
43-
}
44-
}
45-
};
46-
};
47-
4827
struct TrackEventParams {
4928
Date creation_date;
5029
std::string key;

libs/common/include/events/detail/asio_event_processor.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
#include "context_filter.hpp"
1313
#include "events/detail/conn_pool.hpp"
1414
#include "events/detail/outbox.hpp"
15-
#include "events/detail/summary_state.hpp"
15+
#include "events/detail/summarizer.hpp"
1616
#include "events/event_processor.hpp"
1717
#include "events/events.hpp"
1818
#include "logger.hpp"
@@ -43,7 +43,7 @@ class AsioEventProcessor : public IEventProcessor {
4343

4444
boost::asio::any_io_executor io_;
4545
Outbox outbox_;
46-
SummaryState summary_state_;
46+
Summarizer summarizer_;
4747

4848
std::chrono::milliseconds flush_interval_;
4949
boost::asio::steady_timer timer_;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
#pragma once
2+
#include <boost/container_hash/hash.hpp>
3+
#include <chrono>
4+
#include <functional>
5+
#include <set>
6+
#include <unordered_map>
7+
#include <unordered_set>
8+
#include "events/events.hpp"
9+
#include "value.hpp"
10+
11+
namespace launchdarkly::events::detail {
12+
13+
/**
14+
* Summarizer is responsible for accepting FeatureEventParams (the context
15+
* related to a feature evaluation) and outputting summary events (which
16+
* essentially condenses the various evaluation results into a single
17+
* structure).
18+
*/
19+
class Summarizer {
20+
public:
21+
using Time = std::chrono::system_clock::time_point;
22+
using FlagKey = std::string;
23+
24+
/**
25+
* Construct a Summarizer starting at the given time.
26+
* @param start Start time of the summary.
27+
*/
28+
explicit Summarizer(Time start_time);
29+
30+
/**
31+
* Construct a Summarizer at time zero.
32+
*/
33+
Summarizer() = default;
34+
35+
/**
36+
* Updates the summary with a feature event.
37+
* @param event Feature event.
38+
*/
39+
void Update(client::FeatureEventParams const& event);
40+
41+
/**
42+
* Marks the summary as finished at a given timestamp.
43+
* @param end_time End time of the summary.
44+
*/
45+
Summarizer& Finish(Time end_time);
46+
47+
/**
48+
* Returns true if the summary is empty.
49+
*/
50+
[[nodiscard]] bool Empty() const;
51+
52+
/**
53+
* Returns the summary's start time as given in the constructor.
54+
*/
55+
[[nodiscard]] Time start_time() const;
56+
57+
/**
58+
* Returns the summary's end time as specified using Finish.
59+
*/
60+
[[nodiscard]] Time end_time() const;
61+
62+
struct VariationSummary {
63+
public:
64+
explicit VariationSummary(Value value);
65+
void Increment();
66+
[[nodiscard]] std::int32_t count() const;
67+
[[nodiscard]] Value const& value() const;
68+
69+
private:
70+
std::int32_t count_;
71+
Value value_;
72+
};
73+
74+
struct VariationKey {
75+
std::optional<Version> version;
76+
std::optional<VariationIndex> variation;
77+
78+
VariationKey();
79+
VariationKey(Version version, std::optional<VariationIndex> variation);
80+
81+
bool operator==(VariationKey const& k) const {
82+
return k.variation == variation && k.version == version;
83+
}
84+
85+
bool operator<(VariationKey const& k) const {
86+
if (variation < k.variation) {
87+
return true;
88+
}
89+
return version < k.version;
90+
}
91+
};
92+
93+
struct State {
94+
Value default_;
95+
std::set<std::string> context_kinds;
96+
std::map<Summarizer::VariationKey, Summarizer::VariationSummary>
97+
counters;
98+
99+
explicit State(Value defaultVal);
100+
};
101+
102+
[[nodiscard]] std::unordered_map<FlagKey, State> const& features() const;
103+
104+
private:
105+
Time start_time_;
106+
Summarizer::Time end_time_;
107+
std::unordered_map<FlagKey, State> features_;
108+
};
109+
110+
} // namespace launchdarkly::events::detail

libs/common/include/events/detail/summary_state.hpp

Lines changed: 0 additions & 23 deletions
This file was deleted.

libs/common/include/serialization/events/json_events.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <boost/json.hpp>
44

5+
#include "events/detail/summarizer.hpp"
56
#include "events/events.hpp"
67

78
namespace launchdarkly::events::client {
@@ -37,3 +38,13 @@ void tag_invoke(boost::json::value_from_tag const&,
3738
OutputEvent const& event);
3839

3940
} // namespace launchdarkly::events
41+
42+
namespace launchdarkly::events::detail {
43+
44+
void tag_invoke(boost::json::value_from_tag const&,
45+
boost::json::value& json_value,
46+
Summarizer::State const& state);
47+
void tag_invoke(boost::json::value_from_tag const&,
48+
boost::json::value& json_value,
49+
Summarizer const& summary);
50+
} // namespace launchdarkly::events::detail

libs/common/src/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ add_library(${LIBNAME}
4141
events/asio_event_processor.cpp
4242
events/outbox.cpp
4343
events/conn_pool.cpp
44-
events/summary_state.cpp
44+
events/summarizer.cpp
45+
events/client_events.cpp
4546
config/http_properties.cpp
4647
config/data_source_builder.cpp
4748
config/http_properties_builder.cpp)

libs/common/src/events/asio_event_processor.cpp

Lines changed: 50 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,15 @@ AsioEventProcessor::AsioEventProcessor(
2323
Logger& logger)
2424
: io_(boost::asio::make_strand(io)),
2525
outbox_(config.capacity()),
26-
summary_state_(std::chrono::system_clock::now()),
26+
summarizer_(std::chrono::system_clock::now()),
2727
flush_interval_(config.flush_interval()),
2828
timer_(io_),
2929
host_(endpoints.events_base_url()), // TODO: parse and use host
3030
path_(config.path()),
3131
authorization_(std::move(authorization)),
3232
uuids_(),
33-
conns_(),
3433
inbox_capacity_(config.capacity()),
3534
inbox_size_(0),
36-
inbox_mutex_(),
3735
full_outbox_encountered_(false),
3836
full_inbox_encountered_(false),
3937
filter_(config.all_attributes_private(), config.private_attributes()),
@@ -68,16 +66,14 @@ void AsioEventProcessor::AsyncSend(InputEvent input_event) {
6866
if (!InboxIncrement()) {
6967
return;
7068
}
71-
boost::asio::post(io_, [this, e = std::move(input_event)]() mutable {
69+
boost::asio::post(io_, [this, event = std::move(input_event)]() mutable {
7270
InboxDecrement();
73-
HandleSend(std::move(e));
71+
HandleSend(std::move(event));
7472
});
7573
}
7674

77-
void AsioEventProcessor::HandleSend(InputEvent e) {
78-
summary_state_.update(e);
79-
80-
std::vector<OutputEvent> output_events = Process(std::move(e));
75+
void AsioEventProcessor::HandleSend(InputEvent event) {
76+
std::vector<OutputEvent> output_events = Process(std::move(event));
8177

8278
bool inserted = outbox_.PushDiscardingOverflow(std::move(output_events));
8379
if (!inserted && !full_outbox_encountered_) {
@@ -95,6 +91,7 @@ void AsioEventProcessor::Flush(FlushTrigger flush_type) {
9591
LD_LOG(logger_, LogLevel::kDebug)
9692
<< "event-processor: nothing to flush";
9793
}
94+
summarizer_ = Summarizer(std::chrono::system_clock::now());
9895
if (flush_type == FlushTrigger::Automatic) {
9996
ScheduleFlush();
10097
}
@@ -133,8 +130,15 @@ AsioEventProcessor::MakeRequest() {
133130
return std::nullopt;
134131
}
135132

133+
auto events = boost::json::value_from(outbox_.Consume());
134+
135+
if (!summarizer_.Finish(std::chrono::system_clock::now()).Empty()) {
136+
events.as_array().push_back(boost::json::value_from(summarizer_));
137+
}
138+
136139
LD_LOG(logger_, LogLevel::kDebug)
137140
<< "event-processor: generating http request";
141+
138142
RequestType req;
139143

140144
req.set(http::field::host, host_);
@@ -145,8 +149,7 @@ AsioEventProcessor::MakeRequest() {
145149
req.set(kPayloadIdHeader, boost::lexical_cast<std::string>(uuids_()));
146150
req.target(host_ + path_);
147151

148-
req.body() =
149-
boost::json::serialize(boost::json::value_from(outbox_.Consume()));
152+
req.body() = boost::json::serialize(events);
150153
req.prepare_payload();
151154
return req;
152155
}
@@ -160,53 +163,44 @@ struct overloaded : Ts... {
160163
template <class... Ts>
161164
overloaded(Ts...) -> overloaded<Ts...>;
162165

163-
std::vector<OutputEvent> AsioEventProcessor::Process(InputEvent event) {
166+
std::vector<OutputEvent> AsioEventProcessor::Process(InputEvent input_event) {
164167
std::vector<OutputEvent> out;
165168
std::visit(
166-
overloaded{
167-
[&](client::FeatureEventParams&& e) {
168-
if (!e.eval_result.track_events()) {
169-
return;
170-
}
171-
std::optional<Reason> reason;
172-
173-
// TODO(cwaldren): should also add the reason if the variation
174-
// method was VariationDetail().
175-
if (e.eval_result.track_reason()) {
176-
reason = e.eval_result.detail().reason();
177-
}
178-
179-
client::FeatureEventBase b = {
180-
e.creation_date, std::move(e.key), e.eval_result.version(),
181-
e.eval_result.detail().variation_index(),
182-
e.eval_result.detail().value(), reason,
183-
// TODO(cwaldren): change to actual default; figure out
184-
// where this should be plumbed through.
185-
Value::null()};
186-
187-
auto debug_until_date = e.eval_result.debug_events_until_date();
188-
bool emit_debug_event =
189-
debug_until_date &&
190-
debug_until_date.value() > std::chrono::system_clock::now();
191-
192-
if (emit_debug_event) {
193-
out.emplace_back(
194-
client::DebugEvent{b, filter_.filter(e.context)});
195-
}
196-
// TODO(cwaldren): see about not copying the keys / having the
197-
// getter return a value.
198-
out.emplace_back(client::FeatureEvent{
199-
std::move(b), e.context.kinds_to_keys()});
200-
},
201-
[&](client::IdentifyEventParams&& e) {
202-
// Contexts should already have been checked for
203-
// validity by this point.
204-
assert(e.context.valid());
205-
out.emplace_back(client::IdentifyEvent{
206-
e.creation_date, filter_.filter(e.context)});
207-
},
208-
[&](TrackEventParams&& e) { out.emplace_back(std::move(e)); }},
209-
std::move(event));
169+
overloaded{[&](client::FeatureEventParams&& event) {
170+
summarizer_.Update(event);
171+
172+
if (!event.eval_result.track_events()) {
173+
return;
174+
}
175+
176+
client::FeatureEventBase base{event};
177+
178+
auto debug_until_date =
179+
event.eval_result.debug_events_until_date();
180+
181+
bool emit_debug_event =
182+
debug_until_date &&
183+
debug_until_date.value() >
184+
std::chrono::system_clock::now();
185+
186+
if (emit_debug_event) {
187+
out.emplace_back(client::DebugEvent{
188+
base, filter_.filter(event.context)});
189+
}
190+
out.emplace_back(client::FeatureEvent{
191+
std::move(base), event.context.kinds_to_keys()});
192+
},
193+
[&](client::IdentifyEventParams&& event) {
194+
// Contexts should already have been checked for
195+
// validity by this point.
196+
assert(event.context.valid());
197+
out.emplace_back(client::IdentifyEvent{
198+
event.creation_date, filter_.filter(event.context)});
199+
},
200+
[&](TrackEventParams&& event) {
201+
out.emplace_back(std::move(event));
202+
}},
203+
std::move(input_event));
210204

211205
return out;
212206
}

0 commit comments

Comments
 (0)