MarginaliaSearch/code/services-application/api-service/java/nu/marginalia/api/ApiService.java

153 lines
5.0 KiB
Java
Raw Normal View History

2023-03-04 12:19:01 +00:00
package nu.marginalia.api;
2022-05-19 15:45:26 +00:00
import com.google.gson.Gson;
import com.google.inject.Inject;
import io.prometheus.client.Counter;
import io.prometheus.client.Histogram;
2023-03-04 12:19:01 +00:00
import nu.marginalia.api.model.ApiLicense;
import nu.marginalia.api.model.ApiSearchResults;
import nu.marginalia.api.svc.LicenseService;
import nu.marginalia.api.svc.RateLimiterService;
import nu.marginalia.api.svc.ResponseCache;
2023-03-04 12:19:01 +00:00
import nu.marginalia.model.gson.GsonFactory;
import nu.marginalia.service.server.BaseServiceParams;
import nu.marginalia.service.server.SparkService;
import nu.marginalia.service.server.mq.MqRequest;
2022-05-19 15:45:26 +00:00
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
2023-03-22 14:11:22 +00:00
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
2022-05-19 15:45:26 +00:00
import spark.Request;
import spark.Response;
import spark.Spark;
public class ApiService extends SparkService {
2022-05-19 15:45:26 +00:00
private final Logger logger = LoggerFactory.getLogger(getClass());
2022-09-10 18:28:45 +00:00
private final Gson gson = GsonFactory.get();
private final ResponseCache responseCache;
private final LicenseService licenseService;
private final RateLimiterService rateLimiterService;
private final ApiSearchOperator searchOperator;
2022-05-19 15:45:26 +00:00
2023-03-22 14:11:22 +00:00
// Marker for filtering out sensitive content from the persistent logs
private final Marker queryMarker = MarkerFactory.getMarker("QUERY");
private final Counter wmsa_api_timeout_count = Counter.build()
.name("wmsa_api_timeout_count")
.labelNames("key")
.help("API timeout count")
.register();
private final Counter wmsa_api_cache_hit_count = Counter.build()
.name("wmsa_api_cache_hit_count")
.labelNames("key")
.help("API cache hit count")
.register();
private static final Histogram wmsa_api_query_time = Histogram.build()
.name("wmsa_api_query_time")
.labelNames("key")
2024-01-02 16:13:14 +00:00
.linearBuckets(0.05, 0.05, 15)
.help("API-side query time")
.register();
2022-05-19 15:45:26 +00:00
@Inject
public ApiService(BaseServiceParams params,
ResponseCache responseCache,
LicenseService licenseService,
RateLimiterService rateLimiterService,
ApiSearchOperator searchOperator)
throws Exception
{
super(params);
this.responseCache = responseCache;
this.licenseService = licenseService;
this.rateLimiterService = rateLimiterService;
this.searchOperator = searchOperator;
2022-05-19 15:45:26 +00:00
Spark.get("/api/", (rq, rsp) -> {
rsp.redirect("https://about.marginalia-search.com/article/api/");
2022-05-19 15:45:26 +00:00
return "";
});
Spark.get("/api/:key", (rq, rsp) -> licenseService.getLicense(rq.params("key")), gson::toJson);
Spark.get("/api/:key/", (rq, rsp) -> licenseService.getLicense(rq.params("key")), gson::toJson);
Spark.get("/api/:key/search/*", this::search, gson::toJson);
2022-05-19 15:45:26 +00:00
}
@MqRequest(endpoint = "FLUSH_CACHES")
public void flushCaches(String unusedArg) {
logger.info("Flushing caches");
responseCache.flush();
licenseService.flushCache();
}
2022-05-19 15:45:26 +00:00
private Object search(Request request, Response response) {
String[] args = request.splat();
if (args.length != 1) {
Spark.halt(400, "Bad request");
2022-05-19 15:45:26 +00:00
}
var license = licenseService.getLicense(request.params("key"));
2022-05-19 15:45:26 +00:00
response.type("application/json");
2024-02-14 11:09:16 +00:00
// Check if we have a cached response
var cachedResponse = responseCache.getResults(license, args[0], request.queryString());
if (cachedResponse.isPresent()) {
wmsa_api_cache_hit_count.labels(license.key).inc();
return cachedResponse.get();
2022-05-19 15:45:26 +00:00
}
2024-02-14 11:09:16 +00:00
// When no cached response, do the search and cache the result
var result = doSearch(license, args[0], request);
responseCache.putResults(license, args[0], request.queryString(), result);
return result;
2022-05-19 15:45:26 +00:00
}
private ApiSearchResults doSearch(ApiLicense license, String query, Request request) {
if (!rateLimiterService.isAllowed(license)) {
wmsa_api_timeout_count.labels(license.key).inc();
Spark.halt(503, "Slow down");
2022-05-19 15:45:26 +00:00
}
int count = intParam(request, "count", 20);
int index = intParam(request, "index", 3);
2022-05-19 15:45:26 +00:00
logger.info(queryMarker, "{} Search {}", license.key, query);
2022-05-19 15:45:26 +00:00
return wmsa_api_query_time
.labels(license.key)
.time(() ->
searchOperator
.query(query, count, index)
.withLicense(license.getLicense())
);
}
2022-05-19 15:45:26 +00:00
private int intParam(Request request, String name, int defaultValue) {
var value = request.queryParams(name);
2022-05-19 15:45:26 +00:00
if (value == null) {
return defaultValue;
2022-05-19 15:45:26 +00:00
}
try {
return Integer.parseInt(value);
2022-05-19 15:45:26 +00:00
}
catch (NumberFormatException ex) {
Spark.halt(400, "Invalid parameter value for " + name);
2022-05-19 15:45:26 +00:00
return defaultValue;
}
2022-05-19 15:45:26 +00:00
}
2022-05-19 15:45:26 +00:00
}