diff --git a/src/main/java/com/epam/aidial/core/controller/ControllerSelector.java b/src/main/java/com/epam/aidial/core/controller/ControllerSelector.java index bb6b13c7..945c9ef5 100644 --- a/src/main/java/com/epam/aidial/core/controller/ControllerSelector.java +++ b/src/main/java/com/epam/aidial/core/controller/ControllerSelector.java @@ -4,6 +4,7 @@ import com.epam.aidial.core.ProxyContext; import com.epam.aidial.core.config.Deployment; import com.epam.aidial.core.config.Features; +import com.epam.aidial.core.util.SpanUtil; import com.epam.aidial.core.util.UrlUtil; import io.vertx.core.http.HttpMethod; import lombok.experimental.UtilityClass; @@ -16,47 +17,47 @@ @UtilityClass public class ControllerSelector { - private static final Pattern PATTERN_POST_DEPLOYMENT = Pattern.compile("^/+openai/deployments/(.+?)/(completions|chat/completions|embeddings)$"); - private static final Pattern PATTERN_DEPLOYMENT = Pattern.compile("^/+openai/deployments/(.+?)$"); + private static final Pattern PATTERN_POST_DEPLOYMENT = Pattern.compile("^/+openai/deployments/(?.+?)/(completions|chat/completions|embeddings)$"); + private static final Pattern PATTERN_DEPLOYMENT = Pattern.compile("^/+openai/deployments/(?.+?)$"); private static final Pattern PATTERN_DEPLOYMENTS = Pattern.compile("^/+openai/deployments$"); - private static final Pattern PATTERN_MODEL = Pattern.compile("^/+openai/models/(.+?)$"); + private static final Pattern PATTERN_MODEL = Pattern.compile("^/+openai/models/(?.+?)$"); private static final Pattern PATTERN_MODELS = Pattern.compile("^/+openai/models$"); - private static final Pattern PATTERN_ADDON = Pattern.compile("^/+openai/addons/(.+?)$"); + private static final Pattern PATTERN_ADDON = Pattern.compile("^/+openai/addons/(?.+?)$"); private static final Pattern PATTERN_ADDONS = Pattern.compile("^/+openai/addons$"); - private static final Pattern PATTERN_ASSISTANT = Pattern.compile("^/+openai/assistants/(.+?)$"); + private static final Pattern PATTERN_ASSISTANT = Pattern.compile("^/+openai/assistants/(?.+?)$"); private static final Pattern PATTERN_ASSISTANTS = Pattern.compile("^/+openai/assistants$"); - private static final Pattern PATTERN_APPLICATION = Pattern.compile("^/+openai/applications/(.+?)$"); + private static final Pattern PATTERN_APPLICATION = Pattern.compile("^/+openai/applications/(?.+?)$"); private static final Pattern PATTERN_APPLICATIONS = Pattern.compile("^/+openai/applications$"); private static final Pattern PATTERN_BUCKET = Pattern.compile("^/v1/bucket$"); - private static final Pattern PATTERN_FILES = Pattern.compile("^/v1/files/[a-zA-Z0-9]+/.*"); - private static final Pattern PATTERN_FILES_METADATA = Pattern.compile("^/v1/metadata/files/[a-zA-Z0-9]+/.*"); + private static final Pattern PATTERN_FILES = Pattern.compile("^/v1/files/(?[a-zA-Z0-9]+)/(?.*)"); + private static final Pattern PATTERN_FILES_METADATA = Pattern.compile("^/v1/metadata/files/(?[a-zA-Z0-9]+)/(?.*)"); - private static final Pattern PATTERN_RESOURCE = Pattern.compile("^/v1/(conversations|prompts|applications)/[a-zA-Z0-9]+/.*"); - private static final Pattern PATTERN_RESOURCE_METADATA = Pattern.compile("^/v1/metadata/(conversations|prompts|applications)/[a-zA-Z0-9]+/.*"); + private static final Pattern PATTERN_RESOURCE = Pattern.compile("^/v1/(conversations|prompts|applications)/(?[a-zA-Z0-9]+)/(?.*)"); + private static final Pattern PATTERN_RESOURCE_METADATA = Pattern.compile("^/v1/metadata/(conversations|prompts|applications)/(?[a-zA-Z0-9]+)/(?.*)"); // deployment feature patterns - private static final Pattern PATTERN_RATE_RESPONSE = Pattern.compile("^/+v1/(.+?)/rate$"); - private static final Pattern PATTERN_TOKENIZE = Pattern.compile("^/+v1/deployments/(.+?)/tokenize$"); - private static final Pattern PATTERN_TRUNCATE_PROMPT = Pattern.compile("^/+v1/deployments/(.+?)/truncate_prompt$"); - private static final Pattern PATTERN_CONFIGURATION = Pattern.compile("^/+v1/deployments/(.+?)/configuration$"); + private static final Pattern PATTERN_RATE_RESPONSE = Pattern.compile("^/+v1/(?.+?)/rate$"); + private static final Pattern PATTERN_TOKENIZE = Pattern.compile("^/+v1/deployments/(?.+?)/tokenize$"); + private static final Pattern PATTERN_TRUNCATE_PROMPT = Pattern.compile("^/+v1/deployments/(?.+?)/truncate_prompt$"); + private static final Pattern PATTERN_CONFIGURATION = Pattern.compile("^/+v1/deployments/(?.+?)/configuration$"); private static final Pattern SHARE_RESOURCE_OPERATIONS = Pattern.compile("^/v1/ops/resource/share/(create|list|discard|revoke|copy)$"); private static final Pattern INVITATIONS = Pattern.compile("^/v1/invitations$"); - private static final Pattern INVITATION = Pattern.compile("^/v1/invitations/([a-zA-Z0-9]+)$"); + private static final Pattern INVITATION = Pattern.compile("^/v1/invitations/(?[a-zA-Z0-9]+)$"); private static final Pattern PUBLICATIONS = Pattern.compile("^/v1/ops/publication/(list|get|create|delete|approve|reject)$"); private static final Pattern PUBLISHED_RESOURCES = Pattern.compile("^/v1/ops/publication/resource/list$"); private static final Pattern PUBLICATION_RULES = Pattern.compile("^/v1/ops/publication/rule/list$"); private static final Pattern RESOURCE_OPERATIONS = Pattern.compile("^/v1/ops/resource/(move|subscribe)$"); - private static final Pattern DEPLOYMENT_LIMITS = Pattern.compile("^/v1/deployments/(.+?)/limits$"); + private static final Pattern DEPLOYMENT_LIMITS = Pattern.compile("^/v1/deployments/(?.+?)/limits$"); private static final Pattern NOTIFICATIONS = Pattern.compile("^/v1/ops/notification/(list|delete)$"); @@ -81,122 +82,122 @@ public Controller select(Proxy proxy, ProxyContext context) { private static Controller selectGet(Proxy proxy, ProxyContext context, String path) { Matcher match; - match = match(PATTERN_DEPLOYMENT, path); + match = match(PATTERN_DEPLOYMENT, path, context); if (match != null) { DeploymentController controller = new DeploymentController(context); String deploymentId = UrlUtil.decodePath(match.group(1)); return () -> controller.getDeployment(deploymentId); } - match = match(PATTERN_DEPLOYMENTS, path); + match = match(PATTERN_DEPLOYMENTS, path, context); if (match != null) { DeploymentController controller = new DeploymentController(context); return controller::getDeployments; } - match = match(PATTERN_MODEL, path); + match = match(PATTERN_MODEL, path, context); if (match != null) { ModelController controller = new ModelController(context); String modelId = UrlUtil.decodePath(match.group(1)); return () -> controller.getModel(modelId); } - match = match(PATTERN_MODELS, path); + match = match(PATTERN_MODELS, path, context); if (match != null) { ModelController controller = new ModelController(context); return controller::getModels; } - match = match(PATTERN_ADDON, path); + match = match(PATTERN_ADDON, path, context); if (match != null) { AddonController controller = new AddonController(context); String addonId = UrlUtil.decodePath(match.group(1)); return () -> controller.getAddon(addonId); } - match = match(PATTERN_ADDONS, path); + match = match(PATTERN_ADDONS, path, context); if (match != null) { AddonController controller = new AddonController(context); return controller::getAddons; } - match = match(PATTERN_ASSISTANT, path); + match = match(PATTERN_ASSISTANT, path, context); if (match != null) { AssistantController controller = new AssistantController(context); String assistantId = UrlUtil.decodePath(match.group(1)); return () -> controller.getAssistant(assistantId); } - match = match(PATTERN_ASSISTANTS, path); + match = match(PATTERN_ASSISTANTS, path, context); if (match != null) { AssistantController controller = new AssistantController(context); return controller::getAssistants; } - match = match(PATTERN_APPLICATION, path); + match = match(PATTERN_APPLICATION, path, context); if (match != null) { ApplicationController controller = new ApplicationController(context, proxy); String application = UrlUtil.decodePath(match.group(1)); return () -> controller.getApplication(application); } - match = match(PATTERN_APPLICATIONS, path); + match = match(PATTERN_APPLICATIONS, path, context); if (match != null) { ApplicationController controller = new ApplicationController(context, proxy); return controller::getApplicationService; } - match = match(PATTERN_FILES_METADATA, path); + match = match(PATTERN_FILES_METADATA, path, context); if (match != null) { FileMetadataController controller = new FileMetadataController(proxy, context); return () -> controller.handle(resourcePath(path)); } - match = match(PATTERN_FILES, path); + match = match(PATTERN_FILES, path, context); if (match != null) { DownloadFileController controller = new DownloadFileController(proxy, context); return () -> controller.handle(resourcePath(path)); } - match = match(PATTERN_RESOURCE, path); + match = match(PATTERN_RESOURCE, path, context); if (match != null) { ResourceController controller = new ResourceController(proxy, context, false); return () -> controller.handle(resourcePath(path)); } - match = match(PATTERN_RESOURCE_METADATA, path); + match = match(PATTERN_RESOURCE_METADATA, path, context); if (match != null) { ResourceController controller = new ResourceController(proxy, context, true); return () -> controller.handle(resourcePath(path)); } - match = match(PATTERN_BUCKET, path); + match = match(PATTERN_BUCKET, path, context); if (match != null) { BucketController controller = new BucketController(proxy, context); return controller::getBucket; } - match = match(INVITATION, path); + match = match(INVITATION, path, context); if (match != null) { String invitationId = UrlUtil.decodePath(match.group(1)); InvitationController controller = new InvitationController(proxy, context); return () -> controller.getOrAcceptInvitation(invitationId); } - match = match(INVITATIONS, path); + match = match(INVITATIONS, path, context); if (match != null) { InvitationController controller = new InvitationController(proxy, context); return controller::getInvitations; } - match = match(DEPLOYMENT_LIMITS, path); + match = match(DEPLOYMENT_LIMITS, path, context); if (match != null) { String deploymentId = UrlUtil.decodePath(match.group(1)); LimitController controller = new LimitController(proxy, context); return () -> controller.getLimits(deploymentId); } - match = match(PATTERN_CONFIGURATION, path); + match = match(PATTERN_CONFIGURATION, path, context); if (match != null) { String deploymentId = UrlUtil.decodePath(match.group(1)); Function getter = (model) -> Optional.ofNullable(model) @@ -212,7 +213,7 @@ private static Controller selectGet(Proxy proxy, ProxyContext context, String pa } private static Controller selectPost(Proxy proxy, ProxyContext context, String path) { - Matcher match = match(PATTERN_POST_DEPLOYMENT, path); + Matcher match = match(PATTERN_POST_DEPLOYMENT, path, context); if (match != null) { String deploymentId = UrlUtil.decodePath(match.group(1)); String deploymentApi = UrlUtil.decodePath(match.group(2)); @@ -220,7 +221,7 @@ private static Controller selectPost(Proxy proxy, ProxyContext context, String p return () -> controller.handle(deploymentId, deploymentApi); } - match = match(PATTERN_RATE_RESPONSE, path); + match = match(PATTERN_RATE_RESPONSE, path, context); if (match != null) { String deploymentId = UrlUtil.decodePath(match.group(1)); @@ -233,7 +234,7 @@ private static Controller selectPost(Proxy proxy, ProxyContext context, String p return () -> controller.handle(deploymentId, getter, false); } - match = match(PATTERN_TOKENIZE, path); + match = match(PATTERN_TOKENIZE, path, context); if (match != null) { String deploymentId = UrlUtil.decodePath(match.group(1)); @@ -246,7 +247,7 @@ private static Controller selectPost(Proxy proxy, ProxyContext context, String p return () -> controller.handle(deploymentId, getter, true); } - match = match(PATTERN_TRUNCATE_PROMPT, path); + match = match(PATTERN_TRUNCATE_PROMPT, path, context); if (match != null) { String deploymentId = UrlUtil.decodePath(match.group(1)); @@ -259,7 +260,7 @@ private static Controller selectPost(Proxy proxy, ProxyContext context, String p return () -> controller.handle(deploymentId, getter, true); } - match = match(SHARE_RESOURCE_OPERATIONS, path); + match = match(SHARE_RESOURCE_OPERATIONS, path, context); if (match != null) { String operation = match.group(1); ShareController.Operation op = ShareController.Operation.valueOf(operation.toUpperCase()); @@ -268,7 +269,7 @@ private static Controller selectPost(Proxy proxy, ProxyContext context, String p return () -> controller.handle(op); } - match = match(PUBLICATIONS, path); + match = match(PUBLICATIONS, path, context); if (match != null) { String operation = match.group(1); PublicationController controller = new PublicationController(proxy, context); @@ -284,31 +285,31 @@ private static Controller selectPost(Proxy proxy, ProxyContext context, String p }; } - match = match(PUBLICATION_RULES, path); + match = match(PUBLICATION_RULES, path, context); if (match != null) { PublicationController controller = new PublicationController(proxy, context); return controller::listRules; } - match = match(RESOURCE_OPERATIONS, path); + match = match(RESOURCE_OPERATIONS, path, context); if (match != null) { String operation = match.group(1); ResourceOperationController controller = new ResourceOperationController(proxy, context); - return switch (operation) { + return switch (operation) { case "move" -> controller::move; case "subscribe" -> controller::subscribe; default -> null; }; } - match = match(PUBLISHED_RESOURCES, path); + match = match(PUBLISHED_RESOURCES, path, context); if (match != null) { PublicationController controller = new PublicationController(proxy, context); return controller::listPublishedResources; } - match = match(NOTIFICATIONS, path); + match = match(NOTIFICATIONS, path, context); if (match != null) { String operation = match.group(1); NotificationController controller = new NotificationController(proxy, context); @@ -324,19 +325,19 @@ private static Controller selectPost(Proxy proxy, ProxyContext context, String p } private static Controller selectDelete(Proxy proxy, ProxyContext context, String path) { - Matcher match = match(PATTERN_FILES, path); + Matcher match = match(PATTERN_FILES, path, context); if (match != null) { DeleteFileController controller = new DeleteFileController(proxy, context); return () -> controller.handle(resourcePath(path)); } - match = match(PATTERN_RESOURCE, path); + match = match(PATTERN_RESOURCE, path, context); if (match != null) { ResourceController controller = new ResourceController(proxy, context, false); return () -> controller.handle(resourcePath(path)); } - match = match(INVITATION, path); + match = match(INVITATION, path, context); if (match != null) { String invitationId = UrlUtil.decodePath(match.group(1)); InvitationController controller = new InvitationController(proxy, context); @@ -347,13 +348,13 @@ private static Controller selectDelete(Proxy proxy, ProxyContext context, String } private static Controller selectPut(Proxy proxy, ProxyContext context, String path) { - Matcher match = match(PATTERN_FILES, path); + Matcher match = match(PATTERN_FILES, path, context); if (match != null) { UploadFileController controller = new UploadFileController(proxy, context); return () -> controller.handle(resourcePath(path)); } - match = match(PATTERN_RESOURCE, path); + match = match(PATTERN_RESOURCE, path, context); if (match != null) { ResourceController controller = new ResourceController(proxy, context, false); return () -> controller.handle(resourcePath(path)); @@ -362,9 +363,13 @@ private static Controller selectPut(Proxy proxy, ProxyContext context, String pa return null; } - private Matcher match(Pattern pattern, String path) { + private Matcher match(Pattern pattern, String path, ProxyContext context) { Matcher matcher = pattern.matcher(path); - return matcher.find() ? matcher : null; + if (matcher.find()) { + SpanUtil.updateName(pattern, path, context.getRequest().method().name()); + return matcher; + } + return null; } private String resourcePath(String url) { diff --git a/src/main/java/com/epam/aidial/core/util/RegexUtil.java b/src/main/java/com/epam/aidial/core/util/RegexUtil.java new file mode 100644 index 00000000..124ef6bf --- /dev/null +++ b/src/main/java/com/epam/aidial/core/util/RegexUtil.java @@ -0,0 +1,54 @@ +package com.epam.aidial.core.util; + +import lombok.experimental.UtilityClass; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@UtilityClass +public class RegexUtil { + + public String replaceNamedGroups(Pattern pattern, String input, List groups) { + if (groups == null || groups.isEmpty()) { + return input; + } + List regexGroups = collectGroups(pattern, input, groups); + if (regexGroups.isEmpty()) { + return input; + } + regexGroups.sort(Comparator.comparingInt(RegexGroup::start)); + StringBuilder nameBuilder = new StringBuilder(); + int prev = 0; + for (RegexGroup rg : regexGroups) { + nameBuilder + .append(input, prev, rg.start()) + .append('{').append(rg.group()).append('}'); + prev = rg.end(); + } + nameBuilder.append(input, prev, input.length()); + return nameBuilder.toString(); + } + + private List collectGroups(Pattern pattern, String input, List groups) { + List regexGroups = new ArrayList<>(); + Matcher matcher = pattern.matcher(input); + if (matcher.matches() && matcher.groupCount() > 0) { + for (String group : groups) { + try { + int start = matcher.start(group); + int end = matcher.end(group); + regexGroups.add(new RegexGroup(group, start, end)); + } catch (IllegalStateException | IllegalArgumentException ignored) { + //Ignore group mismatch + } + } + } + return regexGroups; + } + + private record RegexGroup(String group, int start, int end) { + } +} diff --git a/src/main/java/com/epam/aidial/core/util/SpanUtil.java b/src/main/java/com/epam/aidial/core/util/SpanUtil.java new file mode 100644 index 00000000..f4965fd5 --- /dev/null +++ b/src/main/java/com/epam/aidial/core/util/SpanUtil.java @@ -0,0 +1,18 @@ +package com.epam.aidial.core.util; + +import io.opentelemetry.api.trace.Span; +import lombok.experimental.UtilityClass; + +import java.util.List; +import java.util.regex.Pattern; + +@UtilityClass +public class SpanUtil { + + private static final List GROUPS = List.of("id", "bucket", "path"); + + public static void updateName(Pattern pathPattern, String path, String httpMethod) { + String spanName = RegexUtil.replaceNamedGroups(pathPattern, path, GROUPS); + Span.current().updateName(httpMethod + " " + spanName); + } +} diff --git a/src/test/java/com/epam/aidial/core/util/RegexUtilTest.java b/src/test/java/com/epam/aidial/core/util/RegexUtilTest.java new file mode 100644 index 00000000..cdcf6bf1 --- /dev/null +++ b/src/test/java/com/epam/aidial/core/util/RegexUtilTest.java @@ -0,0 +1,86 @@ +package com.epam.aidial.core.util; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class RegexUtilTest { + + @ParameterizedTest + @MethodSource("datasource") + void getName(String pathPattern, String path, String expectedName) { + var pattern = Pattern.compile(pathPattern); + var groups = List.of("id", "bucket", "path"); + assertEquals(expectedName, RegexUtil.replaceNamedGroups(pattern, path, groups)); + } + + public static List datasource() { + return List.of( + Arguments.of( + "^/+openai/deployments/(?.+?)/(completions|chat/completions|embeddings)$", + "/openai/deployments/gpt/chat/completions", + "/openai/deployments/{id}/chat/completions" + ), + Arguments.of( + "^/+openai/deployments/(?.+?)/(completions|chat/completions|embeddings)$", + "/openai/deployments/l/embeddings", + "/openai/deployments/{id}/embeddings" + ), + Arguments.of( + "^/+openai/deployments/(?.+?)$", + "/openai/deployments/gpt", + "/openai/deployments/{id}" + ), + Arguments.of( + "^/+openai/deployments/(?.+?)$", + "/openai/deployments/l", + "/openai/deployments/{id}" + ), + Arguments.of( + "^/v1/bucket$", + "/v1/bucket", + "/v1/bucket" + ), + Arguments.of( + "^/v1/files/(?[a-zA-Z0-9]+)/(?.*)", + "/v1/files/GHyJjv7CfrGRiv6RNajWsde7ET6bGTrbD45JatdSfsPK/path/to/file.pdf", + "/v1/files/{bucket}/{path}" + ), + Arguments.of( + "^/v1/files/(?[a-zA-Z0-9]+)/(?.*)", + "/v1/files/f/i/l/e.pdf", + "/v1/files/{bucket}/{path}" + ), + Arguments.of( + "^/v1/files/(?[a-zA-Z0-9]+)/(?.*)", + "/v1/files/f/f", + "/v1/files/{bucket}/{path}" + ), + Arguments.of( + "^/v1/ops/resource/share/(create|list|discard|revoke|copy)$", + "/v1/ops/resource/share/list", + "/v1/ops/resource/share/list" + ), + Arguments.of( + "^/v1/invitations/(?[a-zA-Z0-9]+)$", + "/v1/invitations/123abc", + "/v1/invitations/{id}" + ), + Arguments.of( + "^/api/(?.+?)$", + "/api/123", + "/api/123" + ), + Arguments.of( + "^/v1/(?.+?)$", + "/api/123", + "/api/123" + ) + ); + } +}