diff --git a/src/main/java/com/eactive/eai/common/circuitBreaker/CircuitBreakerManager.java b/src/main/java/com/eactive/eai/common/circuitBreaker/CircuitBreakerManager.java index 1b59209..4dce347 100644 --- a/src/main/java/com/eactive/eai/common/circuitBreaker/CircuitBreakerManager.java +++ b/src/main/java/com/eactive/eai/common/circuitBreaker/CircuitBreakerManager.java @@ -6,6 +6,11 @@ import com.eactive.eai.common.lifecycle.LifecycleListener; import com.eactive.eai.common.lifecycle.LifecycleSupport; import com.eactive.eai.common.property.PropManager; import com.eactive.eai.common.util.Logger; +import com.eactive.eai.custom.alarm.AlarmStateManager; +import com.eactive.eai.custom.alarm.condition.AlarmCondition; +import com.eactive.eai.custom.alarm.event.AlarmEvent; +import com.eactive.eai.custom.alarm.key.CoreAlarmKey; + import io.github.resilience4j.circuitbreaker.CircuitBreaker; import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; @@ -71,6 +76,28 @@ public class CircuitBreakerManager implements Lifecycle { circuitBreakerRegistry = CircuitBreakerRegistry.of(config); TaggedCircuitBreakerMetrics.ofCircuitBreakerRegistry(circuitBreakerRegistry).bindTo(meterRegistry); + + circuitBreakerRegistry.getEventPublisher().onEntryAdded(event -> { + CircuitBreaker cb = event.getAddedEntry(); + String apiId = cb.getName(); + + // 여기서 상태 변경 리스너를 한 번만 등록 + cb.getEventPublisher().onStateTransition(stateEvent -> { + CircuitBreaker.State from = stateEvent.getStateTransition().getFromState(); + CircuitBreaker.State to = stateEvent.getStateTransition().getToState(); + + AlarmEvent alarmEvent = AlarmEvent.builder().key(CoreAlarmKey.CircuitBreaker) + .condition(AlarmCondition.builder().build()) + .message(String.format("CircuitBreaker state changed. apiId=%s, %s -> %s", apiId, from, to)) + .build(); + + AlarmStateManager.getAlarmStateManager().onEvnet(alarmEvent); + + logger.warn("CircuitBreaker state changed. apiId={}, {} -> {}", apiId, from, to); + }); + + logger.info("Registered StateTransition listener for: {}", apiId); + }); logger.info("init CircuitBreaker - "+config); } diff --git a/src/main/java/com/eactive/eai/common/util/Jsons.java b/src/main/java/com/eactive/eai/common/util/Jsons.java new file mode 100644 index 0000000..a9379f4 --- /dev/null +++ b/src/main/java/com/eactive/eai/common/util/Jsons.java @@ -0,0 +1,10 @@ +package com.eactive.eai.common.util; + +import com.google.gson.Gson; + +public final class Jsons { + + private Jsons() {} + + public static final Gson GSON = new Gson(); +} \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/custom/alarm/AlarmService.java b/src/main/java/com/eactive/eai/custom/alarm/AlarmService.java new file mode 100644 index 0000000..7f26e67 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/AlarmService.java @@ -0,0 +1,132 @@ +package com.eactive.eai.custom.alarm; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; + +import org.springframework.stereotype.Service; + +import com.eactive.eai.common.property.PropManager; +import com.eactive.eai.common.util.Jsons; +import com.eactive.eai.common.util.Logger; +import com.eactive.eai.custom.alarm.key.AlarmKey; +import com.eactive.eai.custom.alarm.key.DynamicAlarmKey; +import com.eactive.eai.custom.alarm.policy.AlarmPolicy; +import com.eactive.eai.custom.alarm.policy.CounterPolicy; +import com.eactive.eai.custom.alarm.policy.SendPolicy; +import com.eactive.eai.custom.alarm.policy.TimerPolicy; +import com.google.gson.JsonObject; + +@Service +public class AlarmService { + + protected static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); + + final static String ALARM_GROUP = "Alarm"; + final static String ALARM_KEY = "{ALARM}"; + final static String ALARM_RECIVER = "{RECIVER}"; + final static String ALARM_EABLE = "enable.alarm"; + + public Map getAlarmPolicy() { + + Map policyMap = new HashMap(); + + try { + + Properties props = PropManager.getInstance().getProperties(ALARM_GROUP); + + props.forEach((key, value) -> { + + String keyName = (String) key; + String configStr = (String) value; + + if (keyName.contains(ALARM_KEY)) { + + AlarmPolicy newPolicy = buildAlarmPolicy(keyName, configStr); + policyMap.put(newPolicy.getAlarmKey(), newPolicy); + + } + }); + + } catch (Exception e) { + logger.warn("getAlarmPolicy fail.", e); + return Collections.EMPTY_MAP; + } + + return policyMap; + } + + public boolean isAlarmEnabled() { + return Optional.ofNullable(PropManager.getInstance().getProperty(ALARM_GROUP, ALARM_EABLE)) + .map(Boolean::valueOf).orElse(false); + } + + private AlarmPolicy buildAlarmPolicy(String key, String configJsonStr) { + + JsonObject alarmConfig = Jsons.GSON.fromJson(configJsonStr, JsonObject.class); + + String policyType = alarmConfig.get("type").getAsString(); + int thresholdCount = Optional.ofNullable(alarmConfig.get("thresholdCount")).map(i -> i.getAsInt()).orElse(5); + int timeWindowSec = Optional.ofNullable(alarmConfig.get("timeWindowSec")).map(i -> i.getAsInt()).orElse(60); + + AlarmPolicy alarmPolicy; + + if ("count".equals(policyType)) { + alarmPolicy = CounterPolicy.builder().alarmKey(new DynamicAlarmKey(key)).thresholdCount(thresholdCount) + .build(); + } else { + alarmPolicy = TimerPolicy.builder().alarmKey(new DynamicAlarmKey(key)).timeWindowMs(timeWindowSec * 1000) + .build(); + } + + return alarmPolicy; + + } + + public Map getSendPolicy() { + + Map policyMap = new HashMap(); + + try { + + Properties props = PropManager.getInstance().getProperties(ALARM_GROUP); + + props.forEach((key, value) -> { + String keyName = (String) key; + String configStr = (String) value; + + if (!keyName.contains(ALARM_RECIVER)) + return; + + SendPolicy newPolicy = buildSendPolicy(keyName, configStr); + + policyMap.put(newPolicy.getAlarmKey(), newPolicy); + + }); + + } catch (Exception e) { + logger.warn("getSendPolicy fail.", e); + return Collections.EMPTY_MAP; + } + + return policyMap; + } + + private SendPolicy buildSendPolicy(String key, String configStr) { + + List reciverList = Optional.ofNullable(configStr).map(t -> Arrays.asList(t.split(","))) + .orElse(Collections.EMPTY_LIST); + + key = key.replace(ALARM_RECIVER, "").trim() + "{ALARM}"; + + SendPolicy sendPolicy = new SendPolicy(key, reciverList); + + return sendPolicy; + + } +} diff --git a/src/main/java/com/eactive/eai/custom/alarm/AlarmStateManager.java b/src/main/java/com/eactive/eai/custom/alarm/AlarmStateManager.java new file mode 100644 index 0000000..719a204 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/AlarmStateManager.java @@ -0,0 +1,145 @@ +package com.eactive.eai.custom.alarm; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.eactive.eai.common.util.ApplicationContextProvider; +import com.eactive.eai.common.util.Logger; +import com.eactive.eai.custom.alarm.event.AlarmEvent; +import com.eactive.eai.custom.alarm.key.AlarmKey; +import com.eactive.eai.custom.alarm.policy.AlarmPolicy; +import com.eactive.eai.custom.alarm.policy.SendPolicy; +import com.eactive.eai.custom.alarm.ums.UmsService; +import com.eactive.eai.custom.alarm.ums.payload.ObpMonitoringUmsTemplate; +import com.eactive.eai.custom.alarm.ums.payload.UmsRequestPayload; + +@Component +public class AlarmStateManager { + + List registerAlarmList = new ArrayList(); + + private final ScheduledExecutorService scheduler; + + protected static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); + + private static AlarmStateManager alarmStateManager; + + @Autowired + AlarmService alarmService; + + @Autowired + UmsService usmService; + + public static AlarmStateManager getAlarmStateManager() { + if (alarmStateManager == null) { + alarmStateManager = ApplicationContextProvider.getContext().getBean(AlarmStateManager.class); + } + return alarmStateManager; + } + + public AlarmStateManager() { + + this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r); + t.setName("AlarmStateManager-worker"); + t.setDaemon(false); + return t; + }); + + scheduler.scheduleWithFixedDelay(() -> { + + try { + // === 여기에 지속 실행할 로직 === + fireAlarm(); + + } catch (Throwable e) { + // 반드시 예외 잡기 (안 잡으면 스케줄 자체가 죽음) + logger.error("fireAlarm fail", e); + } + + }, 0, 5, TimeUnit.SECONDS); // 최초 0초, 이후 5초 간격 + } + + public void onEvnet(AlarmEvent alaramEvent) { + + Optional existEvent = registerAlarmList.stream().filter(e -> e.equals(alaramEvent)).findFirst(); + + if (existEvent.isPresent()) { + AlarmEvent event = existEvent.get(); + event.onOcurr(); + } else { + registerAlarmList.add(alaramEvent); + } + + } + + private void fireAlarm() { + + Map alarmPolicyMap = alarmService.getAlarmPolicy(); + + Map sendPolicyMap = alarmService.getSendPolicy(); + + Iterator iterator = registerAlarmList.iterator(); + + while (iterator.hasNext()) { + AlarmEvent event = iterator.next(); + AlarmPolicy alarmPolicy = alarmPolicyMap.get(event.getKey()); + + if (alarmPolicy != null && alarmPolicy.isAlarmTriggered(event)) { + // 알람 발송 로직 + SendPolicy sendPolicy = sendPolicyMap.get(event.getKey()); + if (alarmService.isAlarmEnabled()) { + send(sendPolicy, event); + } else { + logger.info("Alarm is disabled, event = {}, policy = {}", event, sendPolicy); + } + // 리스트에서 안전하게 제거 + iterator.remove(); + }else { //트리거 조건이 안되더라도, 이벤트발생한지 오래 되었으면 제거하여 OOM 예방. + + + long firstOcurredAt = event.getCondition().getOcurredAt(); + + long diffInMs = System.currentTimeMillis() - firstOcurredAt; + + if ( diffInMs >= TimeUnit.MINUTES.toMillis(60) ) { //1시간 지난거면 삭제 + iterator.remove(); + } + } + } + + } + + private void send(SendPolicy sendPolicy, AlarmEvent event) { + + UmsRequestPayload payload = null; + + try { + + List cellPhoneList = sendPolicy.getCellPhoneList(); + + for (String cellPhone : cellPhoneList) { + + payload = UmsRequestPayload.builder() + .content(ObpMonitoringUmsTemplate.builder().message(event.getMessage()).build()) + .cellphone(cellPhone).build(); + + usmService.send(payload); + } + + } catch (Exception e) { + logger.error(String.format("Alarm message delivery failed (UMS), payload=%s", payload), e); + + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/custom/alarm/condition/AlarmCondition.java b/src/main/java/com/eactive/eai/custom/alarm/condition/AlarmCondition.java new file mode 100644 index 0000000..2313fdf --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/condition/AlarmCondition.java @@ -0,0 +1,21 @@ +package com.eactive.eai.custom.alarm.condition; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +@Builder +@Getter +@ToString +public class AlarmCondition { + final long ocurredAt = System.currentTimeMillis(); + long lastOcurredAt; // 일단, 미사용 + @Builder.Default + long count = 1; + + public void onOcurr() { + this.lastOcurredAt = System.currentTimeMillis(); + this.count++; + } + +} diff --git a/src/main/java/com/eactive/eai/custom/alarm/condition/AlarmLevel.java b/src/main/java/com/eactive/eai/custom/alarm/condition/AlarmLevel.java new file mode 100644 index 0000000..f88986a --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/condition/AlarmLevel.java @@ -0,0 +1,5 @@ +package com.eactive.eai.custom.alarm.condition; + +public enum AlarmLevel { + ERROR, WARN, FATAL +} diff --git a/src/main/java/com/eactive/eai/custom/alarm/event/AlarmEvent.java b/src/main/java/com/eactive/eai/custom/alarm/event/AlarmEvent.java new file mode 100644 index 0000000..194128d --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/event/AlarmEvent.java @@ -0,0 +1,44 @@ +package com.eactive.eai.custom.alarm.event; + +import java.util.Objects; + +import com.eactive.eai.custom.alarm.condition.AlarmCondition; +import com.eactive.eai.custom.alarm.key.AlarmKey; +import com.eactive.eai.custom.alarm.policy.AlarmPolicy; +import com.eactive.eai.custom.alarm.policy.CounterPolicy; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Setter +@Getter +@Builder +@ToString +public class AlarmEvent { + AlarmKey key; + AlarmCondition condition; + String message; + + public void onOcurr() { + condition.onOcurr(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AlarmEvent)) return false; + + AlarmEvent that = (AlarmEvent) o; + return Objects.equals(this.key, that.key) + && Objects.equals(this.message, that.message); + } + + @Override + public int hashCode() { + return Objects.hash(key, message); + } + + +} diff --git a/src/main/java/com/eactive/eai/custom/alarm/key/AlarmKey.java b/src/main/java/com/eactive/eai/custom/alarm/key/AlarmKey.java new file mode 100644 index 0000000..ddcf0ad --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/key/AlarmKey.java @@ -0,0 +1,6 @@ +package com.eactive.eai.custom.alarm.key; + + +public interface AlarmKey { + String code(); +} diff --git a/src/main/java/com/eactive/eai/custom/alarm/key/CoreAlarmKey.java b/src/main/java/com/eactive/eai/custom/alarm/key/CoreAlarmKey.java new file mode 100644 index 0000000..cc1d79b --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/key/CoreAlarmKey.java @@ -0,0 +1,12 @@ +package com.eactive.eai.custom.alarm.key; + +public final class CoreAlarmKey { + + private CoreAlarmKey() { } + + public static final AlarmKey CircuitBreaker = new DynamicAlarmKey("CircuitBreaker{ALARM}"); + + public static final AlarmKey Timeout = new DynamicAlarmKey("Timeout{ALARM}"); + + +} \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/custom/alarm/key/DynamicAlarmKey.java b/src/main/java/com/eactive/eai/custom/alarm/key/DynamicAlarmKey.java new file mode 100644 index 0000000..6a76d46 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/key/DynamicAlarmKey.java @@ -0,0 +1,33 @@ +package com.eactive.eai.custom.alarm.key; + +import lombok.ToString; + +@ToString +public final class DynamicAlarmKey implements AlarmKey { + + private final String code; + + public DynamicAlarmKey(String code) { + if (code == null || code.isEmpty()) { + throw new IllegalArgumentException("AlarmKey code must not be empty"); + } + this.code = code; + } + + @Override + public String code() { + return code; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AlarmKey)) return false; + return code.equals(((AlarmKey) o).code()); + } + + @Override + public int hashCode() { + return code.hashCode(); + } +} \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/custom/alarm/policy/AlarmPolicy.java b/src/main/java/com/eactive/eai/custom/alarm/policy/AlarmPolicy.java new file mode 100644 index 0000000..3c6dd8f --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/policy/AlarmPolicy.java @@ -0,0 +1,71 @@ +package com.eactive.eai.custom.alarm.policy; + +import com.eactive.eai.custom.alarm.event.AlarmEvent; +import com.eactive.eai.custom.alarm.key.AlarmKey; + +public abstract class AlarmPolicy { + + private AlarmKey alarmKey; + + private int thresholdCount; + + private long timeWindowMs; + + protected AlarmPolicy(Builder builder) { + this.alarmKey = builder.alarmKey; + this.thresholdCount = builder.thresholdCount; + this.timeWindowMs = builder.timeWindowMs; + } + + public AlarmKey getAlarmKey() { + return alarmKey; + } + + public int getThresholdCount() { + return thresholdCount; + } + + public long getTimeWindowMs() { + return timeWindowMs; + } + + public abstract boolean isAlarmTriggered(AlarmEvent event); + + + public boolean hasElapsed(long baseTimeMs) { + + return System.currentTimeMillis() - baseTimeMs >= timeWindowMs; + + } + + public boolean hasExceededLimit(long count) { + return count >= thresholdCount; + } + + public abstract static class Builder> { + + private AlarmKey alarmKey; + private int thresholdCount; + private long timeWindowMs; + + public T alarmKey(AlarmKey alarmKey) { + this.alarmKey = alarmKey; + return self(); + } + + public T thresholdCount(int thresholdCount) { + this.thresholdCount = thresholdCount; + return self(); + } + + public T timeWindowMs(long timeWindowMs) { + this.timeWindowMs = timeWindowMs; + return self(); + } + + protected abstract T self(); + + abstract AlarmPolicy build(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/custom/alarm/policy/AlarmType.java b/src/main/java/com/eactive/eai/custom/alarm/policy/AlarmType.java new file mode 100644 index 0000000..547eaf2 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/policy/AlarmType.java @@ -0,0 +1,5 @@ +package com.eactive.eai.custom.alarm.policy; + +public enum AlarmType { + COUNT, TIMER +} diff --git a/src/main/java/com/eactive/eai/custom/alarm/policy/CounterPolicy.java b/src/main/java/com/eactive/eai/custom/alarm/policy/CounterPolicy.java new file mode 100644 index 0000000..7bf3c0a --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/policy/CounterPolicy.java @@ -0,0 +1,39 @@ +package com.eactive.eai.custom.alarm.policy; + +import com.eactive.eai.custom.alarm.event.AlarmEvent; + +public class CounterPolicy extends AlarmPolicy { + + private CounterPolicy(Builder builder) { + super(builder); + } + + @Override + public boolean isAlarmTriggered(AlarmEvent event) { + + if (!event.getKey().equals(getAlarmKey())) { + return false; + } + + long count = event.getCondition().getCount(); + return hasExceededLimit(count); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends AlarmPolicy.Builder { + + @Override + protected Builder self() { + return this; + } + + @Override + public CounterPolicy build() { + return new CounterPolicy(this); + } + } + +} diff --git a/src/main/java/com/eactive/eai/custom/alarm/policy/SendPolicy.java b/src/main/java/com/eactive/eai/custom/alarm/policy/SendPolicy.java new file mode 100644 index 0000000..592d529 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/policy/SendPolicy.java @@ -0,0 +1,23 @@ +package com.eactive.eai.custom.alarm.policy; + +import java.util.List; + +import com.eactive.eai.custom.alarm.key.AlarmKey; +import com.eactive.eai.custom.alarm.key.DynamicAlarmKey; + +import lombok.Getter; +import lombok.ToString; +@ToString +@Getter +public class SendPolicy { + + final AlarmKey alarmKey; + + final List cellPhoneList; + + public SendPolicy(String alarmKey, List cellPhoneList) { + this.alarmKey = new DynamicAlarmKey(alarmKey); + this.cellPhoneList = cellPhoneList; + } + +} diff --git a/src/main/java/com/eactive/eai/custom/alarm/policy/TimerPolicy.java b/src/main/java/com/eactive/eai/custom/alarm/policy/TimerPolicy.java new file mode 100644 index 0000000..29c719a --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/policy/TimerPolicy.java @@ -0,0 +1,51 @@ +package com.eactive.eai.custom.alarm.policy; + +import com.eactive.eai.custom.alarm.condition.AlarmCondition; +import com.eactive.eai.custom.alarm.event.AlarmEvent; +import com.eactive.eai.custom.alarm.policy.CounterPolicy.Builder; + +public class TimerPolicy extends AlarmPolicy { + + private TimerPolicy(Builder builder) { + super(builder); + } + + @Override + public boolean isAlarmTriggered(AlarmEvent event) { + + if (event.getKey().equals(getAlarmKey())) { + + AlarmCondition condition = event.getCondition(); + + long baseTimeMs = condition.getOcurredAt(); + + if (hasElapsed(baseTimeMs)) { + return true; + } else { + return false; + } + + } + + return false; + + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends AlarmPolicy.Builder { + + @Override + protected Builder self() { + return this; + } + + @Override + public TimerPolicy build() { + return new TimerPolicy(this); + } + } + +} diff --git a/src/main/java/com/eactive/eai/custom/alarm/ums/UmsCallResult.java b/src/main/java/com/eactive/eai/custom/alarm/ums/UmsCallResult.java new file mode 100644 index 0000000..f63b6d8 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/ums/UmsCallResult.java @@ -0,0 +1,18 @@ +package com.eactive.eai.custom.alarm.ums; + + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class UmsCallResult { + private String umsMessageId; + private boolean isSuccess; + + private String returnCode; + private String returnMessage; + private String returnPayload; + + private Throwable throwable; +} diff --git a/src/main/java/com/eactive/eai/custom/alarm/ums/UmsSendManager.java b/src/main/java/com/eactive/eai/custom/alarm/ums/UmsSendManager.java new file mode 100644 index 0000000..dfab3b5 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/ums/UmsSendManager.java @@ -0,0 +1,62 @@ +package com.eactive.eai.custom.alarm.ums; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.eactive.eai.common.util.Logger; +import com.eactive.eai.custom.alarm.ums.payload.UmsRequestPayload; + +@Component +public class UmsSendManager { + + private final ScheduledExecutorService scheduler; + + protected static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); + + Queue queue = new ConcurrentLinkedQueue(); + + @Autowired + UmsService umsService; + + public UmsSendManager() { + this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r); + t.setName("AlarmStateManager-worker"); + t.setDaemon(false); + return t; + }); + + scheduler.scheduleWithFixedDelay(() -> { + + try { + // === 여기에 지속 실행할 로직 === + sendMessage(); + + } catch (Throwable e) { + // 반드시 예외 잡기 (안 잡으면 스케줄 자체가 죽음) + logger.error("fireAlarm fail", e); + } + + }, 0, 5, TimeUnit.SECONDS); // 최초 0초, 이후 5초 간격 + } + + void sendMessage() throws Exception { + UmsRequestPayload payload; + while ((payload = queue.poll()) != null) { + umsService.send(payload); + } + + } + + public void enqueue(UmsRequestPayload payload) { + queue.offer(payload); + } + + +} diff --git a/src/main/java/com/eactive/eai/custom/alarm/ums/UmsService.java b/src/main/java/com/eactive/eai/custom/alarm/ums/UmsService.java new file mode 100644 index 0000000..4368826 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/ums/UmsService.java @@ -0,0 +1,135 @@ +package com.eactive.eai.custom.alarm.ums; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.entity.EntityBuilder; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.util.Timeout; +import org.springframework.stereotype.Component; +import org.springframework.util.StreamUtils; + +import com.eactive.eai.common.property.PropManager; +import com.eactive.eai.common.util.Jsons; +import com.eactive.eai.common.util.Logger; +import com.eactive.eai.custom.alarm.ums.payload.UmsRequestPayload; +import com.eactive.eai.custom.alarm.ums.payload.ValidationUtil; +import com.google.gson.JsonSyntaxException; + +@Component +public class UmsService { + + protected static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); + + private final static String UMS_GROUP_PROP_NAME = "UMS"; + private final static String UMS_PROP_HOST = "host"; + private final static String UMS_PROP_URL = "url"; + private final static String UMS_PROP_API_KEY = "apiKey"; + private final static String UMS_URL = "http://172.31.35.144:9021"; + private final static String API_KEY = "7cca6d58-e4f7-474d-9edd-525806bc99ff"; + private final static String SEND_URL = "/ums/openapi/send"; + private final static String CONTENT_TYPE = "application/json; charset=utf-8"; + private final static String AUTHORIZATION_FMT = "Bearer %s"; + + public Future send(UmsRequestPayload payload) throws Exception { + + if (payload.getContent() == null) { + throw new IllegalArgumentException("content is null"); + } + + try { + ValidationUtil.validateOrThrow(payload); + } catch (Exception ex) { + logger.debug(String.format("UMS Request payload validation failed - %s", payload)); + throw ex; + } + + return call(payload); + } + + private CloseableHttpClient createHttpClient() { + return HttpClients.createDefault(); + } + + private Future call(UmsRequestPayload requestPayload) throws Exception { + + ExecutorService executor = Executors.newSingleThreadExecutor(); + + String json = Jsons.GSON.toJson(requestPayload); + + String host = PropManager.getInstance().getProperty(UMS_GROUP_PROP_NAME, UMS_PROP_HOST, UMS_URL); + String url = PropManager.getInstance().getProperty(UMS_GROUP_PROP_NAME, UMS_PROP_URL, SEND_URL); + String apiKey = PropManager.getInstance().getProperty(UMS_GROUP_PROP_NAME, UMS_PROP_API_KEY, API_KEY); + + HttpPost httpPost = new HttpPost(host + url); + httpPost.setHeader("Content-Type", CONTENT_TYPE); + httpPost.setHeader("Authorization", String.format(AUTHORIZATION_FMT, apiKey)); + httpPost.setEntity(EntityBuilder.create().setText(json).setContentType(ContentType.APPLICATION_JSON).build()); + + if (logger.isDebugEnabled()) { + try { + logger.debug("Header - {}", httpPost.getHeader("Content-Type")); + logger.debug("Header - {}", httpPost.getHeader("Authorization")); + logger.debug("Request Body : {}", + StreamUtils.copyToString(httpPost.getEntity().getContent(), Charset.defaultCharset())); + } catch (ProtocolException | IOException e) { + throw new RuntimeException(e); + } + } + + httpPost.setConfig(RequestConfig.custom().setConnectTimeout(Timeout.ofSeconds(3)) + .setResponseTimeout(Timeout.ofSeconds(3)).build()); + Future future; + try (CloseableHttpClient httpClient = createHttpClient()) { + future = executor.submit(() -> { + return httpClient.execute(httpPost, res -> { + try { + UmsCallResult result; + String body = EntityUtils.toString(res.getEntity()); + if (logger.isDebugEnabled()) { + logger.debug("Response - {}", body); + } + result = Jsons.GSON.fromJson(body, UmsCallResult.class); + result.setUmsMessageId(requestPayload.getMessageIdentityNo()); + result.setSuccess(true); + return result; + } catch (JsonSyntaxException e) { + logger.error("UMS Call error(JSON Syntax)", e); + return UmsCallResult.builder().umsMessageId(requestPayload.getMessageIdentityNo()) + .isSuccess(false).throwable(e).build(); + } catch (Exception e) { + logger.error("UMS Call error", e); + return UmsCallResult.builder().umsMessageId(requestPayload.getMessageIdentityNo()) + .isSuccess(false).throwable(e).build(); + } finally { + logger.debug("Response status - {}", res.getCode()); + + if (res.getCode() != 200) { + Arrays.asList(res.getHeaders()).forEach(o -> { + logger.warn("[Header] {}: {}", o.getName(), o.getValue()); + }); + } + } + }); + }); + + return future; + } catch (Exception e) { + logger.error("UMS call exception", e); + throw new Exception("UMS call failed", e); + } finally { + executor.shutdown(); + } + + } +} diff --git a/src/main/java/com/eactive/eai/custom/alarm/ums/payload/IUmsGsonObject.java b/src/main/java/com/eactive/eai/custom/alarm/ums/payload/IUmsGsonObject.java new file mode 100644 index 0000000..a1b7220 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/ums/payload/IUmsGsonObject.java @@ -0,0 +1,7 @@ +package com.eactive.eai.custom.alarm.ums.payload; + +/** + * GSON Serialize 용 Object + */ +public interface IUmsGsonObject { +} diff --git a/src/main/java/com/eactive/eai/custom/alarm/ums/payload/ObpMonitoringUmsTemplate.java b/src/main/java/com/eactive/eai/custom/alarm/ums/payload/ObpMonitoringUmsTemplate.java new file mode 100644 index 0000000..9f5af06 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/ums/payload/ObpMonitoringUmsTemplate.java @@ -0,0 +1,12 @@ +package com.eactive.eai.custom.alarm.ums.payload; + +import com.google.gson.annotations.SerializedName; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ObpMonitoringUmsTemplate implements IUmsGsonObject { + @SerializedName("MOTR_EVAL_CNTN") + private String message; +} diff --git a/src/main/java/com/eactive/eai/custom/alarm/ums/payload/UmsBizWorkCode.java b/src/main/java/com/eactive/eai/custom/alarm/ums/payload/UmsBizWorkCode.java new file mode 100644 index 0000000..d669b23 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/ums/payload/UmsBizWorkCode.java @@ -0,0 +1,19 @@ +package com.eactive.eai.custom.alarm.ums.payload; + +import lombok.Getter; + +public enum UmsBizWorkCode { + MONITORING("B_ELFN_09510", "OBP 모니터링"), + VERIFY_PHONE("S55_ICT_0006", "API Portal 인증번호"), + ; + + @Getter + private String code; + @Getter + private String displayName; + + UmsBizWorkCode(String code, String displayName) { + this.code = code; + this.displayName = displayName; + } +} diff --git a/src/main/java/com/eactive/eai/custom/alarm/ums/payload/UmsRequestPayload.java b/src/main/java/com/eactive/eai/custom/alarm/ums/payload/UmsRequestPayload.java new file mode 100644 index 0000000..51fc6f6 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/ums/payload/UmsRequestPayload.java @@ -0,0 +1,105 @@ +package com.eactive.eai.custom.alarm.ums.payload; + +import com.google.gson.annotations.SerializedName; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.constraints.NotEmpty; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Data +@Builder +@Getter +public class UmsRequestPayload { + // MSG_IDNT_NO string 메시지식별번호 50 O "발송 구분을 위한 거래 번호 (GUID 권고) + //GUID로 입력이 어려울시 + //어플리케이션코드(3자리)+난수 로 유니크한 ID로 채번 권고" + @Length(min = 1, max = 50) + @NotEmpty + @SerializedName("MSG_IDNT_NO") + @Builder.Default + private String messageIdentityNo = String.format("APM-%s",DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS").format(LocalDateTime.now())); + + // MSG_BZWK_CD string 메시지업무코드 30 O UMS에 등록된 메시지업무코드(AS-IS 템플릿ID) + @NotEmpty + @Length(min = 1, max = 30) + @SerializedName("MSG_BZWK_CD") + @Builder.Default + private String msgBizWorkCode = UmsBizWorkCode.MONITORING.getCode(); + + // 메시지업무파라미터내용 / 4000 / JSONObject + // MSG_BZWK_PARAM_CTNT object 메시지업무파라미터내용 4000 메시지업무코드의 템플릿에서 사용하는 변수와 변수값 + @NotNull + @SerializedName("MSG_BZWK_PARAM_CTNT") + private IUmsGsonObject content; + + // 발송예약일시 / 14 / 실 발송 일자(yyyyMMddHHmmss) + // SND_RESR_DTTM string 발송예약일시 14 20240418111500 O 실 발송 일자(현재시간) + @Pattern(regexp = "^[0-9]{14}$") + @NotEmpty + @SerializedName("SND_RESR_DTTM") + @Builder.Default + private String sendReservationDatetime = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()); + + // 휴대전화번호 / 44 / 휴대폰번호(평문, 암호화 둘다가능) + // CPNO string 휴대전화번호 44 "암호화 또는 평문 (safedb not rrno) + //문자일 시 암호문 + //숫자일 시 전화번호 + //고객번호 또는 휴대전화번호 필수" + @NotEmpty + @Pattern(regexp = "^[0-9]{10,11}$") + @SerializedName("CPNO") + private String cellphone; + + // 발송부점코드 / 4 / + // SND_BRCD string 발송부점코드 4 O 0333 + @NotEmpty + @SerializedName("SND_BRCD") + @Builder.Default + private String sendBranchCode = "0990"; + + // 발송직원번호 / 7 / ECEB057(텔러번호?) + // SND_EMPNO string 발송직원번호 7 O ? 넣긴해야함 + @NotEmpty + @SerializedName("SND_EMPNO") + @Builder.Default + private String sendEmployeeNo = "ECEB057"; + + // SRVC_ID string 서비스ID 11 요청단 측 서비스ID + @NotEmpty + @SerializedName("SRVC_ID") + @Builder.Default + private String serviceId = "eAPIM"; + + // MSG_SND_CHNL_CD string 메시지발송채널코드 2 + @SerializedName("MSG_SND_CHNL_CD") + @NotNull + @Builder.Default + private ChannelCode msgSndChnlCd = ChannelCode.KM; + + // VALDTN_RSLT_CD string 검증결과코드 2 00으로 고정 + @SerializedName("VALDTN_RSLT_CD") + @NotEmpty + @Builder.Default + private String validationResultCode = "00"; + + public enum ChannelCode { + SM, // SMS + LM, // LMS + MM, // MMS + KM, // 카카오-알림톡 + FM, // 카카오-친구톡 + SR, // RCS-SMS + LR, // RCS-LMS + MR, // RCS-MMS + TR, // RCS-TEMPLATE + IR, // RCS-IMAGE Template + PM // Push + } +} + diff --git a/src/main/java/com/eactive/eai/custom/alarm/ums/payload/ValidationUtil.java b/src/main/java/com/eactive/eai/custom/alarm/ums/payload/ValidationUtil.java new file mode 100644 index 0000000..ecb8cf8 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/ums/payload/ValidationUtil.java @@ -0,0 +1,53 @@ +package com.eactive.eai.custom.alarm.ums.payload; + +import java.util.Set; +import java.util.stream.Collectors; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; + +import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator; + +public class ValidationUtil { + private static final ValidatorFactory factory; + private static final Validator validator; + + static { + factory = Validation.byDefaultProvider() + .configure() + .messageInterpolator(new ParameterMessageInterpolator()) + .buildValidatorFactory(); + + validator = factory.getValidator(); + } + + + private ValidationUtil() { + } + + + public static Set> validate(T target) { + return validator.validate(target); + } + + public static void validateOrThrow(T target) { + Set> violations = validate(target); + if (!violations.isEmpty()) { + String message = violations.stream() + .map(v -> v.getPropertyPath() + " : " + v.getMessage()) + .collect(Collectors.joining(", ")); + throw new ConstraintViolationException("Validation failed: " + message, violations); + } + } + + public static String getViolationMessage(T target) { + Set> violations = validate(target); + if (violations.isEmpty()) return null; + return violations.stream() + .map(v -> v.getPropertyPath() + " : " + v.getMessage()) + .collect(Collectors.joining(", ")); + } +} \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/custom/alarm/ums/payload/VerifyPhoneUmsTemplate.java b/src/main/java/com/eactive/eai/custom/alarm/ums/payload/VerifyPhoneUmsTemplate.java new file mode 100644 index 0000000..4840fbf --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/alarm/ums/payload/VerifyPhoneUmsTemplate.java @@ -0,0 +1,12 @@ +package com.eactive.eai.custom.alarm.ums.payload; + +import com.google.gson.annotations.SerializedName; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class VerifyPhoneUmsTemplate implements IUmsGsonObject { + @SerializedName("ATHN_NO") + private String authNumber; +} diff --git a/src/main/java/com/eactive/eai/outbound/RESTProcess.java b/src/main/java/com/eactive/eai/outbound/RESTProcess.java index 29bb60a..d22cde9 100644 --- a/src/main/java/com/eactive/eai/outbound/RESTProcess.java +++ b/src/main/java/com/eactive/eai/outbound/RESTProcess.java @@ -42,6 +42,11 @@ import com.eactive.eai.common.worker.ResponseMap; import com.eactive.eai.control.HttpSender; import com.eactive.eai.control.LogSender; import com.eactive.eai.control.Transform; +import com.eactive.eai.custom.alarm.AlarmStateManager; +import com.eactive.eai.custom.alarm.condition.AlarmCondition; +import com.eactive.eai.custom.alarm.event.AlarmEvent; +import com.eactive.eai.custom.alarm.key.AlarmKey; +import com.eactive.eai.custom.alarm.key.CoreAlarmKey; import com.eactive.eai.env.ElinkConfig; import com.eactive.eai.inbound.processor.Processor; import com.ext.eai.common.stdmessage.STDMessageKeys; @@ -96,20 +101,24 @@ public class RESTProcess extends HTTPProcess { String circuitBreakerUseYn = PropManager.getInstance().getProperty("CircuitBreaker", "useYn"); try { - if("Y".equals(circuitBreakerUseYn)){ - String apiId = reqEaiMsg.getEAISvcCd(); - CircuitBreaker circuitBreaker = CircuitBreakerManager.getInstance().getCircuitBreaker(apiId); + if (true) { + String apiId = reqEaiMsg.getEAISvcCd(); + CircuitBreaker circuitBreaker = CircuitBreakerManager.getInstance().getCircuitBreaker(apiId); - // ObjectMapper 인스턴스 생성 - ObjectMapper objectMapper = new ObjectMapper(); + // ObjectMapper 인스턴스 생성 + ObjectMapper objectMapper = new ObjectMapper(); - // 서킷 브레이커의 메트릭 정보와 상태 정보를 직접 Node로 변환 - ObjectNode combinedNode = objectMapper.createObjectNode(); - combinedNode.put("state", circuitBreaker.getState().toString()); - combinedNode.set("metrics", objectMapper.valueToTree(circuitBreaker.getMetrics())); + // 서킷 브레이커의 메트릭 정보와 상태 정보를 직접 Node로 변환 + ObjectNode combinedNode = objectMapper.createObjectNode(); + combinedNode.put("state", circuitBreaker.getState().toString()); + combinedNode.set("metrics", objectMapper.valueToTree(circuitBreaker.getMetrics())); + + logger.info("CircuitBreaker state " + combinedNode.toString()); + this.resObject = circuitBreaker.executeCallable(() -> HttpSender + .callService(this.adapterGroupName, this.outboundProp, this.reqObject, this.tempProp)); + + - logger.info("CircuitBreaker state "+combinedNode.toString()); - this.resObject = circuitBreaker.executeCallable(() -> HttpSender.callService(this.adapterGroupName, this.outboundProp, this.reqObject, this.tempProp)); }else{ this.resObject = HttpSender.callService(this.adapterGroupName, this.outboundProp, this.reqObject, this.tempProp); }