Skip to content

Commit 6129741

Browse files
authored
EUDB compliance recommendations and example (#1191)
* EUDB compliance recommendations and example * Fix spelling issue * Addressing code review comments.
1 parent c7bb501 commit 6129741

File tree

5 files changed

+145
-6
lines changed

5 files changed

+145
-6
lines changed

docs/EUDB-compliance.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# EUDB Guidance for 1DS C++ SDK
2+
3+
In order to satisfy the Microsoft commitment to ensuring Privacy and Compliance, specifically
4+
EUDB compliance with respect to EU 'Schrems II' decision, it is imperative for certain
5+
commercial products to perform EUDB URL upload determination during application launch.
6+
7+
1DS Collector service accepts data from all One Observability client SDKs. By default, traffic
8+
simply flows to whichever region can best handle the traffic. This approach works well for
9+
system required metadata. However some client scenarios require that data is sent to a specific
10+
geographic location only.
11+
12+
1DS C++ SDK supports ability to specify / adjust the upload URL at runtime.
13+
14+
Two approaches could be applied to implement EUDB-compliant data upload.
15+
16+
## Option 1: Create two instances of 1DS C++ SDK - one for US collector, another for EU collector
17+
18+
See [Multiple Log Managers Example](https://github.com/microsoft/cpp_client_telemetry/tree/main/examples/cpp/SampleCppLogManagers)
19+
that illustrates how to create multiple instances, each acting as a separate vertical pillar with
20+
their own data collection URL. Two instances `LogManagerUS` and `LogManagerEU` may be configured
21+
each with their own data collection URL, for example:
22+
23+
- For US customers: `https://us-mobile.events.data.microsoft.com/OneCollector/1.0/`
24+
- For EU customers: `https://eu-mobile.events.data.microsoft.com/OneCollector/1.0/`
25+
26+
Depending on data requirements and outcome of dynamic EUDB determination, i.e. organization /
27+
M365 Commercial Tenant is located in EU, the app decides to use `LogManagerEU` instance for
28+
telemetry. Default `LogManager` instance can still be used for region-agnostic "global"
29+
collection of required system diagnostics data. Remember to use the proper compliant instance
30+
depending on event type.
31+
32+
## Option 2: Autodetect the corresponding data collection URL on app start
33+
34+
EventSender example has been modified to illustrate the concept:
35+
36+
- Application starts.
37+
38+
- `LogManager::Initialize(...)` is called with `ILogConfiguration[CFG_STR_COLLECTOR_URL]` set to
39+
empty value `""`. This configuration instructs the SDK to run in offline mode. All data gets
40+
logged to offline storage and not uploaded. This setting has the same effect as running in
41+
paused state. Key difference is that irrespective of upload timer cadence - even for immediate
42+
priority events, 1DS SDK never attempts to trigger the upload. This special configuration option
43+
is safer than simply issuing `PauseTransmission` on app start.
44+
45+
Then application must perform asynchronous EUDB URL detection once in its own asynchronous task /
46+
thread. URL detection process is asynchronous and may take significant amount of time from hundred
47+
milliseconds to seconds. In order to avoid affecting application launch startup performance,
48+
application may perform other startup and logging actions concurrently. All events get logged
49+
in offline cache.
50+
51+
- As part of the configuration update process - application calls `LogManager::PauseTransmission()`
52+
done to ensure exclusive access to uploader configuration.
53+
54+
- Once the EUDB URL is obtained from remote configuration provisioning service (ECS, MSGraph,
55+
OneSettings, etc.), or read cached value from local app configuration storage, the value is supplied
56+
to 1DS SDK:
57+
58+
`ILogConfiguration[CFG_STR_COLLECTOR_URL] = eudb_url`
59+
60+
This assignment of URL is done once during application start. Application does not need to change the
61+
data collection URL after that.
62+
63+
Note that 1DS SDK itself does not provide a feature to store the cached URL value. It is up to the
64+
product owners to decide what caching mechanism they would like to use: registry, ECS cache, Unity
65+
player settings, mobile app settings provider, etc.
66+
67+
- Finally the app code could call `LogManager::ResumeTransmission()` - to apply the new configuration
68+
settings and enable the data upload to compliant destination.

examples/cpp/EventSender/EventSender.cpp

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <iostream>
44
#include <iterator>
55
#include <fstream>
6+
#include <chrono>
67

78
#include "LogManager.hpp"
89

@@ -71,6 +72,40 @@ const char* defaultConfig = static_cast<const char *> JSON_CONFIG
7172
}
7273
);
7374

