Skip to content

Commit fc4c33c

Browse files
Price Floor Logs Update (prebid#3950)
1 parent 46cdf39 commit fc4c33c

23 files changed

Lines changed: 1514 additions & 417 deletions

src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java

Lines changed: 82 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import org.prebid.server.log.ConditionalLogger;
2222
import org.prebid.server.log.Logger;
2323
import org.prebid.server.log.LoggerFactory;
24+
import org.prebid.server.metric.MetricName;
25+
import org.prebid.server.metric.Metrics;
2426
import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebidFloors;
2527
import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
2628
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
@@ -50,19 +52,35 @@ public class BasicPriceFloorProcessor implements PriceFloorProcessor {
5052
private static final int MODEL_WEIGHT_MAX_VALUE = 100;
5153
private static final int MODEL_WEIGHT_MIN_VALUE = 1;
5254

55+
private static final String FETCH_FAILED_ERROR_MESSAGE = "Price floors processing failed: %s. "
56+
+ "Following parsing of request price floors is failed: %s";
57+
private static final String DYNAMIC_DATA_NOT_ALLOWED_MESSAGE =
58+
"Price floors processing failed: Using dynamic data is not allowed. "
59+
+ "Following parsing of request price floors is failed: %s";
60+
private static final String INVALID_REQUEST_WARNING_MESSAGE =
61+
"Price floors processing failed: parsing of request price floors is failed: %s";
62+
private static final String ERROR_LOG_MESSAGE =
63+
"Price Floors can't be resolved for account %s and request %s, reason: %s";
64+
5365
private final PriceFloorFetcher floorFetcher;
5466
private final PriceFloorResolver floorResolver;
67+
private final Metrics metrics;
5568
private final JacksonMapper mapper;
69+
private final double logSamplingRate;
5670

5771
private final RandomWeightedEntrySupplier<PriceFloorModelGroup> modelPicker;
5872

5973
public BasicPriceFloorProcessor(PriceFloorFetcher floorFetcher,
6074
PriceFloorResolver floorResolver,
61-
JacksonMapper mapper) {
75+
Metrics metrics,
76+
JacksonMapper mapper,
77+
double logSamplingRate) {
6278

6379
this.floorFetcher = Objects.requireNonNull(floorFetcher);
6480
this.floorResolver = Objects.requireNonNull(floorResolver);
81+
this.metrics = Objects.requireNonNull(metrics);
6582
this.mapper = Objects.requireNonNull(mapper);
83+
this.logSamplingRate = logSamplingRate;
6684

6785
modelPicker = new RandomPositiveWeightedEntrySupplier<>(BasicPriceFloorProcessor::resolveModelGroupWeight);
6886
}
@@ -82,7 +100,7 @@ public BidRequest enrichWithPriceFloors(BidRequest bidRequest,
82100
return disableFloorsForRequest(bidRequest);
83101
}
84102

85-
final PriceFloorRules floors = resolveFloors(account, bidRequest, errors);
103+
final PriceFloorRules floors = resolveFloors(account, bidRequest, warnings);
86104
return updateBidRequestWithFloors(bidRequest, bidder, floors, errors, warnings);
87105
}
88106

@@ -122,49 +140,13 @@ private static PriceFloorRules extractRequestFloors(BidRequest bidRequest) {
122140
return ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors);
123141
}
124142

