# Directory Structure
```
├── .gitignore
├── build.gradle.kts
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── infrastructures
│ └── elasticsearch
│ ├── build.gradle.kts
│ └── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── silbaram
│ │ │ └── github
│ │ │ └── infrastructures
│ │ │ └── elasticsearch
│ │ │ ├── config
│ │ │ │ └── ElasticsearchClientConfig.java
│ │ │ ├── properties
│ │ │ │ └── ElasticsearchProperties.java
│ │ │ └── provider
│ │ │ ├── ElasticsearchAliasesProvider.java
│ │ │ ├── ElasticsearchCatAllocationProvider.java
│ │ │ ├── ElasticsearchClusterStatisticsProvider.java
│ │ │ ├── ElasticsearchHealthProvider.java
│ │ │ ├── ElasticsearchIndicesProvider.java
│ │ │ ├── ElasticsearchMappingsProvider.java
│ │ │ └── ElasticsearchSearchProvider.java
│ │ └── resources
│ │ └── application-elasticsearch.yml
│ └── test
│ └── java
│ └── com
│ └── silbaram
│ └── github
│ └── infrastructures
│ └── elasticsearch
│ └── provider
│ ├── ElasticsearchAliasesProviderTest.java
│ ├── ElasticsearchCatAllocationProviderTest.java
│ ├── ElasticsearchClusterStatisticsProviderTest.java
│ ├── ElasticsearchHealthProviderTest.java
│ ├── ElasticsearchIndicesProviderTest.java
│ ├── ElasticsearchMappingsProviderTest.java
│ └── ElasticsearchSearchProviderTest.java
├── LICENSE
├── mcp-server
│ └── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── silbaram
│ │ │ └── github
│ │ │ └── mcp
│ │ │ └── server
│ │ │ ├── elasticsearch
│ │ │ │ └── tools
│ │ │ │ ├── AliasesToolsService.java
│ │ │ │ ├── ClusterHealthToolsService.java
│ │ │ │ ├── ClusterStatisticsToolsService.java
│ │ │ │ ├── config
│ │ │ │ │ └── ToolConfig.java
│ │ │ │ ├── DocumentSearchToolsService.java
│ │ │ │ ├── IndicesToolsService.java
│ │ │ │ ├── MappingsToolsService.java
│ │ │ │ └── ShardAllocationToolsService.java
│ │ │ └── ElasticSearchMcpServerApplication.java
│ │ └── resources
│ │ ├── application.yml
│ │ ├── logback-dev.xml
│ │ └── logback-prod.xml
│ └── test
│ └── java
│ └── com
│ └── silbaram
│ └── github
│ └── mcp
│ └── server
│ └── elasticsearch
│ └── tools
│ └── ShardAllocationToolsServiceTest.java
├── readme-ko.md
├── readme.md
└── settings.gradle.kts
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | .gradle
2 | build/
3 | !gradle/wrapper/gradle-wrapper.jar
4 | !**/src/main/**/build/
5 | !**/src/test/**/build/
6 |
7 | ### IntelliJ IDEA ###
8 | .idea/
9 | *.iws
10 | *.iml
11 | *.ipr
12 | out/
13 | !**/src/main/**/out/
14 | !**/src/test/**/out/
15 |
16 | ### Eclipse ###
17 | .apt_generated
18 | .classpath
19 | .factorypath
20 | .project
21 | .settings
22 | .springBeans
23 | .sts4-cache
24 | bin/
25 | !**/src/main/**/bin/
26 | !**/src/test/**/bin/
27 |
28 | ### NetBeans ###
29 | /nbproject/private/
30 | /nbbuild/
31 | /dist/
32 | /nbdist/
33 | /.nb-gradle/
34 |
35 | ### VS Code ###
36 | .vscode/
37 |
38 | ### Mac OS ###
39 | .DS_Store
```
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
```markdown
1 | <div align="center">
2 | <h1>Elasticsearch MCP Server</h1>
3 | <p>Spring AI MCP-based Elasticsearch Data Processing and Search Server</p>
4 | </div>
5 | <hr/>
6 |
7 | # Elasticsearch MCP Server
8 |
9 | ## Introduction
10 |
11 | The Elasticsearch Model Context Protocol (MCP) Server is a server application developed based on Spring AI MCP.
12 | It is designed to easily define various data processing workflows through MCP and efficiently index and search the results in an Elasticsearch cluster.
13 |
14 | ### Main Features
15 | - **Automatic MCP Tool Registration and Execution**: Features defined with the `@Tool` annotation in a Spring Boot environment are automatically registered with the MCP server. This allows external clients (e.g., Claude, FastMCP CLI) to call these functions via standard JSON-RPC based communication.
16 | - **Elasticsearch Cluster Integration**: Uses the official Elasticsearch Java client to easily call major Elasticsearch APIs such as cluster health checks, index mapping lookups, document indexing and searching, and provides the results in the form of MCP tools.
17 | - **Scalable Architecture**: A modular package structure allows for flexible separation and management of client settings for different Elasticsearch versions. It is also designed to easily add new tools or external integration features.
18 |
19 | ## Available MCP Tools
20 |
21 | - `get_cluster_health`: Returns basic information about the status of the Elasticsearch cluster.
22 | - `get_cluster_statistics`: Retrieves comprehensive cluster statistics including cluster name, UUID, status, node roles, OS and JVM resource usage, index count, and shard metrics.
23 | - `get_cat_mappings`: Retrieves field mapping information for a specific Elasticsearch index.
24 | - `get_cat_indices`: Retrieves a list of all indices in Elasticsearch.
25 | - `get_cat_indices_by_name`: Retrieves a list of indices that match the specified index name or wildcard pattern.
26 | - `get_cat_aliases`: Retrieves a list of all aliases in Elasticsearch.
27 | - `get_cat_aliases_by_name`: Retrieves a list of aliases that match the specified alias name or wildcard pattern.
28 | - `get_document_search_by_index`: Searches for documents within an Elasticsearch index using AI-generated queryDSL.
29 | - `get_shard_allocation`: Returns information about shard allocation in the Elasticsearch cluster.
30 | - `get_shard_allocation_for_node`: Returns information about shard allocation for a specific node in the Elasticsearch cluster.
31 |
32 | ## Technology Stack
33 |
34 | - **Language**: Java 17
35 | - **Framework**: Spring Boot 3.4.5, Spring AI MCP
36 | - **Search Engine**: Elasticsearch 7.16 or later
37 | - **Build Tool**: Gradle 8.12
38 |
39 | ## Prerequisites
40 |
41 | The following software is required to build and run this project:
42 | - JDK: Version 17 or later
43 | - Running Elasticsearch instance: Version 7.16 or later
44 | - MCP Client (e.g., Claude Desktop)
45 |
46 | ## Installation and Execution
47 |
48 | Here's how to set up and run the project in your local environment:
49 |
50 | ### 1. Clone the Source Code
51 | ```bash
52 | git clone https://github.com/silbaram/elasticsearch-mcp-server.git
53 | cd elasticsearch-mcp-server
54 | ```
55 |
56 | ### 2. Configure `application.yml` in the `mcp-server` module
57 | Open the `application.yml` file located in `mcp-server/src/main/resources/application.yml` to set up your Elasticsearch cluster information.
58 | ```yaml
59 | elasticsearch:
60 | version: "8.6.1" # Specifies the version of the Elasticsearch cluster to connect to.
61 | search:
62 | hosts:
63 | - http://localhost:9200 # Specifies the access address of the Elasticsearch cluster.
64 | ```
65 |
66 | ### 3. Build
67 | Use the following command to build the project:
68 | ```bash
69 | ./gradlew build
70 | ```
71 | - The built JAR file can be found in the `mcp-server/build/libs/` directory.
72 |
73 | ### 4. Configure MCP Client
74 | - Launch your MCP client. (You can find a list of MCP clients at [MCP Client List](https://modelcontextprotocol.io/clients). This guide uses Claude Desktop as an example.)
75 | - In your MCP client's settings menu, navigate to 'Developer' > 'MCP Servers'.
76 | - Click the 'Edit Config' button and add a new MCP server configuration with the following content. (You must use the actual path to the JAR file you built earlier.)
77 | ```json
78 | {
79 | "mcpServers": {
80 | "elasticsearch-server": {
81 | "command": "java",
82 | "args": [
83 | "-Dusername=YOUR_USERNAME",
84 | "-Dpassword=YOUR_PASSWORD",
85 | "-jar",
86 | "/path/to/your/mcp-server.jar"
87 | ]
88 | }
89 | }
90 | }
91 | ```
92 | - `-Dusername` (Optional): Specifies the user ID required to access the Elasticsearch cluster. (e.g., `-Dusername=elastic`)
93 | - `-Dpassword` (Optional): Specifies the password required to access the Elasticsearch cluster. (e.g., `-Dpassword=yoursecurepassword`)
94 | - `/path/to/your/mcp-server.jar`: This must be replaced with the actual full path to your built `mcp-server.jar` file.
```
--------------------------------------------------------------------------------
/infrastructures/elasticsearch/src/main/resources/application-elasticsearch.yml:
--------------------------------------------------------------------------------
```yaml
1 |
```
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
```
1 | #Wed May 07 22:31:17 KST 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
```
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
```kotlin
1 | buildscript {
2 | repositories {
3 | mavenCentral()
4 | }
5 | dependencies {
6 | // yml 파싱용
7 | classpath("org.yaml:snakeyaml:2.1")
8 | }
9 | }
10 |
11 | rootProject.name = "elasticsearch-mcp-server"
12 |
13 | include("mcp-server")
14 |
15 | include("infrastructures")
16 | include("infrastructures:elasticsearch")
```
--------------------------------------------------------------------------------
/mcp-server/src/main/resources/application.yml:
--------------------------------------------------------------------------------
```yaml
1 | elasticsearch:
2 | version: "8.6.1"
3 | search:
4 | username: ${username:EMPTY}
5 | password: ${password:EMPTY}
6 | hosts:
7 | - http://localhost:9200
8 |
9 | spring:
10 | main:
11 | web-application-type: none
12 | banner-mode: off
13 | include:
14 | - elasticsearch
15 | ai:
16 | mcp:
17 | server:
18 | stdio: true
19 | name: elasticsearch-mcp-server
20 | version: 0.0.1
21 | type: ASYNC
22 | resource-change-notification: false
23 | prompt-change-notification: false
24 | tool-change-notification: false
25 |
26 | logging:
27 | config: classpath:logback-prod.xml
```
--------------------------------------------------------------------------------
/mcp-server/src/main/java/com/silbaram/github/mcp/server/ElasticSearchMcpServerApplication.java:
--------------------------------------------------------------------------------
```java
1 | package com.silbaram.github.mcp.server;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.context.annotation.ComponentScan;
6 |
7 | @SpringBootApplication
8 | @ComponentScan(basePackages = {
9 | "com.silbaram.github.mcp.server",
10 | "com.silbaram.github.infrastructures.elasticsearch"
11 | })
12 | public class ElasticSearchMcpServerApplication {
13 |
14 | public static void main(String[] args) {
15 | SpringApplication.run(ElasticSearchMcpServerApplication.class, args);
16 | }
17 | }
```
--------------------------------------------------------------------------------
/infrastructures/elasticsearch/src/main/java/com/silbaram/github/infrastructures/elasticsearch/properties/ElasticsearchProperties.java:
--------------------------------------------------------------------------------
```java
1 | package com.silbaram.github.infrastructures.elasticsearch.properties;
2 |
3 | import org.springframework.boot.context.properties.ConfigurationProperties;
4 |
5 | import java.util.List;
6 |
7 | @ConfigurationProperties(prefix = "elasticsearch.search")
8 | public class ElasticsearchProperties {
9 | /** application.yml 의 elasticsearch.search.hosts 값을 바인딩 */
10 | private List<String> hosts;
11 | private String username;
12 | private String password;
13 |
14 | public List<String> getHosts() {
15 | return hosts;
16 | }
17 | public void setHosts(List<String> hosts) {
18 | this.hosts = hosts;
19 | }
20 |
21 | public String getUsername() {
22 | return username;
23 | }
24 | public void setUsername(String username) {
25 | this.username = username;
26 | }
27 |
28 | public String getPassword() {
29 | return password;
30 | }
31 | public void setPassword(String password) {
32 | this.password = password;
33 | }
34 | }
35 |
```
--------------------------------------------------------------------------------
/mcp-server/src/main/java/com/silbaram/github/mcp/server/elasticsearch/tools/ClusterHealthToolsService.java:
--------------------------------------------------------------------------------
```java
1 | package com.silbaram.github.mcp.server.elasticsearch.tools;
2 |
3 | import com.silbaram.github.infrastructures.elasticsearch.provider.ElasticsearchHealthProvider;
4 | import org.springframework.ai.tool.annotation.Tool;
5 | import org.springframework.stereotype.Service;
6 |
7 | import java.io.IOException;
8 | import java.util.Map;
9 |
10 | @Service
11 | public class ClusterHealthToolsService {
12 |
13 | private final ElasticsearchHealthProvider elasticsearchHealthProvider;
14 |
15 | public ClusterHealthToolsService(ElasticsearchHealthProvider elasticsearchHealthProvider) {
16 | this.elasticsearchHealthProvider = elasticsearchHealthProvider;
17 | }
18 |
19 |
20 | @Tool(
21 | name = "get_cluster_health",
22 | description = "Returns basic information about the health of the cluster."
23 | )
24 | public Map<String, String> getClusterHealth() {
25 | try {
26 | return elasticsearchHealthProvider.getClusterHealth();
27 | } catch (IOException e) {
28 | throw new RuntimeException(e.getMessage());
29 | }
30 | }
31 | }
32 |
```
--------------------------------------------------------------------------------
/mcp-server/src/main/java/com/silbaram/github/mcp/server/elasticsearch/tools/MappingsToolsService.java:
--------------------------------------------------------------------------------
```java
1 | package com.silbaram.github.mcp.server.elasticsearch.tools;
2 |
3 | import com.silbaram.github.infrastructures.elasticsearch.provider.ElasticsearchMappingsProvider;
4 | import org.springframework.ai.tool.annotation.Tool;
5 | import org.springframework.ai.tool.annotation.ToolParam;
6 | import org.springframework.stereotype.Service;
7 |
8 | import java.io.IOException;
9 |
10 | @Service
11 | public class MappingsToolsService {
12 |
13 | private final ElasticsearchMappingsProvider elasticsearchMappingsProvider;
14 |
15 | public MappingsToolsService(ElasticsearchMappingsProvider elasticsearchMappingsProvider) {
16 | this.elasticsearchMappingsProvider = elasticsearchMappingsProvider;
17 | }
18 |
19 | @Tool(
20 | name = "get_cat_mappings",
21 | description = "Get field mappings for a specific Elasticsearch index"
22 | )
23 | public String getCatMappings(
24 | @ToolParam(description = "Name of the Elasticsearch index to get mappings for")
25 | String index
26 | ) {
27 | try {
28 | return elasticsearchMappingsProvider.getCatMappings(index);
29 | } catch (IOException e) {
30 | throw new RuntimeException(e.getMessage());
31 | }
32 | }
33 | }
34 |
```
--------------------------------------------------------------------------------
/mcp-server/src/main/java/com/silbaram/github/mcp/server/elasticsearch/tools/ClusterStatisticsToolsService.java:
--------------------------------------------------------------------------------
```java
1 | package com.silbaram.github.mcp.server.elasticsearch.tools;
2 |
3 | import com.silbaram.github.infrastructures.elasticsearch.provider.ElasticsearchClusterStatisticsProvider;
4 | import org.springframework.ai.tool.annotation.Tool;
5 | import org.springframework.stereotype.Service;
6 |
7 | import java.io.IOException;
8 | import java.util.Map;
9 |
10 | @Service
11 | public class ClusterStatisticsToolsService {
12 |
13 | private final ElasticsearchClusterStatisticsProvider elasticsearchClusterStatisticsProvider;
14 |
15 | public ClusterStatisticsToolsService(ElasticsearchClusterStatisticsProvider elasticsearchClusterStatisticsProvider) {
16 | this.elasticsearchClusterStatisticsProvider = elasticsearchClusterStatisticsProvider;
17 | }
18 |
19 | @Tool(
20 | name = "get_cluster_statistics",
21 | description = "Returns comprehensive cluster statistics including cluster name, UUID, health status, node roles, OS and JVM resource usage, index counts, and shard metrics."
22 | )
23 | public Map<String, Object> getClusterStatistics() {
24 | try {
25 | return elasticsearchClusterStatisticsProvider.getClusterStatistics();
26 | } catch (IOException e) {
27 | throw new RuntimeException(e.getMessage());
28 | }
29 | }
30 | }
31 |
```
--------------------------------------------------------------------------------
/mcp-server/src/main/java/com/silbaram/github/mcp/server/elasticsearch/tools/DocumentSearchToolsService.java:
--------------------------------------------------------------------------------
```java
1 | package com.silbaram.github.mcp.server.elasticsearch.tools;
2 |
3 | import com.silbaram.github.infrastructures.elasticsearch.provider.ElasticsearchSearchProvider;
4 | import org.springframework.ai.tool.annotation.Tool;
5 | import org.springframework.ai.tool.annotation.ToolParam;
6 | import org.springframework.stereotype.Service;
7 |
8 | import java.io.IOException;
9 |
10 | @Service
11 | public class DocumentSearchToolsService {
12 |
13 | private final ElasticsearchSearchProvider elasticsearchSearchProvider;
14 |
15 | public DocumentSearchToolsService(ElasticsearchSearchProvider elasticsearchSearchProvider) {
16 | this.elasticsearchSearchProvider = elasticsearchSearchProvider;
17 | }
18 |
19 | @Tool(
20 | name = "get_document_search_by_index",
21 | description = "Search for documents in your Elasticsearch index using queryDsl"
22 | )
23 | public String getDocumentSearchByIndex(
24 | @ToolParam(description = "The name of the elasticsearch index to search")
25 | String index,
26 | @ToolParam(description = "elasticsearch Search queryDSL")
27 | String queryBody
28 | ) {
29 | try {
30 | return elasticsearchSearchProvider.searchByIndex(index, queryBody);
31 | } catch (IOException e) {
32 | throw new RuntimeException(e);
33 | }
34 | }
35 | }
36 |
```
--------------------------------------------------------------------------------
/infrastructures/elasticsearch/build.gradle.kts:
--------------------------------------------------------------------------------
```kotlin
1 | import org.springframework.boot.gradle.tasks.bundling.BootJar
2 | import org.yaml.snakeyaml.Yaml
3 | import java.io.FileInputStream
4 |
5 | val jar: Jar by tasks
6 | val bootJar: BootJar by tasks
7 |
8 | bootJar.enabled = false
9 | jar.enabled = true
10 |
11 | // 1) 파싱기 준비
12 | val yaml = Yaml()
13 |
14 | // 2) 리소스 디렉터리의 application.yml 파일 위치 지정
15 | val ymlFile = file("../../mcp-server/src/main/resources/application.yml")
16 |
17 | // 3) 파일을 맵으로 읽어들임
18 | @Suppress("UNCHECKED_CAST")
19 | val root: Map<String, Any> = yaml.load(FileInputStream(ymlFile)) as Map<String, Any>
20 |
21 | // 4) nested 맵에서 버전 꺼내기
22 | val esConfig = root["elasticsearch"] as Map<*, *>
23 | val elasticsearchVersion = esConfig["version"] as String
24 |
25 | dependencies {
26 | // Elasticsearch Java API Client
27 | implementation("co.elastic.clients:elasticsearch-java:$elasticsearchVersion")
28 | implementation("com.fasterxml.jackson.core:jackson-databind")
29 |
30 | // Test dependencies
31 | testImplementation("org.junit.platform:junit-platform-commons:1.10.0") // Align JUnit Platform Commons version
32 | testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
33 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0")
34 | testImplementation("org.mockito:mockito-core:5.5.0")
35 | testImplementation("org.mockito:mockito-junit-jupiter:5.5.0")
36 | }
37 |
38 | // Configure the test task to use JUnit Platform
39 | tasks.withType<Test> {
40 | useJUnitPlatform()
41 | }
```
--------------------------------------------------------------------------------
/mcp-server/src/main/java/com/silbaram/github/mcp/server/elasticsearch/tools/config/ToolConfig.java:
--------------------------------------------------------------------------------
```java
1 | package com.silbaram.github.mcp.server.elasticsearch.tools.config;
2 |
3 | import com.silbaram.github.mcp.server.elasticsearch.tools.*;
4 | import com.silbaram.github.mcp.server.elasticsearch.tools.ShardAllocationToolsService;
5 | import org.springframework.ai.tool.ToolCallbackProvider;
6 | import org.springframework.ai.tool.method.MethodToolCallbackProvider;
7 | import org.springframework.context.annotation.Bean;
8 | import org.springframework.context.annotation.Configuration;
9 |
10 | import java.util.ArrayList;
11 | import java.util.List;
12 |
13 | @Configuration
14 | public class ToolConfig {
15 |
16 | @Bean
17 | public ToolCallbackProvider elasticSearchTools(
18 | ClusterHealthToolsService clusterHealthToolsService,
19 | MappingsToolsService mappingsToolsService,
20 | ClusterStatisticsToolsService clusterStatisticsToolsService,
21 | IndicesToolsService indicesToolsService,
22 | AliasesToolsService aliasesToolsService,
23 | DocumentSearchToolsService documentSearchToolsService,
24 | ShardAllocationToolsService shardAllocationToolsService
25 | ) {
26 |
27 | List<Object> toolList = new ArrayList<>();
28 | toolList.add(clusterHealthToolsService);
29 | toolList.add(mappingsToolsService);
30 | toolList.add(clusterStatisticsToolsService);
31 | toolList.add(indicesToolsService);
32 | toolList.add(aliasesToolsService);
33 | toolList.add(documentSearchToolsService);
34 | toolList.add(shardAllocationToolsService);
35 |
36 | return MethodToolCallbackProvider.builder().toolObjects(toolList.toArray()).build();
37 |
38 | }
39 | }
40 |
```
--------------------------------------------------------------------------------
/mcp-server/src/main/java/com/silbaram/github/mcp/server/elasticsearch/tools/AliasesToolsService.java:
--------------------------------------------------------------------------------
```java
1 | package com.silbaram.github.mcp.server.elasticsearch.tools;
2 |
3 | import com.silbaram.github.infrastructures.elasticsearch.provider.ElasticsearchAliasesProvider;
4 | import org.springframework.ai.tool.annotation.Tool;
5 | import org.springframework.ai.tool.annotation.ToolParam;
6 | import org.springframework.stereotype.Service;
7 |
8 | import java.io.IOException;
9 | import java.util.List;
10 | import java.util.Map;
11 |
12 | @Service
13 | public class AliasesToolsService {
14 |
15 | private final ElasticsearchAliasesProvider elasticsearchAliasesProvider;
16 |
17 | public AliasesToolsService(ElasticsearchAliasesProvider elasticsearchAliasesProvider) {
18 | this.elasticsearchAliasesProvider = elasticsearchAliasesProvider;
19 | }
20 |
21 | @Tool(
22 | name = "get_cat_aliases",
23 | description = "Get a list of all aliases Elasticsearch."
24 | )
25 | public List<Map<String, Object>> getCatAliases() {
26 | try {
27 | return elasticsearchAliasesProvider.getCatAliases();
28 | } catch (IOException e) {
29 | throw new RuntimeException(e.getMessage());
30 | }
31 | }
32 |
33 | @Tool(
34 | name = "get_cat_aliases_by_name",
35 | description = "Get only the aliases matching the specified alias name or wildcard pattern."
36 | )
37 | public List<Map<String, Object>> getCatAliasesByName(
38 | @ToolParam(description = "Alias name or wildcard pattern to filter")
39 | String aliasName
40 | ) {
41 | try {
42 | return elasticsearchAliasesProvider.getCatAliasesByName(aliasName);
43 | } catch (IOException e) {
44 | throw new RuntimeException(e.getMessage());
45 | }
46 | }
47 | }
48 |
```
--------------------------------------------------------------------------------
/mcp-server/src/main/java/com/silbaram/github/mcp/server/elasticsearch/tools/IndicesToolsService.java:
--------------------------------------------------------------------------------
```java
1 | package com.silbaram.github.mcp.server.elasticsearch.tools;
2 |
3 | import com.silbaram.github.infrastructures.elasticsearch.provider.ElasticsearchIndicesProvider;
4 | import org.springframework.ai.tool.annotation.Tool;
5 | import org.springframework.ai.tool.annotation.ToolParam;
6 | import org.springframework.stereotype.Service;
7 |
8 | import java.io.IOException;
9 | import java.util.List;
10 | import java.util.Map;
11 |
12 | @Service
13 | public class IndicesToolsService {
14 |
15 | private final ElasticsearchIndicesProvider elasticsearchIndicesProvider;
16 |
17 | public IndicesToolsService(ElasticsearchIndicesProvider elasticsearchIndicesProvider) {
18 | this.elasticsearchIndicesProvider = elasticsearchIndicesProvider;
19 | }
20 |
21 | @Tool(
22 | name = "get_cat_indices",
23 | description = "Get a list of all indices in Elasticsearch."
24 | )
25 | public List<Map<String, Object>> getCatIndices() {
26 | try {
27 | return elasticsearchIndicesProvider.getCatIndices();
28 | } catch (IOException e) {
29 | throw new RuntimeException(e.getMessage());
30 | }
31 | }
32 |
33 | @Tool(
34 | name = "get_cat_indices_by_name",
35 | description = "Get a list of indices matching the specified index name or wildcard pattern."
36 | )
37 | public List<Map<String, Object>> getCatIndicesByName(
38 | @ToolParam(description = "Index name or pattern to filter indices by")
39 | String indexName
40 | ) {
41 | try {
42 | return elasticsearchIndicesProvider.getCatIndicesByName(indexName);
43 | } catch (IOException e) {
44 | throw new RuntimeException(e.getMessage());
45 | }
46 | }
47 | }
48 |
```
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
```kotlin
1 | import org.gradle.kotlin.dsl.dependencies
2 | import org.springframework.boot.gradle.tasks.bundling.BootJar
3 |
4 | val jar: Jar by tasks
5 | val bootJar: BootJar by tasks
6 |
7 | bootJar.enabled = false
8 | jar.enabled = true
9 |
10 | plugins {
11 | // Spring Boot 플러그인: 의존성 관리, 실행·패키징 지원
12 | id("org.springframework.boot") version "3.4.5"
13 | // Maven BOM(import) 기능 제공
14 | id("io.spring.dependency-management") version "1.1.7"
15 | // Spring Java Format 플러그인
16 | id("io.spring.javaformat") version "0.0.43"
17 | // Java 플러그인
18 | java
19 | }
20 |
21 | java {
22 | sourceCompatibility = JavaVersion.VERSION_17
23 | }
24 |
25 | allprojects {
26 | group = "com.silbaram.github"
27 | version = "0.0.1-SNAPSHOT"
28 | description = "Elasticsearch MCP Server"
29 |
30 | repositories {
31 | mavenCentral()
32 |
33 | maven("https://repo.spring.io/milestone")
34 | maven("https://repo.spring.io/snapshot")
35 | maven("https://central.sonatype.com/repository/maven-snapshots/")
36 | }
37 | }
38 |
39 | subprojects {
40 | apply(plugin = "java")
41 |
42 | apply(plugin = "org.springframework.boot")
43 | apply(plugin = "io.spring.dependency-management")
44 |
45 | dependencyManagement {
46 | imports {
47 | // Spring AI BOM
48 | mavenBom("org.springframework.ai:spring-ai-bom:1.0.0-SNAPSHOT")
49 | }
50 | }
51 |
52 | dependencies {
53 | implementation("org.springframework.ai:spring-ai-starter-mcp-server-webflux")
54 |
55 | testImplementation("org.springframework.boot:spring-boot-starter-test")
56 | }
57 |
58 | tasks.withType<JavaCompile> {
59 | options.encoding = "UTF-8"
60 | }
61 |
62 | // 테스트 실행 시 JUnit Platform 사용
63 | tasks.withType<Test> {
64 | useJUnitPlatform()
65 | }
66 | }
67 |
68 | project(":mcp-server") {
69 | dependencies {
70 | //모듈 의존성 (mcp-server <- infrastructures:elasticsearch)
71 | implementation(project(":infrastructures:elasticsearch"))
72 | }
73 | }
74 |
75 | project(":infrastructures") {
76 | val jar: Jar by tasks
77 | val bootJar: BootJar by tasks
78 |
79 | bootJar.enabled = false
80 | jar.enabled = false
81 |
82 | // Disable Java compilation and resource processing
83 | tasks.withType<JavaCompile> {
84 | enabled = false
85 | }
86 | tasks.withType<ProcessResources> {
87 | enabled = false
88 | }
89 | // Disable classes and jar tasks (already present)
90 | tasks.named("classes") {
91 | enabled = false
92 | }
93 | }
94 |
```
--------------------------------------------------------------------------------
/mcp-server/src/main/resources/logback-dev.xml:
--------------------------------------------------------------------------------
```
1 | <?xml version="1.0" encoding="UTF-8"?>
2 | <configuration>
3 |
4 | <include resource="org/springframework/boot/logging/logback/defaults.xml" />
5 |
6 | <property name="CONSOLE_PATTERN" value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS, Asia/Seoul}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) [${java.rmi.server.hostname:-127.0.0.1}] [${nd.hostname:-localhost}] %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr([%class{5} > %method:%line]){magenta} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
7 | <property name="FILE_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS, Asia/Seoul} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} [%class{5} > %method:%line] : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
8 |
9 | <property name="LOG_PATH" value="../logs"/>
10 | <property name="LOG_FILE" value="mcp-server"/>
11 |
12 | <appender name="FILE_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
13 | <file>${LOG_PATH}/${LOG_FILE}.log</file>
14 | <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
15 | <fileNamePattern>${LOG_PATH}/old/${LOG_FILE}-%d{yyyyMMdd}.%i.log.gz</fileNamePattern>
16 | <maxHistory>5</maxHistory>
17 | <maxFileSize>300MB</maxFileSize>
18 | <totalSizeCap>300MB</totalSizeCap>
19 | </rollingPolicy>
20 | <encoder>
21 | <pattern>${FILE_PATTERN}</pattern>
22 | <charset>utf8</charset>
23 | </encoder>
24 | </appender>
25 |
26 | <appender name="FILE_ERROR_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
27 | <file>${LOG_PATH}/${LOG_FILE}-error.log</file>
28 | <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
29 | <level>ERROR</level>
30 | </filter>
31 | <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
32 | <fileNamePattern>${LOG_PATH}/old/${LOG_FILE}-error-%d{yyyyMMdd}.%i.log.gz</fileNamePattern>
33 | <maxHistory>10</maxHistory>
34 | <maxFileSize>300MB</maxFileSize>
35 | <totalSizeCap>300MB</totalSizeCap>
36 | </rollingPolicy>
37 | <encoder>
38 | <pattern>${FILE_PATTERN}</pattern>
39 | <charset>utf8</charset>
40 | </encoder>
41 | </appender>
42 |
43 | <!-- 로그 레벨 설정 -->
44 | <logger name="org.springframework" level="INFO" />
45 | <logger name="com.silbaram.github.mcp.server" level="DEBUG" />
46 | <logger name="com.silbaram.github.infrastructures.elasticsearch" level="DEBUG" />
47 |
48 | <!-- 정의 되지 않은 logger 들에게 일괄 적용됨 -->
49 | <root level="INFO">
50 | <appender-ref ref="FILE_APPENDER" />
51 | <appender-ref ref="FILE_ERROR_APPENDER" />
52 | </root>
53 |
54 | </configuration>
55 |
```
--------------------------------------------------------------------------------
/infrastructures/elasticsearch/src/main/java/com/silbaram/github/infrastructures/elasticsearch/config/ElasticsearchClientConfig.java:
--------------------------------------------------------------------------------
```java
1 | package com.silbaram.github.infrastructures.elasticsearch.config;
2 |
3 | import co.elastic.clients.elasticsearch.ElasticsearchClient;
4 | import co.elastic.clients.json.jackson.JacksonJsonpMapper;
5 | import co.elastic.clients.transport.ElasticsearchTransport;
6 | import co.elastic.clients.transport.rest_client.RestClientTransport;
7 | import com.silbaram.github.infrastructures.elasticsearch.properties.ElasticsearchProperties;
8 | import org.apache.http.HttpHost;
9 | import org.apache.http.auth.AuthScope;
10 | import org.apache.http.auth.UsernamePasswordCredentials;
11 | import org.apache.http.client.CredentialsProvider;
12 | import org.apache.http.impl.client.BasicCredentialsProvider;
13 | import org.elasticsearch.client.RestClient;
14 | import org.springframework.boot.context.properties.EnableConfigurationProperties;
15 | import org.springframework.context.annotation.Bean;
16 | import org.springframework.context.annotation.Configuration;
17 |
18 | import java.util.Objects;
19 |
20 | @Configuration
21 | @EnableConfigurationProperties(ElasticsearchProperties.class)
22 | public class ElasticsearchClientConfig {
23 |
24 | private final ElasticsearchProperties props;
25 | private static final String EMPTY_VALUE = "EMPTY";
26 |
27 | public ElasticsearchClientConfig(ElasticsearchProperties props) {
28 | this.props = props;
29 | }
30 |
31 | @Bean
32 | public RestClient restClient() {
33 |
34 | HttpHost[] httpHosts = props.getHosts().stream()
35 | .map(HttpHost::create)
36 | .toArray(HttpHost[]::new);
37 |
38 | // 아이디, 패스워드가 있을 때만 인증 추가
39 | if (!Objects.equals(props.getUsername(), EMPTY_VALUE) && !Objects.equals(props.getPassword(), EMPTY_VALUE)) {
40 | final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
41 | credentialsProvider.setCredentials(
42 | AuthScope.ANY,
43 | new UsernamePasswordCredentials(props.getUsername(), props.getPassword())
44 | );
45 |
46 | return RestClient.builder(httpHosts)
47 | .setHttpClientConfigCallback(httpClientBuilder ->
48 | httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)
49 | )
50 | .build();
51 | } else {
52 | return RestClient.builder(httpHosts).build();
53 | }
54 | }
55 |
56 | @Bean
57 | public ElasticsearchTransport elasticsearchTransport(RestClient restClient) {
58 | // JacksonJsonpMapper를 이용한 JSON 파싱
59 | return new RestClientTransport(restClient, new JacksonJsonpMapper());
60 | }
61 |
62 | @Bean
63 | public ElasticsearchClient elasticsearchMcpClient(ElasticsearchTransport elasticsearchTransport) {
64 | // 최종 Java API 클라이언트
65 | return new ElasticsearchClient(elasticsearchTransport);
66 | }
67 |
68 | }
```
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
```
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
```
--------------------------------------------------------------------------------
/mcp-server/src/main/java/com/silbaram/github/mcp/server/elasticsearch/tools/ShardAllocationToolsService.java:
--------------------------------------------------------------------------------
```java
1 | package com.silbaram.github.mcp.server.elasticsearch.tools;
2 |
3 | import com.silbaram.github.infrastructures.elasticsearch.provider.ElasticsearchCatAllocationProvider;
4 | import org.springframework.ai.tool.annotation.Tool;
5 | import org.springframework.ai.tool.annotation.ToolParam;
6 | import org.springframework.stereotype.Service;
7 |
8 | import java.io.IOException;
9 | import java.util.List;
10 | import java.util.Map;
11 |
12 | /**
13 | * Service class that provides tools to get shard allocation information from Elasticsearch.
14 | */
15 | @Service
16 | public class ShardAllocationToolsService {
17 |
18 | private final ElasticsearchCatAllocationProvider elasticsearchCatAllocationProvider;
19 |
20 | /**
21 | * Constructs a ShardAllocationToolsService with the given ElasticsearchCatAllocationProvider.
22 | *
23 | * @param elasticsearchCatAllocationProvider The provider for Elasticsearch cat allocation information.
24 | */
25 | public ShardAllocationToolsService(ElasticsearchCatAllocationProvider elasticsearchCatAllocationProvider) {
26 | this.elasticsearchCatAllocationProvider = elasticsearchCatAllocationProvider;
27 | }
28 |
29 | /**
30 | * Retrieves information about shard allocation in the Elasticsearch cluster.
31 | *
32 | * @return A list of maps, where each map represents shard allocation information.
33 | */
34 | @Tool(
35 | name = "get_shard_allocation",
36 | description = "Returns information about shard allocation in the Elasticsearch cluster."
37 | )
38 | public List<Map<String, Object>> getShardAllocation() {
39 | try {
40 | return elasticsearchCatAllocationProvider.getCatAllocation();
41 | } catch (IOException e) {
42 | throw new RuntimeException("Error retrieving shard allocation information: " + e.getMessage(), e);
43 | }
44 | }
45 |
46 | /**
47 | * Retrieves shard allocation information for a specific node in the Elasticsearch cluster.
48 | *
49 | * @param nodeId The ID of the node.
50 | * @return A list of maps, where each map represents a shard and its allocation details.
51 | * @throws RuntimeException if an IOException occurs during the Elasticsearch API call.
52 | */
53 | @Tool(
54 | name = "get_shard_allocation_for_node",
55 | description = "Returns information about shard allocation for a specific node in the Elasticsearch cluster."
56 | )
57 | public List<Map<String, Object>> getShardAllocationForNode(
58 | @ToolParam(description = "The ID of the node to get shard allocation for.") String nodeId) { // Annotation changed
59 | try {
60 | return elasticsearchCatAllocationProvider.getCatAllocation(nodeId);
61 | } catch (IOException e) {
62 | // Updated exception throwing:
63 | throw new RuntimeException("Error retrieving shard allocation information for node " + nodeId + ": " + e.getMessage(), e);
64 | }
65 | }
66 | }
67 |
```
--------------------------------------------------------------------------------
/infrastructures/elasticsearch/src/test/java/com/silbaram/github/infrastructures/elasticsearch/provider/ElasticsearchCatAllocationProviderTest.java:
--------------------------------------------------------------------------------
```java
1 | package com.silbaram.github.infrastructures.elasticsearch.provider;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import org.apache.http.HttpEntity;
5 | import org.apache.http.entity.ContentType;
6 | import org.apache.http.nio.entity.NStringEntity;
7 | import org.elasticsearch.client.Request;
8 | import org.elasticsearch.client.Response;
9 | import org.elasticsearch.client.RestClient;
10 | import org.junit.jupiter.api.BeforeEach;
11 | import org.junit.jupiter.api.Test;
12 | import org.junit.jupiter.api.extension.ExtendWith;
13 | import org.mockito.ArgumentMatchers;
14 | import org.mockito.InjectMocks;
15 | import org.mockito.Mock;
16 | import org.mockito.junit.jupiter.MockitoExtension;
17 |
18 | import java.io.IOException;
19 | import java.util.List;
20 | import java.util.Map;
21 |
22 | import static org.junit.jupiter.api.Assertions.*;
23 | import static org.mockito.Mockito.when;
24 |
25 | /**
26 | * Tests for {@link ElasticsearchCatAllocationProvider}.
27 | */
28 | @ExtendWith(MockitoExtension.class)
29 | public class ElasticsearchCatAllocationProviderTest {
30 |
31 | @Mock
32 | private RestClient mockRestClient;
33 |
34 | @Mock
35 | private Response mockResponse;
36 |
37 | @InjectMocks
38 | private ElasticsearchCatAllocationProvider provider;
39 |
40 | private final ObjectMapper objectMapper = new ObjectMapper();
41 |
42 | /**
43 | * Sets up mocks before each test.
44 | * Note: MockitoAnnotations.openMocks(this) is not strictly needed with MockitoExtension,
45 | * but can be useful for more complex setups or if not using the extension.
46 | */
47 | @BeforeEach
48 | void setUp() {
49 | // Initialization is handled by @ExtendWith(MockitoExtension.class)
50 | // and @Mock / @InjectMocks annotations.
51 | }
52 |
53 | /**
54 | * Tests {@link ElasticsearchCatAllocationProvider#getCatAllocation()} for a successful API call.
55 | *
56 | * @throws IOException if the simulated API call fails, which is part of the test.
57 | */
58 | @Test
59 | void testGetCatAllocation_Success() throws IOException {
60 | String jsonResponse = "[{\"node\": \"node1\", \"shards\": \"10\"}, {\"node\": \"node2\", \"shards\": \"5\"}]";
61 | HttpEntity entity = new NStringEntity(jsonResponse, ContentType.APPLICATION_JSON);
62 |
63 | when(mockResponse.getEntity()).thenReturn(entity);
64 | when(mockRestClient.performRequest(ArgumentMatchers.any(Request.class))).thenReturn(mockResponse);
65 |
66 | List<Map<String, Object>> result = provider.getCatAllocation();
67 |
68 | assertNotNull(result);
69 | assertEquals(2, result.size());
70 | assertEquals("node1", result.get(0).get("node"));
71 | assertEquals("10", result.get(0).get("shards"));
72 | assertEquals("node2", result.get(1).get("node"));
73 | assertEquals("5", result.get(1).get("shards"));
74 | }
75 |
76 | /**
77 | * Tests {@link ElasticsearchCatAllocationProvider#getCatAllocation()} when an IOException occurs.
78 | *
79 | * @throws IOException if the simulated API call fails, which is part of the test.
80 | */
81 | @Test
82 | void testGetCatAllocation_IOException() throws IOException {
83 | when(mockRestClient.performRequest(ArgumentMatchers.any(Request.class))).thenThrow(new IOException("Simulated API error"));
84 |
85 | assertThrows(IOException.class, () -> provider.getCatAllocation());
86 | }
87 |
88 | /**
89 | * Tests {@link ElasticsearchCatAllocationProvider#getCatAllocation(String)} for a successful API call with a valid nodeId.
90 | *
91 | * @throws IOException if the simulated API call fails, which is part of the test.
92 | */
93 | @Test
94 | void testGetCatAllocation_WithNodeId_Success() throws IOException {
95 | String nodeId = "node1";
96 | String jsonResponse = "[{\"node\": \"node1\", \"shards\": \"10\"}]";
97 | HttpEntity entity = new NStringEntity(jsonResponse, ContentType.APPLICATION_JSON);
98 |
99 | // Mock the response from the REST client
100 | Request expectedRequest = new Request("GET", "/_cat/allocation/" + nodeId + "?format=json");
101 |
102 | when(mockResponse.getEntity()).thenReturn(entity);
103 | // We need to be more specific with request matching if the path changes
104 | when(mockRestClient.performRequest(ArgumentMatchers.argThat(request ->
105 | request.getMethod().equals(expectedRequest.getMethod()) &&
106 | request.getEndpoint().equals(expectedRequest.getEndpoint())
107 | ))).thenReturn(mockResponse);
108 |
109 |
110 | List<Map<String, Object>> result = provider.getCatAllocation(nodeId);
111 |
112 | assertNotNull(result);
113 | assertEquals(1, result.size());
114 | assertEquals("node1", result.get(0).get("node"));
115 | assertEquals("10", result.get(0).get("shards"));
116 | }
117 |
118 | /**
119 | * Tests {@link ElasticsearchCatAllocationProvider#getCatAllocation(String)} when an IOException occurs.
120 | *
121 | * @throws IOException if the simulated API call fails, which is part of the test.
122 | */
123 | @Test
124 | void testGetCatAllocation_WithNodeId_IOException() throws IOException {
125 | String nodeId = "node1_error";
126 | Request expectedRequest = new Request("GET", "/_cat/allocation/" + nodeId + "?format=json");
127 |
128 | when(mockRestClient.performRequest(ArgumentMatchers.argThat(request ->
129 | request.getMethod().equals(expectedRequest.getMethod()) &&
130 | request.getEndpoint().equals(expectedRequest.getEndpoint())
131 | ))).thenThrow(new IOException("Simulated API error for node"));
132 |
133 | assertThrows(IOException.class, () -> provider.getCatAllocation(nodeId));
134 | }
135 | }
136 |
```
--------------------------------------------------------------------------------
/mcp-server/src/test/java/com/silbaram/github/mcp/server/elasticsearch/tools/ShardAllocationToolsServiceTest.java:
--------------------------------------------------------------------------------
```java
1 | package com.silbaram.github.mcp.server.elasticsearch.tools;
2 |
3 | import com.silbaram.github.infrastructures.elasticsearch.provider.ElasticsearchCatAllocationProvider;
4 | import org.junit.jupiter.api.BeforeEach;
5 | import org.junit.jupiter.api.Test;
6 | import org.junit.jupiter.api.extension.ExtendWith;
7 | import org.mockito.InjectMocks;
8 | import org.mockito.Mock;
9 | import org.mockito.junit.jupiter.MockitoExtension;
10 |
11 | import java.io.IOException;
12 | import java.util.ArrayList;
13 | import java.util.HashMap;
14 | import java.util.List;
15 | import java.util.Map;
16 |
17 | import static org.junit.jupiter.api.Assertions.*;
18 | import static org.mockito.Mockito.*;
19 |
20 | /**
21 | * Tests for {@link ShardAllocationToolsService}.
22 | */
23 | @ExtendWith(MockitoExtension.class)
24 | public class ShardAllocationToolsServiceTest {
25 |
26 | @Mock
27 | private ElasticsearchCatAllocationProvider mockElasticsearchCatAllocationProvider;
28 |
29 | @InjectMocks
30 | private ShardAllocationToolsService shardAllocationToolsService;
31 |
32 | private List<Map<String, Object>> sampleAllocationData;
33 |
34 | /**
35 | * Sets up common test data before each test.
36 | */
37 | @BeforeEach
38 | void setUp() {
39 | sampleAllocationData = new ArrayList<>();
40 | Map<String, Object> node1Data = new HashMap<>();
41 | node1Data.put("node", "nodeA");
42 | node1Data.put("shards", "10");
43 | node1Data.put("disk.indices", "100gb");
44 | sampleAllocationData.add(node1Data);
45 |
46 | Map<String, Object> node2Data = new HashMap<>();
47 | node2Data.put("node", "nodeB");
48 | node2Data.put("shards", "12");
49 | node2Data.put("disk.indices", "120gb");
50 | sampleAllocationData.add(node2Data);
51 | }
52 |
53 | /**
54 | * Tests {@link ShardAllocationToolsService#getShardAllocation()} for a successful call.
55 | *
56 | * @throws IOException if the mocked provider call fails (not expected in this test).
57 | */
58 | @Test
59 | void testGetShardAllocation_Success() throws IOException {
60 | // Arrange
61 | when(mockElasticsearchCatAllocationProvider.getCatAllocation()).thenReturn(sampleAllocationData);
62 |
63 | // Act
64 | List<Map<String, Object>> result = shardAllocationToolsService.getShardAllocation();
65 |
66 | // Assert
67 | assertNotNull(result);
68 | assertEquals(2, result.size());
69 | assertEquals("nodeA", result.get(0).get("node"));
70 | assertEquals("10", result.get(0).get("shards"));
71 | verify(mockElasticsearchCatAllocationProvider, times(1)).getCatAllocation();
72 | }
73 |
74 | /**
75 | * Tests {@link ShardAllocationToolsService#getShardAllocation()} when the provider throws an IOException.
76 | *
77 | * @throws IOException if the mocked provider call fails (expected in this test).
78 | */
79 | @Test
80 | void testGetShardAllocation_ProviderThrowsIOException() throws IOException {
81 | // Arrange
82 | when(mockElasticsearchCatAllocationProvider.getCatAllocation()).thenThrow(new IOException("Simulated Elasticsearch error"));
83 |
84 | // Act & Assert
85 | RuntimeException exception = assertThrows(RuntimeException.class, () -> {
86 | shardAllocationToolsService.getShardAllocation();
87 | });
88 | assertTrue(exception.getMessage().contains("Error retrieving shard allocation information"));
89 | assertTrue(exception.getCause() instanceof IOException);
90 | verify(mockElasticsearchCatAllocationProvider, times(1)).getCatAllocation();
91 | }
92 |
93 | /**
94 | * Tests {@link ShardAllocationToolsService#getShardAllocationForNode(String)} for a successful call.
95 | *
96 | * @throws IOException if the mocked provider call fails (not expected in this test).
97 | */
98 | @Test
99 | void testGetShardAllocationForNode_Success() throws IOException {
100 | // Arrange
101 | String nodeId = "nodeA";
102 | List<Map<String, Object>> nodeSpecificData = new ArrayList<>();
103 | Map<String, Object> nodeData = new HashMap<>();
104 | nodeData.put("node", nodeId);
105 | nodeData.put("shards", "10");
106 | nodeSpecificData.add(nodeData);
107 |
108 | when(mockElasticsearchCatAllocationProvider.getCatAllocation(nodeId)).thenReturn(nodeSpecificData);
109 |
110 | // Act
111 | List<Map<String, Object>> result = shardAllocationToolsService.getShardAllocationForNode(nodeId);
112 |
113 | // Assert
114 | assertNotNull(result);
115 | assertEquals(1, result.size());
116 | assertEquals(nodeId, result.get(0).get("node"));
117 | assertEquals("10", result.get(0).get("shards"));
118 | verify(mockElasticsearchCatAllocationProvider, times(1)).getCatAllocation(nodeId);
119 | }
120 |
121 | /**
122 | * Tests {@link ShardAllocationToolsService#getShardAllocationForNode(String)} when the provider throws an IOException.
123 | *
124 | * @throws IOException if the mocked provider call fails (expected in this test).
125 | */
126 | @Test
127 | void testGetShardAllocationForNode_ProviderThrowsIOException() throws IOException {
128 | // Arrange
129 | String nodeId = "nodeX";
130 | when(mockElasticsearchCatAllocationProvider.getCatAllocation(nodeId)).thenThrow(new IOException("Simulated Elasticsearch error for node"));
131 |
132 | // Act & Assert
133 | RuntimeException exception = assertThrows(RuntimeException.class, () -> {
134 | shardAllocationToolsService.getShardAllocationForNode(nodeId);
135 | });
136 | assertTrue(exception.getMessage().contains("Error retrieving shard allocation information for node " + nodeId));
137 | assertTrue(exception.getCause() instanceof IOException);
138 | verify(mockElasticsearchCatAllocationProvider, times(1)).getCatAllocation(nodeId);
139 | }
140 | }
141 |
```
--------------------------------------------------------------------------------
/infrastructures/elasticsearch/src/main/java/com/silbaram/github/infrastructures/elasticsearch/provider/ElasticsearchClusterStatisticsProvider.java:
--------------------------------------------------------------------------------
```java
1 | package com.silbaram.github.infrastructures.elasticsearch.provider;
2 |
3 | import com.fasterxml.jackson.core.type.TypeReference;
4 | import com.fasterxml.jackson.databind.ObjectMapper;
5 | import org.elasticsearch.client.Request;
6 | import org.elasticsearch.client.Response;
7 | import org.elasticsearch.client.RestClient;
8 | import org.springframework.stereotype.Component;
9 |
10 | import java.io.IOException;
11 | import java.io.InputStream;
12 | import java.util.HashMap;
13 | import java.util.Map;
14 |
15 | @Component
16 | public class ElasticsearchClusterStatisticsProvider {
17 |
18 | private final RestClient restClient;
19 | private final ObjectMapper objectMapper;
20 |
21 | public ElasticsearchClusterStatisticsProvider(RestClient restClient) {
22 | this.restClient = restClient;
23 | this.objectMapper = new ObjectMapper();
24 | }
25 |
26 | /**
27 | * Elasticsearch: /_cluster/stats API
28 | * 클러스터 통계 정보를 조회합니다.
29 | * @return 클러스터 통계 정보를 담은 Map
30 | * @throws IOException API 호출 실패 시
31 | */
32 | @SuppressWarnings("unchecked") // JSON 파싱 시 Map 캐스팅에 대한 경고를 무시합니다.
33 | public Map<String, Object> getClusterStatistics() throws IOException {
34 | Request request = new Request("GET", "/_cluster/stats");
35 | Response response = restClient.performRequest(request);
36 | Map<String, Object> rootJsonMap;
37 | try (InputStream inputStream = response.getEntity().getContent()) {
38 | rootJsonMap = objectMapper.readValue(inputStream, new TypeReference<Map<String, Object>>() {});
39 | }
40 |
41 | Map<String, Object> result = new HashMap<>();
42 |
43 | // 클러스터 정보 (Cluster Info)
44 | Map<String, Object> clusterInfo = new HashMap<>();
45 | clusterInfo.put("name", rootJsonMap.get("cluster_name"));
46 | clusterInfo.put("uuid", rootJsonMap.get("cluster_uuid"));
47 | clusterInfo.put("status", rootJsonMap.get("status")); // JSON 응답의 status 필드 (예: "green", "yellow", "red")
48 | clusterInfo.put("timestamp", rootJsonMap.get("timestamp"));
49 | result.put("cluster", clusterInfo);
50 |
51 | // 노드 정보 (Nodes Info)
52 | Map<String, Object> nodesData = (Map<String, Object>) rootJsonMap.get("nodes");
53 | Map<String, Object> nodesInfo = new HashMap<>();
54 | if (nodesData != null) {
55 | Map<String, Object> countData = (Map<String, Object>) nodesData.get("count");
56 | if (countData != null) {
57 | nodesInfo.put("total", countData.get("total"));
58 | nodesInfo.put("data", countData.get("data"));
59 | nodesInfo.put("master", countData.get("master"));
60 | nodesInfo.put("ingest", countData.get("ingest"));
61 | }
62 |
63 | Map<String, Object> osData = (Map<String, Object>) nodesData.get("os");
64 | if (osData != null) {
65 | Map<String, Object> memData = (Map<String, Object>) osData.get("mem");
66 | if (memData != null) {
67 | nodesInfo.put("mem_used_percent", memData.get("used_percent"));
68 | }
69 | nodesInfo.put("processors", osData.get("available_processors"));
70 | }
71 |
72 | Map<String, Object> jvmData = (Map<String, Object>) nodesData.get("jvm");
73 | if (jvmData != null) {
74 | Map<String, Object> jvmMemData = (Map<String, Object>) jvmData.get("mem");
75 | if (jvmMemData != null) {
76 | // JVM 힙 사용량 계산 (Calculating JVM heap usage)
77 | Object heapUsedBytesObj = jvmMemData.get("heap_used_in_bytes");
78 | Object heapMaxBytesObj = jvmMemData.get("heap_max_in_bytes");
79 |
80 | if (heapUsedBytesObj instanceof Number && heapMaxBytesObj instanceof Number) {
81 | long heapUsedBytes = ((Number) heapUsedBytesObj).longValue();
82 | long heapMaxBytes = ((Number) heapMaxBytesObj).longValue();
83 | double heapUsedPercent = heapMaxBytes > 0 ? (double) heapUsedBytes / heapMaxBytes * 100 : 0.0;
84 | nodesInfo.put("heap_used_bytes", heapUsedBytes);
85 | nodesInfo.put("heap_max_bytes", heapMaxBytes);
86 | nodesInfo.put("heap_used_percent", heapUsedPercent);
87 | } else {
88 | nodesInfo.put("heap_used_bytes", 0L);
89 | nodesInfo.put("heap_max_bytes", 0L);
90 | nodesInfo.put("heap_used_percent", 0.0);
91 | }
92 | }
93 | }
94 | }
95 | result.put("nodes", nodesInfo);
96 |
97 | // 인덱스 정보 (Indices Info)
98 | Map<String, Object> indicesData = (Map<String, Object>) rootJsonMap.get("indices");
99 | Map<String, Object> indicesInfo = new HashMap<>();
100 | if (indicesData != null) {
101 | indicesInfo.put("count", indicesData.get("count"));
102 |
103 | Map<String, Object> shardsData = (Map<String, Object>) indicesData.get("shards");
104 | Map<String, Object> shardsInfo = new HashMap<>();
105 | if (shardsData != null) {
106 | shardsInfo.put("total", shardsData.get("total"));
107 | shardsInfo.put("primaries", shardsData.get("primaries"));
108 | // 'replication' 필드는 _cluster/stats API 응답에 직접적으로 없을 수 있습니다.
109 | // 이전 클라이언트는 이를 계산했을 수 있으나, 여기서는 응답에 있다면 사용하고 없다면 기본값을 설정합니다.
110 | shardsInfo.put("replication", shardsData.getOrDefault("replication", 0.0f)); // Assuming float or default
111 | }
112 | indicesInfo.put("shards", shardsInfo);
113 |
114 | Map<String, Object> docsData = (Map<String, Object>) indicesData.get("docs");
115 | Map<String, Object> docsInfo = new HashMap<>();
116 | if (docsData != null) {
117 | docsInfo.put("count", docsData.get("count"));
118 | docsInfo.put("deleted", docsData.get("deleted"));
119 | }
120 | indicesInfo.put("docs", docsInfo);
121 | }
122 | result.put("indices", indicesInfo);
123 |
124 | return result;
125 | }
126 | }
127 |
```
--------------------------------------------------------------------------------
/infrastructures/elasticsearch/src/test/java/com/silbaram/github/infrastructures/elasticsearch/provider/ElasticsearchClusterStatisticsProviderTest.java:
--------------------------------------------------------------------------------
```java
1 | package com.silbaram.github.infrastructures.elasticsearch.provider;
2 |
3 | import org.apache.http.HttpEntity;
4 | import org.elasticsearch.client.Request;
5 | import org.elasticsearch.client.Response;
6 | import org.elasticsearch.client.RestClient;
7 | import org.junit.jupiter.api.DisplayName;
8 | import org.junit.jupiter.api.Test;
9 | import org.junit.jupiter.api.extension.ExtendWith;
10 | import org.mockito.ArgumentCaptor;
11 | import org.mockito.InjectMocks;
12 | import org.mockito.Mock;
13 | import org.mockito.junit.jupiter.MockitoExtension;
14 |
15 | import java.io.ByteArrayInputStream;
16 | import java.io.IOException;
17 | import java.io.InputStream;
18 | import java.nio.charset.StandardCharsets;
19 | import java.util.Map;
20 |
21 | import static org.junit.jupiter.api.Assertions.*;
22 | import static org.mockito.ArgumentMatchers.any;
23 | import static org.mockito.Mockito.verify;
24 | import static org.mockito.Mockito.when;
25 |
26 | @ExtendWith(MockitoExtension.class) // Mockito 확장 기능 사용
27 | class ElasticsearchClusterStatisticsProviderTest {
28 |
29 | @Mock
30 | private RestClient restClient; // RestClient Mock 객체
31 |
32 | @Mock
33 | private Response mockResponse; // Response Mock 객체
34 |
35 | @Mock
36 | private HttpEntity mockHttpEntity; // HttpEntity Mock 객체
37 |
38 | @InjectMocks
39 | private ElasticsearchClusterStatisticsProvider statisticsProvider; // 테스트 대상 클래스
40 |
41 | @Test
42 | @DisplayName("getClusterStatistics_성공_통계정보반환")
43 | @SuppressWarnings("unchecked") // 테스트 중 Map 캐스팅 경고 무시
44 | void testGetClusterStatistics_Success_ReturnsStatistics() throws IOException {
45 | // given: 테스트용 샘플 JSON 응답 문자열 준비
46 | String sampleJsonResponse = "{\n" +
47 | " \"cluster_name\": \"test_cluster\",\n" +
48 | " \"cluster_uuid\": \"test_uuid\",\n" +
49 | " \"status\": \"green\",\n" +
50 | " \"timestamp\": 1678886400000,\n" +
51 | " \"nodes\": {\n" +
52 | " \"count\": {\n" +
53 | " \"total\": 3,\n" +
54 | " \"data\": 2,\n" +
55 | " \"master\": 1,\n" +
56 | " \"ingest\": 2\n" +
57 | " },\n" +
58 | " \"os\": {\n" +
59 | " \"mem\": {\n" +
60 | " \"used_percent\": 60\n" +
61 | " },\n" +
62 | " \"available_processors\": 8\n" +
63 | " },\n" +
64 | " \"jvm\": {\n" +
65 | " \"mem\": {\n" +
66 | " \"heap_used_in_bytes\": 1073741824,\n" + // 1GB
67 | " \"heap_max_in_bytes\": 2147483648\n" + // 2GB
68 | " }\n" +
69 | " }\n" +
70 | " },\n" +
71 | " \"indices\": {\n" +
72 | " \"count\": 5,\n" +
73 | " \"shards\": {\n" +
74 | " \"total\": 10,\n" +
75 | " \"primaries\": 5,\n" +
76 | " \"replication\": 1.0\n" + // JSON에서 숫자로 제공될 수 있음
77 | " },\n" +
78 | " \"docs\": {\n" +
79 | " \"count\": 10000,\n" +
80 | " \"deleted\": 500\n" +
81 | " }\n" +
82 | " }\n" +
83 | "}";
84 |
85 | InputStream inputStream = new ByteArrayInputStream(sampleJsonResponse.getBytes(StandardCharsets.UTF_8));
86 | when(mockHttpEntity.getContent()).thenReturn(inputStream);
87 | when(mockResponse.getEntity()).thenReturn(mockHttpEntity);
88 | when(restClient.performRequest(any(Request.class))).thenReturn(mockResponse);
89 |
90 | // when: 테스트 대상 메소드 호출
91 | Map<String, Object> result = statisticsProvider.getClusterStatistics();
92 |
93 | // then: 결과 검증
94 | ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class);
95 | verify(restClient).performRequest(requestCaptor.capture());
96 | assertEquals("GET", requestCaptor.getValue().getMethod());
97 | assertEquals("/_cluster/stats", requestCaptor.getValue().getEndpoint());
98 |
99 | assertNotNull(result, "결과 맵은 null이 아니어야 합니다.");
100 |
101 | // 클러스터 정보 검증
102 | Map<String, Object> clusterInfo = (Map<String, Object>) result.get("cluster");
103 | assertNotNull(clusterInfo);
104 | assertEquals("test_cluster", clusterInfo.get("name"));
105 | assertEquals("test_uuid", clusterInfo.get("uuid"));
106 | assertEquals("green", clusterInfo.get("status"));
107 | assertEquals(1678886400000L, clusterInfo.get("timestamp"));
108 |
109 | // 노드 정보 검증
110 | Map<String, Object> nodesInfo = (Map<String, Object>) result.get("nodes");
111 | assertNotNull(nodesInfo);
112 | assertEquals(3, nodesInfo.get("total"));
113 | assertEquals(2, nodesInfo.get("data"));
114 | assertEquals(1, nodesInfo.get("master"));
115 | assertEquals(2, nodesInfo.get("ingest"));
116 | assertEquals(60, nodesInfo.get("mem_used_percent"));
117 | assertEquals(8, nodesInfo.get("processors"));
118 | assertEquals(1073741824L, nodesInfo.get("heap_used_bytes"));
119 | assertEquals(2147483648L, nodesInfo.get("heap_max_bytes"));
120 | assertEquals(50.0, (Double) nodesInfo.get("heap_used_percent"), 0.001, "힙 사용률 계산 검증");
121 |
122 | // 인덱스 정보 검증
123 | Map<String, Object> indicesInfo = (Map<String, Object>) result.get("indices");
124 | assertNotNull(indicesInfo);
125 | assertEquals(5, indicesInfo.get("count"));
126 |
127 | Map<String, Object> shardsInfo = (Map<String, Object>) indicesInfo.get("shards");
128 | assertNotNull(shardsInfo);
129 | assertEquals(10, shardsInfo.get("total"));
130 | assertEquals(5, shardsInfo.get("primaries"));
131 | // API 응답에서 replication이 float (1.0f)으로 설정되어 있으므로 이에 맞게 검증
132 | assertEquals(1.0f, ((Number) shardsInfo.get("replication")).floatValue(), 0.001f, "샤드 복제본 수 검증");
133 |
134 |
135 | Map<String, Object> docsInfo = (Map<String, Object>) indicesInfo.get("docs");
136 | assertNotNull(docsInfo);
137 | assertEquals(10000, docsInfo.get("count"));
138 | assertEquals(500, docsInfo.get("deleted"));
139 | }
140 |
141 | @Test
142 | @DisplayName("getClusterStatistics_IOException발생")
143 | void testGetClusterStatistics_ThrowsIOException() throws IOException {
144 | // given: RestClient.performRequest 호출 시 IOException 발생하도록 Mock 설정
145 | when(restClient.performRequest(any(Request.class))).thenThrow(new IOException("Simulated Network Error"));
146 |
147 | // when & then: getClusterStatistics 호출 시 IOException 발생하는지 검증
148 | IOException exception = assertThrows(IOException.class, () -> {
149 | statisticsProvider.getClusterStatistics();
150 | }, "IOException이 발생해야 합니다.");
151 |
152 | assertEquals("Simulated Network Error", exception.getMessage(), "예외 메시지가 예상과 동일해야 합니다.");
153 | }
154 | }
155 |
```
--------------------------------------------------------------------------------
/infrastructures/elasticsearch/src/test/java/com/silbaram/github/infrastructures/elasticsearch/provider/ElasticsearchIndicesProviderTest.java:
--------------------------------------------------------------------------------
```java
1 | package com.silbaram.github.infrastructures.elasticsearch.provider;
2 |
3 | import org.apache.http.HttpEntity;
4 | import org.elasticsearch.client.Request;
5 | import org.elasticsearch.client.Response;
6 | import org.elasticsearch.client.RestClient;
7 | import org.junit.jupiter.api.DisplayName;
8 | import org.junit.jupiter.api.Test;
9 | import org.junit.jupiter.api.extension.ExtendWith;
10 | import org.mockito.ArgumentCaptor;
11 | import org.mockito.InjectMocks;
12 | import org.mockito.Mock;
13 | import org.mockito.junit.jupiter.MockitoExtension;
14 |
15 | import java.io.ByteArrayInputStream;
16 | import java.io.IOException;
17 | import java.io.InputStream;
18 | import java.nio.charset.StandardCharsets;
19 | import java.util.List;
20 | import java.util.Map;
21 |
22 | import static org.junit.jupiter.api.Assertions.*;
23 | import static org.mockito.ArgumentMatchers.any;
24 | import static org.mockito.Mockito.verify;
25 | import static org.mockito.Mockito.when;
26 |
27 | @ExtendWith(MockitoExtension.class) // Mockito 확장 기능 사용
28 | class ElasticsearchIndicesProviderTest {
29 |
30 | @Mock
31 | private RestClient restClient; // RestClient Mock 객체
32 |
33 | @Mock
34 | private Response mockResponse; // Response Mock 객체
35 |
36 | @Mock
37 | private HttpEntity mockHttpEntity; // HttpEntity Mock 객체
38 |
39 | @InjectMocks
40 | private ElasticsearchIndicesProvider indicesProvider; // 테스트 대상 클래스
41 |
42 | private static final String CAT_INDICES_HEADERS = "health,status,index,docs.count,docs.deleted,pri.store.size,store.size";
43 |
44 | @Test
45 | @DisplayName("getCatIndices_성공_인덱스목록반환")
46 | void testGetCatIndices_Success_ReturnsIndexList() throws IOException {
47 | // given: 테스트용 샘플 JSON 응답
48 | String sampleJsonResponse = "[{\"health\":\"green\", \"status\":\"open\", \"index\":\"index1\", \"docs.count\":\"100\", \"docs.deleted\":\"10\", \"pri.store.size\":\"10mb\", \"store.size\":\"20mb\"}," +
49 | "{\"health\":\"yellow\", \"status\":\"open\", \"index\":\"index2\", \"docs.count\":\"50\", \"docs.deleted\":\"5\", \"pri.store.size\":\"5mb\", \"store.size\":\"10mb\"}]";
50 | InputStream inputStream = new ByteArrayInputStream(sampleJsonResponse.getBytes(StandardCharsets.UTF_8));
51 | when(mockHttpEntity.getContent()).thenReturn(inputStream);
52 | when(mockResponse.getEntity()).thenReturn(mockHttpEntity);
53 | when(restClient.performRequest(any(Request.class))).thenReturn(mockResponse);
54 |
55 | // when: 테스트 대상 메소드 호출
56 | List<Map<String, Object>> result = indicesProvider.getCatIndices();
57 |
58 | // then: 결과 검증
59 | ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class);
60 | verify(restClient).performRequest(requestCaptor.capture());
61 | assertEquals("GET", requestCaptor.getValue().getMethod());
62 | assertEquals("/_cat/indices?format=json&h=" + CAT_INDICES_HEADERS, requestCaptor.getValue().getEndpoint());
63 |
64 | assertNotNull(result, "결과 리스트는 null이 아니어야 합니다.");
65 | assertEquals(2, result.size(), "결과 리스트의 크기가 예상과 다릅니다.");
66 |
67 | Map<String, Object> index1 = result.get(0);
68 | assertEquals("green", index1.get("health"));
69 | assertEquals("open", index1.get("status"));
70 | assertEquals("index1", index1.get("index"));
71 | assertEquals("100", index1.get("docsCount")); // 필드명 매핑 확인
72 | assertEquals("10", index1.get("docsDeleted"));
73 | assertEquals("10mb", index1.get("priStoreSize"));
74 | assertEquals("20mb", index1.get("storeSize"));
75 |
76 | Map<String, Object> index2 = result.get(1);
77 | assertEquals("yellow", index2.get("health"));
78 | assertEquals("index2", index2.get("index"));
79 | assertEquals("50", index2.get("docsCount"));
80 | }
81 |
82 | @Test
83 | @DisplayName("getCatIndicesByName_성공_특정인덱스반환")
84 | void testGetCatIndicesByName_Success_ReturnsSpecificIndex() throws IOException {
85 | // given: 테스트용 샘플 JSON 응답 (단일 인덱스)
86 | String indexName = "my_index";
87 | String sampleJsonResponse = "[{\"health\":\"green\", \"status\":\"open\", \"index\":\"my_index\", \"docs.count\":\"123\", \"docs.deleted\":\"23\", \"pri.store.size\":\"12mb\", \"store.size\":\"24mb\"}]";
88 | InputStream inputStream = new ByteArrayInputStream(sampleJsonResponse.getBytes(StandardCharsets.UTF_8));
89 | when(mockHttpEntity.getContent()).thenReturn(inputStream);
90 | when(mockResponse.getEntity()).thenReturn(mockHttpEntity);
91 | when(restClient.performRequest(any(Request.class))).thenReturn(mockResponse);
92 |
93 | // when: 테스트 대상 메소드 호출
94 | List<Map<String, Object>> result = indicesProvider.getCatIndicesByName(indexName);
95 |
96 | // then: 결과 검증
97 | ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class);
98 | verify(restClient).performRequest(requestCaptor.capture());
99 | assertEquals("GET", requestCaptor.getValue().getMethod());
100 | assertEquals("/_cat/indices/" + indexName + "?format=json&h=" + CAT_INDICES_HEADERS, requestCaptor.getValue().getEndpoint());
101 |
102 | assertNotNull(result, "결과 리스트는 null이 아니어야 합니다.");
103 | assertEquals(1, result.size(), "결과 리스트에는 하나의 인덱스만 포함되어야 합니다.");
104 |
105 | Map<String, Object> index = result.get(0);
106 | assertEquals("green", index.get("health"));
107 | assertEquals("open", index.get("status"));
108 | assertEquals("my_index", index.get("index"));
109 | assertEquals("123", index.get("docsCount"));
110 | assertEquals("23", index.get("docsDeleted"));
111 | assertEquals("12mb", index.get("priStoreSize"));
112 | assertEquals("24mb", index.get("storeSize"));
113 | }
114 |
115 | @Test
116 | @DisplayName("getCatIndices_IOException발생")
117 | void testGetCatIndices_ThrowsIOException() throws IOException {
118 | // given: RestClient.performRequest 호출 시 IOException 발생하도록 Mock 설정
119 | when(restClient.performRequest(any(Request.class))).thenThrow(new IOException("Simulated Network Error"));
120 |
121 | // when & then: getCatIndices 호출 시 IOException 발생하는지 검증
122 | IOException exception = assertThrows(IOException.class, () -> {
123 | indicesProvider.getCatIndices();
124 | }, "IOException이 발생해야 합니다.");
125 | assertEquals("Simulated Network Error", exception.getMessage());
126 | }
127 |
128 | @Test
129 | @DisplayName("getCatIndicesByName_IOException발생")
130 | void testGetCatIndicesByName_ThrowsIOException() throws IOException {
131 | // given: RestClient.performRequest 호출 시 IOException 발생하도록 Mock 설정
132 | String indexName = "error_index";
133 | when(restClient.performRequest(any(Request.class))).thenThrow(new IOException("Simulated Network Error for specific index"));
134 |
135 | // when & then: getCatIndicesByName 호출 시 IOException 발생하는지 검증
136 | IOException exception = assertThrows(IOException.class, () -> {
137 | indicesProvider.getCatIndicesByName(indexName);
138 | }, "IOException이 발생해야 합니다.");
139 | assertEquals("Simulated Network Error for specific index", exception.getMessage());
140 | }
141 | }
142 |
```
--------------------------------------------------------------------------------
/infrastructures/elasticsearch/src/test/java/com/silbaram/github/infrastructures/elasticsearch/provider/ElasticsearchAliasesProviderTest.java:
--------------------------------------------------------------------------------
```java
1 | package com.silbaram.github.infrastructures.elasticsearch.provider;
2 |
3 | import org.apache.http.HttpEntity;
4 | import org.elasticsearch.client.Request;
5 | import org.elasticsearch.client.Response;
6 | import org.elasticsearch.client.RestClient;
7 | import org.junit.jupiter.api.DisplayName;
8 | import org.junit.jupiter.api.Test;
9 | import org.junit.jupiter.api.extension.ExtendWith;
10 | import org.mockito.ArgumentCaptor;
11 | import org.mockito.InjectMocks;
12 | import org.mockito.Mock;
13 | import org.mockito.junit.jupiter.MockitoExtension;
14 |
15 | import java.io.ByteArrayInputStream;
16 | import java.io.IOException;
17 | import java.io.InputStream;
18 | import java.nio.charset.StandardCharsets;
19 | import java.util.List;
20 | import java.util.Map;
21 |
22 | import static org.junit.jupiter.api.Assertions.*;
23 | import static org.mockito.Mockito.*;
24 |
25 | @ExtendWith(MockitoExtension.class) // Mockito 확장 기능 사용
26 | class ElasticsearchAliasesProviderTest {
27 |
28 | @Mock
29 | private RestClient restClient; // RestClient Mock 객체
30 |
31 | @Mock
32 | private Response mockResponse; // Response Mock 객체
33 |
34 | @Mock
35 | private HttpEntity mockHttpEntity; // HttpEntity Mock 객체
36 |
37 | @InjectMocks
38 | private ElasticsearchAliasesProvider aliasesProvider; // 테스트 대상 클래스
39 |
40 | @Test
41 | @DisplayName("getCatAliases_성공_별칭목록반환_숨김항목필터링")
42 | void testGetCatAliases_Success_ReturnsAliases_FiltersHidden() throws IOException {
43 | // given
44 | String jsonResponse = "[{\"alias\":\"alias1\", \"index\":\"index1\", \"filter\":\"-\", \"routing.index\":\"ri1\", \"routing.search\":\"rs1\", \"is_write_index\":\"true\"}," +
45 | "{\"alias\":\".hidden_alias\", \"index\":\"index2\"}]";
46 | InputStream inputStream = new ByteArrayInputStream(jsonResponse.getBytes(StandardCharsets.UTF_8));
47 | when(mockHttpEntity.getContent()).thenReturn(inputStream);
48 | when(mockResponse.getEntity()).thenReturn(mockHttpEntity);
49 | when(restClient.performRequest(any(Request.class))).thenReturn(mockResponse);
50 |
51 | // when
52 | List<Map<String, Object>> result = aliasesProvider.getCatAliases();
53 |
54 | // then
55 | ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class);
56 | verify(restClient).performRequest(requestCaptor.capture());
57 | assertEquals("GET", requestCaptor.getValue().getMethod());
58 | assertEquals("/_cat/aliases?format=json", requestCaptor.getValue().getEndpoint());
59 |
60 | assertNotNull(result);
61 | assertEquals(1, result.size(), "숨김 별칭은 필터링되어야 합니다.");
62 | Map<String, Object> aliasMap = result.get(0);
63 | assertEquals("alias1", aliasMap.get("alias"));
64 | assertEquals("index1", aliasMap.get("index"));
65 | assertEquals("-", aliasMap.get("filter"));
66 | assertEquals("ri1", aliasMap.get("routingIndex"), "routing.index 필드가 routingIndex로 매핑되어야 합니다.");
67 | assertEquals("rs1", aliasMap.get("routingSearch"), "routing.search 필드가 routingSearch로 매핑되어야 합니다.");
68 | assertEquals("true", aliasMap.get("isWriteIndex"), "is_write_index 필드가 isWriteIndex로 매핑되어야 합니다.");
69 | }
70 |
71 | @Test
72 | @DisplayName("getCatAliasesByName_성공_특정별칭반환")
73 | void testGetCatAliasesByName_Success_ReturnsSpecificAlias() throws IOException {
74 | // given
75 | String aliasName = "my_alias";
76 | String jsonResponse = "[{\"alias\":\"my_alias\", \"index\":\"my_index\", \"filter\":\"*\", \"routing.index\":\"my_ri\", \"routing.search\":\"my_rs\", \"is_write_index\":\"false\"}]";
77 | InputStream inputStream = new ByteArrayInputStream(jsonResponse.getBytes(StandardCharsets.UTF_8));
78 | when(mockHttpEntity.getContent()).thenReturn(inputStream);
79 | when(mockResponse.getEntity()).thenReturn(mockHttpEntity);
80 | when(restClient.performRequest(any(Request.class))).thenReturn(mockResponse);
81 |
82 | // when
83 | List<Map<String, Object>> result = aliasesProvider.getCatAliasesByName(aliasName);
84 |
85 | // then
86 | ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class);
87 | verify(restClient).performRequest(requestCaptor.capture());
88 | assertEquals("GET", requestCaptor.getValue().getMethod());
89 | assertEquals("/_cat/aliases/" + aliasName + "?format=json", requestCaptor.getValue().getEndpoint());
90 |
91 | assertNotNull(result);
92 | assertEquals(1, result.size());
93 | Map<String, Object> aliasMap = result.get(0);
94 | assertEquals("my_alias", aliasMap.get("alias"));
95 | assertEquals("my_index", aliasMap.get("index"));
96 | assertEquals("*", aliasMap.get("filter"));
97 | assertEquals("my_ri", aliasMap.get("routingIndex"));
98 | assertEquals("my_rs", aliasMap.get("routingSearch"));
99 | assertEquals("false", aliasMap.get("isWriteIndex"));
100 | }
101 |
102 | @Test
103 | @DisplayName("getCatAliasesByName_성공_숨김별칭필터링")
104 | void testGetCatAliasesByName_Success_FiltersHiddenAlias() throws IOException {
105 | // given
106 | String hiddenAliasName = ".my_hidden_alias";
107 | String jsonResponse = "[{\"alias\":\".my_hidden_alias\", \"index\":\"my_index\"}]";
108 | InputStream inputStream = new ByteArrayInputStream(jsonResponse.getBytes(StandardCharsets.UTF_8));
109 | when(mockHttpEntity.getContent()).thenReturn(inputStream);
110 | when(mockResponse.getEntity()).thenReturn(mockHttpEntity);
111 | when(restClient.performRequest(any(Request.class))).thenReturn(mockResponse);
112 |
113 | // when
114 | List<Map<String, Object>> result = aliasesProvider.getCatAliasesByName(hiddenAliasName);
115 |
116 | // then
117 | ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class);
118 | verify(restClient).performRequest(requestCaptor.capture());
119 | assertEquals("GET", requestCaptor.getValue().getMethod());
120 | assertEquals("/_cat/aliases/" + hiddenAliasName + "?format=json", requestCaptor.getValue().getEndpoint());
121 |
122 | assertNotNull(result);
123 | assertTrue(result.isEmpty(), "숨김 별칭은 필터링되어 목록이 비어있어야 합니다.");
124 | }
125 |
126 | @Test
127 | @DisplayName("getCatAliases_IOException발생")
128 | void testGetCatAliases_ThrowsIOException() throws IOException {
129 | // given
130 | when(restClient.performRequest(any(Request.class))).thenThrow(new IOException("Simulated IO Error"));
131 |
132 | // when & then
133 | assertThrows(IOException.class, () -> {
134 | aliasesProvider.getCatAliases();
135 | }, "IOException이 발생해야 합니다.");
136 | }
137 |
138 | @Test
139 | @DisplayName("getCatAliasesByName_IOException발생")
140 | void testGetCatAliasesByName_ThrowsIOException() throws IOException {
141 | // given
142 | String aliasName = "some_alias";
143 | when(restClient.performRequest(any(Request.class))).thenThrow(new IOException("Simulated IO Error"));
144 |
145 | // when & then
146 | assertThrows(IOException.class, () -> {
147 | aliasesProvider.getCatAliasesByName(aliasName);
148 | }, "IOException이 발생해야 합니다.");
149 | }
150 | }
151 |
```