75+
// Mock function that performs random selection of destination URL. 1DS SDK does not define how the app needs to perform
76+
// the region determination. Products should use MSGraph API, OCPS, or other remote config provisioning sources, such as
77+
// ECS: https://learn.microsoft.com/en-us/deployedge/edge-configuration-and-experiments - in order to identify what 1DS
78+
// collector to use for specific Enterprise or Consumer end-user telemetry uploads. Note that the EUDB URL determination
79+
// is performed asynchronously and could take a few seconds. EUDB URL for Enterprise applications may be cached
80+
// in app-specific configuration storage. 1DS SDK does not provide a feature to cache the data collection URL used for
81+
// a previous session.
82+
//
83+
// Note that this function to determine the URL is called once, early at boot.
84+
std::string GetEudbCollectorUrl()
85+
{
86+
const auto randSeed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
87+
srand(static_cast<unsigned>(randSeed));
88+
return (rand() % 2) ? "https://us-mobile.events.data.microsoft.com/OneCollector/1.0/" : "https://eu-mobile.events.data.microsoft.com/OneCollector/1.0/";
89+
}
90+
91+
void UpdateUploadUrl()
92+
{
93+
printf("Performing collector URL detection...\n");
94+
// Transmissions must be paused prior to adjusting the URL.
95+
LogManager::PauseTransmission();
96+
97+
// Obtain a reference to current configuration.
98+
auto& config = LogManager::GetLogConfiguration();
99+
100+
// Update configuration in-place. This is done once after the regional data collection URL is determined.
101+
config[CFG_STR_COLLECTOR_URL] = GetEudbCollectorUrl();
102+
103+
// Resume transmission once EUDB collector URL detection is obtained. In case if EUDB collector determination fails, only required
104+
// system diagnostics data containing no EUPI MAY be uploaded to global data collection endpoint. It is up to product teams to
105+
// decide what strategy works best for their product.
106+
LogManager::ResumeTransmission();
107+
}
108+
74109
int main(int argc, char *argv[])
75110
{
76111
// 2nd (optional) parameter - path to custom SDK configuration
@@ -87,24 +122,43 @@ int main(int argc, char *argv[])
87122

88123
// LogManager configuration
89124
auto& config = LogManager::GetLogConfiguration();
90-
config = MAT::FromJSON(jsonConfig);
125+
auto customLogConfig = MAT::FromJSON(jsonConfig);
126+
config = customLogConfig; // Assignment operation COLLATES the default + custom config
91127

92128
// LogManager initialization
93129
ILogger *logger = LogManager::Initialize();
94-
bool utcActive = (bool)(config[CFG_STR_UTC][CFG_BOOL_UTC_ACTIVE]);
130+
const bool utcActive = (bool)(config[CFG_STR_UTC][CFG_BOOL_UTC_ACTIVE]);
95131

96132
printf("Running in %s mode...\n", (utcActive) ? "UTC" : "direct upload");
97133
if (utcActive)
98134
{
99-
printf("UTC provider group Id: %s\n", (const char *)(config[CFG_STR_UTC][CFG_STR_PROVIDER_GROUP_ID]));
100-
printf("UTC large payloads: %s\n", ((bool)(config[CFG_STR_UTC][CFG_BOOL_UTC_LARGE_PAYLOADS])) ? "supported" : "not supported");
135+
printf("UTC provider group Id: %s\n", static_cast<const char*>(config[CFG_STR_UTC][CFG_STR_PROVIDER_GROUP_ID]));
136+
printf("UTC large payloads: %s\n", static_cast<bool>(config[CFG_STR_UTC][CFG_BOOL_UTC_LARGE_PAYLOADS]) ? "supported" : "not supported");
101137
}
102138
else
103139
{
104-
printf("Collector URL: %s\n", (const char *)(config[CFG_STR_COLLECTOR_URL]));
140+
// LogManager::ILogConfiguration[CFG_STR_COLLECTOR_URL] defaults to global URL.
141+
//
142+
// If app-provided JSON config is empty on start, means the app intended to asynchronously
143+
// obtain the data collection URL for EUDB compliance. App subsequently sets an empty URL -
144+
// by assigning an empty value to the log manager instance CFG_STR_COLLECTOR_URL. At this
145+
// point the Uploads are not performed until EUDB-compliant endpoint URL is obtained.
146+
//
147+
// Note that since ILogConfiguration configuration tree does not provide a thread-safety
148+
// guarantee between the main thread and SDK uploader thread(s), adjusting the upload
149+
// parameters, e.g. URL or timers, requires the app to pause transmission, adjust params,
150+
// then resume transmission.
151+
//
152+
if (!customLogConfig.HasConfig(CFG_STR_COLLECTOR_URL))
153+
{
154+
// If configuration provided as a parameter does not contain the URL
155+
UpdateUploadUrl();
156+
}
157+
const std::string url = config[CFG_STR_COLLECTOR_URL];
158+
printf("Collector URL: %s\n", url.c_str());
105159
}
106160

107-
printf("Token (iKey): %s\n", (const char *)(config[CFG_STR_PRIMARY_TOKEN]));
161+
printf("Token (iKey): %s\n", static_cast<const char*>(config[CFG_STR_PRIMARY_TOKEN]));
108162

109163
#if 0
110164
// Code example that shows how to convert ILogConfiguration to JSON

lib/api/IRuntimeConfig.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ namespace MAT_NS_BEGIN
3434
/// <returns>A string that contains the collector URI.</returns>
3535
virtual std::string GetCollectorUrl() = 0;
3636

37+
/// <summary>
38+
/// Check used by uploader sequence to verify if URL is defined.
39+
/// </summary>
40+
/// <returns>true if URL is set, false otherwise.</returns>
41+
virtual bool IsCollectorUrlSet() = 0;
42+
3743
/// <summary>
3844
/// Adds extension fields (created by the configuration provider) to an
3945
/// event.

lib/config/RuntimeConfig_Default.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ namespace MAT_NS_BEGIN
104104
return std::string(url);
105105
}
106106

107+
virtual bool IsCollectorUrlSet() override
108+
{
109+
const char* url = config[CFG_STR_COLLECTOR_URL];
110+
return (url != nullptr) && (url[0] != '\0');
111+
}
112+
107113
virtual void DecorateEvent(std::map<std::string, std::string>& extension, std::string const& experimentationProject, std::string const& eventName) override
108114
{
109115
UNREFERENCED_PARAMETER(extension);

lib/tpm/TransmissionPolicyManager.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ namespace MAT_NS_BEGIN {
106106
if (guard.isPaused()) {
107107
return;
108108
}
109+
if (!m_config.IsCollectorUrlSet())
110+
{
111+
LOG_TRACE("Collector URL is not set, no upload.");
112+
return;
113+
}
109114
LOCKGUARD(m_scheduledUploadMutex);
110115
if (delay.count() < 0 || m_timerdelay.count() < 0)
111116
{

0 commit comments

Comments
 (0)