blob: e6d8d26309e657a4c0243aebf2408f4adc90127d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.skywalking.oap.server.core.storage;
import com.google.common.base.Joiner;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.skywalking.oap.server.core.Const;
import org.apache.skywalking.oap.server.library.util.StringUtil;
/**
* StorageID represents an identification for the metric or the record.
* Typically, an ID is composited by two parts
* 1. Time bucket based on downsampling.
* 2. The encoded entity ID, such as Service ID.
*
* In the SQL database and ElasticSearch, the string ID is preferred.
* In the BanyanDB, time series and entity ID(series ID) would be treated separately.
*
* @since 9.4.0 StorageID replaced the `string id()` method in the StorageData. An object-oriented ID provides a more
* friendly interface for various database implementation.
*/
@EqualsAndHashCode(of = {
"fragments"
})
public class StorageID {
private final List<Fragment> fragments;
/**
* Once the storage ID was {@link #build()} or {@link #read()},
* this object would switch to the sealed status, no more append is allowed.
*/
private boolean sealed = false;
/**
* The string ID would only be built once.
*/
private String builtID;
public StorageID() {
fragments = new ArrayList<>(2);
}
public StorageID append(String name, String value) {
if (StringUtil.isBlank(name)) {
throw new IllegalArgumentException("The name of storage ID should not be null or empty.");
}
if (sealed) {
throw new IllegalStateException("The storage ID is sealed. Can't append a new fragment, name=" + name);
}
fragments.add(new Fragment(new String[] {name}, String.class, false, value));
return this;
}
public StorageID append(String name, long value) {
if (StringUtil.isBlank(name)) {
throw new IllegalArgumentException("The name of storage ID should not be null or empty.");
}
if (sealed) {
throw new IllegalStateException("The storage ID is sealed. Can't append a new fragment, name=" + name);
}
fragments.add(new Fragment(new String[] {name}, Long.class, false, value));
return this;
}
public StorageID append(String name, int value) {
if (StringUtil.isBlank(name)) {
throw new IllegalArgumentException("The name of storage ID should not be null or empty.");
}
if (sealed) {
throw new IllegalStateException("The storage ID is sealed. Can't append a new fragment, name=" + name);
}
fragments.add(new Fragment(new String[] {name}, Integer.class, false, value));
return this;
}
public StorageID appendMutant(String[] source, long value) {
if (sealed) {
throw new IllegalStateException("The storage ID is sealed. Can't append a new fragment, source=" + Arrays.toString(source));
}
fragments.add(new Fragment(source, Long.class, true, value));
return this;
}
public StorageID appendMutant(final String[] source, final String value) {
if (sealed) {
throw new IllegalStateException("The storage ID is sealed. Can't append a new fragment, source=" + Arrays.toString(source));
}
fragments.add(new Fragment(source, String.class, true, value));
return this;
}
/**
* @return the string ID concatenating the values of {@link #fragments} by the underline(_).
*/
public String build() {
sealed = true;
if (builtID == null) {
builtID = Joiner.on(Const.ID_CONNECTOR).join(fragments);
}
return builtID;
}
/**
* @return a read-only list to avoid unexpected change for metric ID.
*/
public List<Fragment> read() {
sealed = true;
return Collections.unmodifiableList(fragments);
}
@RequiredArgsConstructor
@Getter
@EqualsAndHashCode(of = {
"name",
"value"
}, doNotUseGetters = true)
public static class Fragment {
/**
* The column name of the value, or the original column names of the mutate value.
*
* The names could be
* 1. Always one column if this is not {@link #mutate} and from a certain persistent column.
* 2. Be null if {@link #mutate} is true and no relative column, such as the original value is not in
* the persistence.
* 3. One or multi-values if the value is built through a symmetrical or asymmetrical encoding algorithm.
*/
private final String[] name;
/**
* Represent the class type of the {@link #value}.
*/
private final Class<?> type;
/**
* If true, the field was from {@link #name}, and value is changed by internal rules.
* Such as time bucket downsampling, use a day-level time-bucket to build the ID for a minute dimension metric.
*/
private final boolean mutate;
private final Object value;
public Optional<String[]> getName() {
return Optional.ofNullable(name);
}
@Override
public String toString() {
return value.toString();
}
}
}