125-
private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, List<String> errors) {
143+
private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, List<String> warnings) {
126144
final PriceFloorRules requestFloors = extractRequestFloors(bidRequest);
127145

128146
final FetchResult fetchResult = floorFetcher.fetch(account);
129-
final FetchStatus fetchStatus = ObjectUtil.getIfNotNull(fetchResult, FetchResult::getFetchStatus);
130-
131-
if (fetchResult != null && fetchStatus == FetchStatus.success && shouldUseDynamicData(account, fetchResult)) {
132-
final PriceFloorRules mergedFloors = mergeFloors(requestFloors, fetchResult.getRulesData());
133-
return createFloorsFrom(mergedFloors, fetchStatus, PriceFloorLocation.fetch);
134-
}
147+
final FetchStatus fetchStatus = fetchResult.getFetchStatus();
135148

136-
if (requestFloors != null) {
137-
try {
138-
final Optional<AccountPriceFloorsConfig> priceFloorsConfig = Optional.of(account)
139-
.map(Account::getAuction)
140-
.map(AccountAuctionConfig::getPriceFloors);
141-
142-
final Long maxRules = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxRules)
143-
.orElse(null);
144-
final Long maxDimensions = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxSchemaDims)
145-
.orElse(null);
146-
147-
PriceFloorRulesValidator.validateRules(
148-
requestFloors,
149-
PriceFloorsConfigResolver.resolveMaxValue(maxRules),
150-
PriceFloorsConfigResolver.resolveMaxValue(maxDimensions));
151-
152-
return createFloorsFrom(requestFloors, fetchStatus, PriceFloorLocation.request);
153-
} catch (PreBidException e) {
154-
errors.add("Failed to parse price floors from request, with a reason: %s".formatted(e.getMessage()));
155-
conditionalLogger.error(
156-
"Failed to parse price floors from request with id: '%s', with a reason: %s"
157-
.formatted(bidRequest.getId(), e.getMessage()),
158-
0.01d);
159-
}
160-
}
161-
162-
return createFloorsFrom(null, fetchStatus, PriceFloorLocation.noData);
163-
}
164-
165-
private static boolean shouldUseDynamicData(Account account, FetchResult fetchResult) {
166-
final boolean isUsingDynamicDataAllowed = Optional.of(account)
167-
.map(Account::getAuction)
149+
final boolean isUsingDynamicDataAllowed = Optional.ofNullable(account.getAuction())
168150
.map(AccountAuctionConfig::getPriceFloors)
169151
.map(AccountPriceFloorsConfig::getUseDynamicData)
170152
.map(BooleanUtils::isNotFalse)
@@ -175,12 +157,68 @@ private static boolean shouldUseDynamicData(Account account, FetchResult fetchRe
175157
.map(rate -> ThreadLocalRandom.current().nextInt(USE_FETCH_DATA_RATE_MAX) < rate)
176158
.orElse(true);
177159

178-
return isUsingDynamicDataAllowed && shouldUseDynamicData;
160+
if (fetchStatus == FetchStatus.success && isUsingDynamicDataAllowed && shouldUseDynamicData) {
161+
final PriceFloorRules mergedFloors = mergeFloors(requestFloors, fetchResult.getRulesData());
162+
return createFloorsFrom(mergedFloors, fetchStatus, PriceFloorLocation.fetch);
163+
}
164+
165+
return requestFloors == null
166+
? createFloorsFrom(null, fetchStatus, PriceFloorLocation.noData)
167+
: getPriceFloorRules(
168+
bidRequest, account, requestFloors, fetchResult, isUsingDynamicDataAllowed, warnings);
169+
}
170+
171+
private PriceFloorRules getPriceFloorRules(BidRequest bidRequest,
172+
Account account,
173+
PriceFloorRules requestFloors,
174+
FetchResult fetchResult,
175+
boolean isDynamicDataAllowed,
176+
List<String> warnings) {
177+
178+
try {
179+
final Optional<AccountPriceFloorsConfig> priceFloorsConfig = Optional.of(account.getAuction())
180+
.map(AccountAuctionConfig::getPriceFloors);
181+
182+
final Long maxRules = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxRules)
183+
.orElse(null);
184+
final Long maxDimensions = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxSchemaDims)
185+
.orElse(null);
186+
187+
PriceFloorRulesValidator.validateRules(
188+
requestFloors,
189+
PriceFloorsConfigResolver.resolveMaxValue(maxRules),
190+
PriceFloorsConfigResolver.resolveMaxValue(maxDimensions));
191+
192+
return createFloorsFrom(requestFloors, fetchResult.getFetchStatus(), PriceFloorLocation.request);
193+
} catch (PreBidException e) {
194+
logErrorMessage(fetchResult, isDynamicDataAllowed, e, account.getId(), bidRequest.getId(), warnings);
195+
return createFloorsFrom(null, fetchResult.getFetchStatus(), PriceFloorLocation.noData);
196+
}
179197
}
180198

