|  | 
|  | 1 | +/* | 
|  | 2 | + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | 
|  | 3 | + * or more contributor license agreements. Licensed under the Elastic License | 
|  | 4 | + * 2.0; you may not use this file except in compliance with the Elastic License | 
|  | 5 | + * 2.0. | 
|  | 6 | + */ | 
|  | 7 | +package org.elasticsearch.xpack.textstructure.rest; | 
|  | 8 | + | 
|  | 9 | +import com.carrotsearch.randomizedtesting.annotations.Name; | 
|  | 10 | +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; | 
|  | 11 | + | 
|  | 12 | +import org.apache.http.entity.ContentType; | 
|  | 13 | +import org.apache.http.entity.StringEntity; | 
|  | 14 | +import org.elasticsearch.client.Request; | 
|  | 15 | +import org.elasticsearch.client.Response; | 
|  | 16 | +import org.elasticsearch.test.cluster.ElasticsearchCluster; | 
|  | 17 | +import org.elasticsearch.test.cluster.local.distribution.DistributionType; | 
|  | 18 | +import org.elasticsearch.test.rest.ESRestTestCase; | 
|  | 19 | +import org.junit.ClassRule; | 
|  | 20 | + | 
|  | 21 | +import java.io.IOException; | 
|  | 22 | +import java.util.Arrays; | 
|  | 23 | +import java.util.List; | 
|  | 24 | +import java.util.Map; | 
|  | 25 | + | 
|  | 26 | +import static org.hamcrest.Matchers.containsInAnyOrder; | 
|  | 27 | +import static org.hamcrest.Matchers.containsString; | 
|  | 28 | +import static org.hamcrest.Matchers.equalTo; | 
|  | 29 | +import static org.hamcrest.Matchers.hasKey; | 
|  | 30 | + | 
|  | 31 | +public class TextStructureTimestampFormatsIT extends ESRestTestCase { | 
|  | 32 | + | 
|  | 33 | +    public static final String[] ISO_08601_JAVA_FORMATS = new String[] { "yyyy-MM-dd HH:mm:ss" }; | 
|  | 34 | +    public static final String ISO_08601_TIMESTAMP_GROK_PATTERN = "%{TIMESTAMP_ISO8601:timestamp}"; | 
|  | 35 | + | 
|  | 36 | +    public static final String[] TIMESTAMP_YMD_JAVA_FORMATS = new String[] { | 
|  | 37 | +        "yyyy/MM/dd HH:mm:ss", | 
|  | 38 | +        "yyyy.MM.dd HH:mm:ss", | 
|  | 39 | +        "yyyy-MM-dd HH:mm:ss" }; | 
|  | 40 | +    public static final String TIMESTAMP_YMD_TIMESTAMP_GROK_PATTERN = "%{TIMESTAMP_YMD:timestamp}"; | 
|  | 41 | + | 
|  | 42 | +    public static final String[] MONTH_EXPLICIT_NAME_JAVA_FORMATS = new String[] { "MMM d, yyyy" }; | 
|  | 43 | + | 
|  | 44 | +    private final String ecsCompatibility; | 
|  | 45 | + | 
|  | 46 | +    @ClassRule | 
|  | 47 | +    public static ElasticsearchCluster cluster = ElasticsearchCluster.local() | 
|  | 48 | +        .distribution(DistributionType.DEFAULT) | 
|  | 49 | +        .module("x-pack-text-structure") | 
|  | 50 | +        .setting("xpack.security.enabled", "false") | 
|  | 51 | +        .build(); | 
|  | 52 | + | 
|  | 53 | +    public TextStructureTimestampFormatsIT(@Name("ecs_compatibility") String ecsCompatibility) { | 
|  | 54 | +        this.ecsCompatibility = ecsCompatibility; | 
|  | 55 | +    } | 
|  | 56 | + | 
|  | 57 | +    @Override | 
|  | 58 | +    protected String getTestRestCluster() { | 
|  | 59 | +        return cluster.getHttpAddresses(); | 
|  | 60 | +    } | 
|  | 61 | + | 
|  | 62 | +    @ParametersFactory | 
|  | 63 | +    public static Iterable<Object[]> parameters() { | 
|  | 64 | +        return Arrays.asList(new Object[] { "v1" }, new Object[] { "disabled" }); | 
|  | 65 | +    } | 
|  | 66 | + | 
|  | 67 | +    public void testTimestampYearYmdSlashFormat() throws IOException { | 
|  | 68 | +        // use a multi-line sample to ensure we are detecting ndjson format | 
|  | 69 | +        Map<String, Object> responseMap = executeAndVerifyRequest(""" | 
|  | 70 | +            "2025/07/10 10:30:35" | 
|  | 71 | +            "2025/07/10 10:31:42" | 
|  | 72 | +            "2025/07/10 10:32:15" | 
|  | 73 | +            """, ecsCompatibility); | 
|  | 74 | +        verifyTimestampDetected(responseMap, "date"); | 
|  | 75 | +        verifyTimestampFormat(responseMap, TIMESTAMP_YMD_TIMESTAMP_GROK_PATTERN, TIMESTAMP_YMD_JAVA_FORMATS); | 
|  | 76 | +    } | 
|  | 77 | + | 
|  | 78 | +    public void testTimestampYearYmdSlashFormat_WithDotAndMillis() throws IOException { | 
|  | 79 | +        // use a multi-line sample to ensure we are detecting ndjson format | 
|  | 80 | +        Map<String, Object> responseMap = executeAndVerifyRequest(""" | 
|  | 81 | +            "2025/07/10 10:30:35.123" | 
|  | 82 | +            "2025/07/10 10:31:42.123" | 
|  | 83 | +            "2025/07/10 10:32:15.123" | 
|  | 84 | +            """, ecsCompatibility); | 
|  | 85 | +        verifyTimestampDetected(responseMap, "date"); | 
|  | 86 | +        verifyTimestampFormat( | 
|  | 87 | +            responseMap, | 
|  | 88 | +            TIMESTAMP_YMD_TIMESTAMP_GROK_PATTERN, | 
|  | 89 | +            "yyyy/MM/dd HH:mm:ss.SSS", | 
|  | 90 | +            "yyyy.MM.dd HH:mm:ss.SSS", | 
|  | 91 | +            "yyyy-MM-dd HH:mm:ss.SSS" | 
|  | 92 | +        ); | 
|  | 93 | +    } | 
|  | 94 | + | 
|  | 95 | +    public void testTimestampYearYmdSlashFormat_WithSlashAndNanos() throws IOException { | 
|  | 96 | +        // use a multi-line sample to ensure we are detecting ndjson format | 
|  | 97 | +        Map<String, Object> responseMap = executeAndVerifyRequest(""" | 
|  | 98 | +            "2025/07/10 10:30:35,123456789" | 
|  | 99 | +            "2025/07/10 10:31:42,123456789" | 
|  | 100 | +            "2025/07/10 10:32:15,123456789" | 
|  | 101 | +            """, ecsCompatibility); | 
|  | 102 | +        verifyTimestampDetected(responseMap, "date_nanos"); | 
|  | 103 | +        verifyTimestampFormat( | 
|  | 104 | +            responseMap, | 
|  | 105 | +            TIMESTAMP_YMD_TIMESTAMP_GROK_PATTERN, | 
|  | 106 | +            "yyyy/MM/dd HH:mm:ss,SSSSSSSSS", | 
|  | 107 | +            "yyyy.MM.dd HH:mm:ss,SSSSSSSSS", | 
|  | 108 | +            "yyyy-MM-dd HH:mm:ss,SSSSSSSSS" | 
|  | 109 | +        ); | 
|  | 110 | +    } | 
|  | 111 | + | 
|  | 112 | +    public void testTimestampYearYmdDotFormat() throws IOException { | 
|  | 113 | +        // use a multi-line sample to ensure we are detecting ndjson format | 
|  | 114 | +        Map<String, Object> responseMap = executeAndVerifyRequest(""" | 
|  | 115 | +            "2025.07.10 10:30:35" | 
|  | 116 | +            "2025.07.10 10:31:42" | 
|  | 117 | +            "2025.07.10 10:32:15" | 
|  | 118 | +            """, ecsCompatibility); | 
|  | 119 | +        verifyTimestampDetected(responseMap, "date"); | 
|  | 120 | +        verifyTimestampFormat(responseMap, TIMESTAMP_YMD_TIMESTAMP_GROK_PATTERN, TIMESTAMP_YMD_JAVA_FORMATS); | 
|  | 121 | +    } | 
|  | 122 | + | 
|  | 123 | +    public void testIso08601TimestampFormat() throws IOException { | 
|  | 124 | +        // use a multi-line sample to ensure we are detecting ndjson format | 
|  | 125 | +        Map<String, Object> responseMap = executeAndVerifyRequest(""" | 
|  | 126 | +            "2025-07-10 10:30:35" | 
|  | 127 | +            "2025-07-10 10:31:42" | 
|  | 128 | +            "2025-07-10 10:32:15" | 
|  | 129 | +            """, ecsCompatibility); | 
|  | 130 | +        verifyTimestampDetected(responseMap, "date"); | 
|  | 131 | +        // ISO_8601 should have higher priority than TIMESTAMP_YMD | 
|  | 132 | +        verifyTimestampFormat(responseMap, ISO_08601_TIMESTAMP_GROK_PATTERN, ISO_08601_JAVA_FORMATS); | 
|  | 133 | +    } | 
|  | 134 | + | 
|  | 135 | +    public void testMonthExplicitNameFormat() throws IOException { | 
|  | 136 | +        // use a multi-line sample to ensure we are detecting ndjson format | 
|  | 137 | +        Map<String, Object> responseMap = executeAndVerifyRequest(""" | 
|  | 138 | +            "Aug 9, 2025" | 
|  | 139 | +            "Aug 10, 2025" | 
|  | 140 | +            "Aug 11, 2025" | 
|  | 141 | +            """, ecsCompatibility); | 
|  | 142 | +        verifyTimestampDetected(responseMap, "date"); | 
|  | 143 | +        verifyTimestampFormat(responseMap, "CUSTOM_TIMESTAMP", MONTH_EXPLICIT_NAME_JAVA_FORMATS); | 
|  | 144 | +    } | 
|  | 145 | + | 
|  | 146 | +    private static Map<String, Object> executeAndVerifyRequest(String sample, String ecsCompatibility) throws IOException { | 
|  | 147 | +        Request request = new Request("POST", "/_text_structure/find_structure"); | 
|  | 148 | +        request.addParameter("ecs_compatibility", ecsCompatibility); | 
|  | 149 | +        request.setEntity(new StringEntity(sample, ContentType.APPLICATION_JSON)); | 
|  | 150 | +        Response response = client().performRequest(request); | 
|  | 151 | +        assertOK(response); | 
|  | 152 | +        return entityAsMap(response); | 
|  | 153 | +    } | 
|  | 154 | + | 
|  | 155 | +    private static void verifyTimestampDetected(Map<String, Object> responseMap, String expectedType) { | 
|  | 156 | +        @SuppressWarnings("unchecked") | 
|  | 157 | +        Map<String, Object> mappings = (Map<String, Object>) responseMap.get("mappings"); | 
|  | 158 | +        assertThat(mappings, hasKey("properties")); | 
|  | 159 | +        @SuppressWarnings("unchecked") | 
|  | 160 | +        Map<String, Object> properties = (Map<String, Object>) mappings.get("properties"); | 
|  | 161 | +        assertThat(properties, hasKey("@timestamp")); | 
|  | 162 | +        @SuppressWarnings("unchecked") | 
|  | 163 | +        Map<String, Object> timestamp = (Map<String, Object>) properties.get("@timestamp"); | 
|  | 164 | +        assertThat(timestamp.get("type"), equalTo(expectedType)); | 
|  | 165 | +    } | 
|  | 166 | + | 
|  | 167 | +    private static void verifyTimestampFormat(Map<String, Object> responseMap, String expectedGrokPattern, String... expectedJavaFormats) { | 
|  | 168 | +        assertThat(responseMap, hasKey("java_timestamp_formats")); | 
|  | 169 | +        @SuppressWarnings("unchecked") | 
|  | 170 | +        List<String> javaTimestampFormats = (List<String>) responseMap.get("java_timestamp_formats"); | 
|  | 171 | +        assertThat(javaTimestampFormats, containsInAnyOrder(expectedJavaFormats)); | 
|  | 172 | +        String grokPattern = (String) responseMap.get("grok_pattern"); | 
|  | 173 | +        assertThat(grokPattern, containsString(expectedGrokPattern)); | 
|  | 174 | +    } | 
|  | 175 | +} | 
0 commit comments