181-
private PriceFloorRules mergeFloors(PriceFloorRules requestFloors,
182-
PriceFloorData providerRulesData) {
199+
private void logErrorMessage(FetchResult fetchResult,
200+
boolean isDynamicDataAllowed,
201+
PreBidException requestFloorsValidationException,
202+
String accountId,
203+
String requestId,
204+
List<String> warnings) {
205+
206+
final String validationMessage = requestFloorsValidationException.getMessage();
207+
final String errorMessage = switch (fetchResult.getFetchStatus()) {
208+
case inprogress -> null;
209+
case error, timeout, none -> FETCH_FAILED_ERROR_MESSAGE.formatted(
210+
fetchResult.getErrorMessage(), validationMessage);
211+
case success -> isDynamicDataAllowed ? null : DYNAMIC_DATA_NOT_ALLOWED_MESSAGE.formatted(validationMessage);
212+
};
213+
214+
if (errorMessage != null) {
215+
warnings.add(INVALID_REQUEST_WARNING_MESSAGE.formatted(validationMessage));
216+
conditionalLogger.error(ERROR_LOG_MESSAGE.formatted(accountId, requestId, errorMessage), logSamplingRate);
217+
metrics.updateAlertsMetrics(MetricName.general);
218+
}
219+
}
183220

221+
private PriceFloorRules mergeFloors(PriceFloorRules requestFloors, PriceFloorData providerRulesData) {
184222
final Price floorMinPrice = resolveFloorMinPrice(requestFloors);
185223

186224
return (requestFloors != null ? requestFloors.toBuilder() : PriceFloorRules.builder())

src/main/java/org/prebid/server/floors/PriceFloorFetcher.java

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ public FetchResult fetch(Account account) {
9090
final AccountFetchContext accountFetchContext = fetchedData.get(account.getId());
9191

9292
return accountFetchContext != null
93-
? FetchResult.of(accountFetchContext.getRulesData(), accountFetchContext.getFetchStatus())
93+
? FetchResult.of(
94+
accountFetchContext.getRulesData(),
95+
accountFetchContext.getFetchStatus(),
96+
accountFetchContext.getErrorMessage())
9497
: fetchPriceFloorData(account);
9598
}
9699

@@ -99,20 +102,20 @@ private FetchResult fetchPriceFloorData(Account account) {
99102
final Boolean fetchEnabled = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getEnabled);
100103

101104
if (BooleanUtils.isFalse(fetchEnabled)) {
102-
return FetchResult.of(null, FetchStatus.none);
105+
return FetchResult.none("Fetching is disabled");
103106
}
104107

105108
final String accountId = account.getId();
106109
final String fetchUrl = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getUrl);
107110
if (!isUrlValid(fetchUrl)) {
108-
logger.error("Malformed fetch.url: '%s', passed for account %s".formatted(fetchUrl, accountId));
109-
return FetchResult.of(null, FetchStatus.error);
111+
logger.error("Malformed fetch.url: '%s' passed for account %s".formatted(fetchUrl, accountId));
112+
return FetchResult.error("Malformed fetch.url '%s' passed".formatted(fetchUrl));
110113
}
111114
if (!fetchInProgress.contains(accountId)) {
112115
fetchPriceFloorDataAsynchronous(fetchConfig, accountId);
113116
}
114117

115-
return FetchResult.of(null, FetchStatus.inprogress);
118+
return FetchResult.inProgress();
116119
}
117120

118121
private boolean isUrlValid(String url) {
@@ -148,7 +151,7 @@ private void fetchPriceFloorDataAsynchronous(AccountPriceFloorsFetchConfig fetch
148151

149152
fetchInProgress.add(accountId);
150153
httpClient.get(fetchUrl, timeout, resolveMaxFileSize(maxFetchFileSizeKb))
151-
.map(httpClientResponse -> parseFloorResponse(httpClientResponse, fetchConfig, accountId))
154+
.map(httpClientResponse -> parseFloorResponse(httpClientResponse, fetchConfig))
152155
.recover(throwable -> recoverFromFailedFetching(throwable, fetchUrl, accountId))
153156
.map(cacheInfo -> updateCache(cacheInfo, fetchConfig, accountId))
154157
.map(priceFloorData -> createPeriodicTimerForRulesFetch(priceFloorData, fetchConfig, accountId));
@@ -159,40 +162,38 @@ private static long resolveMaxFileSize(Long maxSizeInKBytes) {
159162
}
160163

161164
private ResponseCacheInfo parseFloorResponse(HttpClientResponse httpClientResponse,
162-
AccountPriceFloorsFetchConfig fetchConfig,
163-
String accountId) {
165+
AccountPriceFloorsFetchConfig fetchConfig) {
164166

165167
final int statusCode = httpClientResponse.getStatusCode();
166168
if (statusCode != HttpStatus.SC_OK) {
167-
throw new PreBidException("Failed to request for account %s, provider respond with status %s"
168-
.formatted(accountId, statusCode));
169+
throw new PreBidException("Failed to request, provider respond with status %s".formatted(statusCode));
169170
}
170171
final String body = httpClientResponse.getBody();
171172

172173
if (StringUtils.isBlank(body)) {
173-
throw new PreBidException(
174-
"Failed to parse price floor response for account %s, response body can not be empty"
175-
.formatted(accountId));
174+
throw new PreBidException("Failed to parse price floor response, response body can not be empty");
176175
}
177176

178-
final PriceFloorData priceFloorData = parsePriceFloorData(body, accountId);
177+
final PriceFloorData priceFloorData = parsePriceFloorData(body);
178+
179179
PriceFloorRulesValidator.validateRulesData(
180180
priceFloorData,
181181
PriceFloorsConfigResolver.resolveMaxValue(fetchConfig.getMaxRules()),
182182
PriceFloorsConfigResolver.resolveMaxValue(fetchConfig.getMaxSchemaDims()));
183183

184184
return ResponseCacheInfo.of(priceFloorData,
185185
FetchStatus.success,
186+
null,
186187
cacheTtlFromResponse(httpClientResponse, fetchConfig.getUrl()));
187188
}
188189

189-
private PriceFloorData parsePriceFloorData(String body, String accountId) {
190+
private PriceFloorData parsePriceFloorData(String body) {
190191
final PriceFloorData priceFloorData;
191192
try {
192193
priceFloorData = mapper.decodeValue(body, PriceFloorData.class);
193194
} catch (DecodeException e) {
194-
throw new PreBidException("Failed to parse price floor response for account %s, cause: %s"
195-
.formatted(accountId, ExceptionUtils.getMessage(e)));
195+
throw new PreBidException(
196+
"Failed to parse price floor response, cause: %s".formatted(ExceptionUtils.getMessage(e)));
196197
}
197198
return priceFloorData;
198199
}
@@ -220,8 +221,11 @@ private PriceFloorData updateCache(ResponseCacheInfo cacheInfo,
220221
String accountId) {
221222

222223
final long maxAgeTimerId = createMaxAgeTimer(accountId, resolveCacheTtl(cacheInfo, fetchConfig));
223-
final AccountFetchContext fetchContext =
224-
AccountFetchContext.of(cacheInfo.getRulesData(), cacheInfo.getFetchStatus(), maxAgeTimerId);
224+
final AccountFetchContext fetchContext = AccountFetchContext.of(
225+
cacheInfo.getRulesData(),
226+
cacheInfo.getFetchStatus(),
227+
cacheInfo.getErrorMessage(),
228+
maxAgeTimerId);
225229

226230
if (cacheInfo.getFetchStatus() == FetchStatus.success || !fetchedData.containsKey(accountId)) {
227231
fetchedData.put(accountId, fetchContext);
@@ -274,23 +278,24 @@ private Future<ResponseCacheInfo> recoverFromFailedFetching(Throwable throwable,
274278
metrics.updatePriceFloorFetchMetric(MetricName.failure);
275279

276280
final FetchStatus fetchStatus;
281+
final String errorMessage;
277282
if (throwable instanceof TimeoutException || throwable instanceof ConnectTimeoutException) {
278283
fetchStatus = FetchStatus.timeout;
279-
logger.error("Fetch price floor request timeout for fetch.url: '%s', account %s exceeded."
280-
.formatted(fetchUrl, accountId));
284+
errorMessage = "Fetch price floor request timeout for fetch.url '%s' exceeded.".formatted(fetchUrl);
281285
} else {
282286
fetchStatus = FetchStatus.error;
283-
logger.error(
284-
"Failed to fetch price floor from provider for fetch.url: '%s', account = %s with a reason : %s "
285-
.formatted(fetchUrl, accountId, throwable.getMessage()));
287+
errorMessage = "Failed to fetch price floor from provider for fetch.url '%s', with a reason: %s"
288+
.formatted(fetchUrl, throwable.getMessage());
286289
}
287290

288-
return Future.succeededFuture(ResponseCacheInfo.withStatus(fetchStatus));
291+
logger.error("Price floor fetching failed for account %s: %s".formatted(accountId, errorMessage));
292+
return Future.succeededFuture(ResponseCacheInfo.withError(fetchStatus, errorMessage));
289293
}
290294

291295
private PriceFloorData createPeriodicTimerForRulesFetch(PriceFloorData priceFloorData,
292296
AccountPriceFloorsFetchConfig fetchConfig,
293297
String accountId) {
298+
294299
final long accountPeriodicTimeSec =
295300
ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getPeriodSec);
296301
final long periodicTimeSec =
@@ -318,6 +323,8 @@ private static class AccountFetchContext {
318323

319324
FetchStatus fetchStatus;
320325

326+
String errorMessage;
327+
321328
Long maxAgeTimerId;
322329
}
323330

@@ -328,10 +335,12 @@ private static class ResponseCacheInfo {
328335

329336
FetchStatus fetchStatus;
330337

338+
String errorMessage;
339+
331340
Long cacheTtl;
332341

333-
public static ResponseCacheInfo withStatus(FetchStatus status) {
334-
return ResponseCacheInfo.of(null, status, null);
342+
public static ResponseCacheInfo withError(FetchStatus status, String errorMessage) {
343+
return ResponseCacheInfo.of(null, status, errorMessage, null);
335344
}
336345
}
337346
}

src/main/java/org/prebid/server/floors/proto/FetchResult.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,18 @@ public class FetchResult {
99
PriceFloorData rulesData;
1010

1111
FetchStatus fetchStatus;
12+
13+
String errorMessage;
14+
15+
public static FetchResult none(String errorMessage) {
16+
return FetchResult.of(null, FetchStatus.none, errorMessage);
17+
}
18+
19+
public static FetchResult error(String errorMessage) {
20+
return FetchResult.of(null, FetchStatus.error, errorMessage);
21+
}
22+
23+
public static FetchResult inProgress() {
24+
return FetchResult.of(null, FetchStatus.inprogress, null);
25+
}
1226
}

src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.prebid.server.metric.Metrics;
2121
import org.prebid.server.settings.ApplicationSettings;
2222
import org.prebid.server.vertx.httpclient.HttpClient;
23+
import org.springframework.beans.factory.annotation.Value;
2324
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
2425
import org.springframework.boot.context.properties.ConfigurationProperties;
2526
import org.springframework.context.annotation.Bean;
@@ -84,9 +85,11 @@ PriceFloorResolver noOpPriceFloorResolver() {
8485
@ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true")
8586
PriceFloorProcessor basicPriceFloorProcessor(PriceFloorFetcher floorFetcher,
8687
PriceFloorResolver floorResolver,
87-
JacksonMapper mapper) {
88+
Metrics metrics,
89+
JacksonMapper mapper,
90+
@Value("${logging.sampling-rate:0.01}") double logSamplingRate) {
8891

89-
return new BasicPriceFloorProcessor(floorFetcher, floorResolver, mapper);
92+
return new BasicPriceFloorProcessor(floorFetcher, floorResolver, metrics, mapper, logSamplingRate);
9093
}
9194

9295
@Bean

0 commit comments

Comments
 (0)