Add HTTP headers configuration for webhook (#13106)

Co-authored-by: yinyijun <yinyijun6@mgtv.com>
This commit is contained in:
Sherlock Yin 2025-03-17 20:49:17 +08:00 committed by GitHub
parent 97f0f0be67
commit d838277f4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 84 additions and 15 deletions

View File

@ -84,6 +84,7 @@
* Bump Armeria to 1.32.0 and some transitive dependencies.
* Skip persisting metrics/record data that have been expired.
* Fix the issue of missing Last Ping data.
* Add HTTP headers configuration for the alarm webhook.
#### UI

View File

@ -189,6 +189,25 @@ webhook:
urls:
- http://ip:port/xxx
- http://ip:port/yyy
custom1:
urls:
- http://127.0.0.1/custom1
# headers config is provided to add custom configurations or authentications that are required from the server side.
headers:
Authorization: Bearer bearer_token
custom2:
urls:
- http://127.0.0.1/custom2
# headers config is provided to add custom configurations or authentications that are required from the server
headers:
Authorization: Basic basic_token
custom3:
urls:
- http://127.0.0.1/internal-hook
# headers config is provided to add custom configurations or authentications that are required from the server
headers:
x-company-token: whatever-token-defined-internally-within-the-company
x-company-header: arbitrary-additional-http-headers
```
The JSON format is based on `List<org.apache.skywalking.oap.server.core.alarm.AlarmMessage>` with the following key information:

View File

@ -157,11 +157,12 @@ public class RulesReader {
Map<String, Object> config = (Map<String, Object>) v;
WebhookSettings settings = new WebhookSettings(
k.toString(), AlarmHooksType.webhook, (Boolean) config.getOrDefault("is-default", false));
List<String> urls = (List<String>) config.get("urls");
if (urls != null) {
settings.getUrls().addAll(urls);
}
Map<String, String> headers = (Map<String, String>) config.getOrDefault("headers", new HashMap<>());
settings.setHeaders(headers);
rules.getWebhookSettingsMap().put(settings.getFormattedName(), settings);
if (settings.isDefault()) {
this.defaultHooks.add(settings.getFormattedName());

View File

@ -56,7 +56,7 @@ public class WebhookCallback extends HttpAlarmCallback {
}
for (final var url : setting.getUrls()) {
try {
post(URI.create(url), gson.toJson(messages), Map.of());
post(URI.create(url), gson.toJson(messages), setting.getHeaders());
} catch (Exception e) {
log.error("Failed to send alarm message to Webhook: {}", url, e);
}

View File

@ -19,7 +19,10 @@
package org.apache.skywalking.oap.server.core.alarm.provider.webhook;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@ -31,6 +34,7 @@ import org.apache.skywalking.oap.server.core.alarm.provider.AlarmHooksType;
@ToString
public class WebhookSettings extends AlarmHookSettings {
private List<String> urls = new ArrayList<>();
private Map<String, String> headers = new HashMap<>();
public WebhookSettings(final String name,
final AlarmHooksType type,

View File

@ -74,6 +74,10 @@ public class RulesReaderTest {
WebhookSettings rulesWebhooks = rules.getWebhookSettingsMap().get(AlarmHooksType.webhook.name() + ".default");
Assertions.assertEquals(2, rulesWebhooks.getUrls().size());
Assertions.assertEquals("http://127.0.0.1/go-wechat/", rulesWebhooks.getUrls().get(1));
WebhookSettings rulesWebhooks2 = rules.getWebhookSettingsMap().get(AlarmHooksType.webhook.name() + ".custom1");
Assertions.assertEquals(2, rulesWebhooks2.getHeaders().size());
Assertions.assertEquals("Bearer bearer_token", rulesWebhooks2.getHeaders().get("Authorization"));
Assertions.assertEquals("arbitrary-additional-http-headers", rulesWebhooks2.getHeaders().get("x-company-header"));
GRPCAlarmSetting grpcAlarmSetting = rules.getGrpcAlarmSettingMap().get(AlarmHooksType.gRPC.name() + ".default");
assertNotNull(grpcAlarmSetting);

View File

@ -18,13 +18,13 @@
package org.apache.skywalking.oap.server.core.alarm.provider.webhook;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.linecorp.armeria.common.HttpResponse;
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.common.RequestHeaders;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.testing.junit5.server.ServerExtension;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.skywalking.oap.server.core.alarm.AlarmMessage;
import org.apache.skywalking.oap.server.core.alarm.provider.AlarmHooksType;
import org.apache.skywalking.oap.server.core.alarm.provider.AlarmRulesWatcher;
@ -33,9 +33,13 @@ import org.apache.skywalking.oap.server.core.source.DefaultScopeDefine;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.testcontainers.shaded.com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class WebhookCallbackTest {
private static final AtomicBoolean IS_SUCCESS = new AtomicBoolean();
@ -45,19 +49,30 @@ public class WebhookCallbackTest {
public static final ServerExtension SERVER = new ServerExtension() {
@Override
protected void configure(ServerBuilder sb) {
sb.service("/webhook/receiveAlarm", (ctx, req) -> HttpResponse.from(
req.aggregate().thenApply(r -> {
final String content = r.content().toStringUtf8();
final JsonArray elements = new Gson().fromJson(content, JsonArray.class);
if (elements.size() == 1) {
COUNTER.getAndIncrement();
sb.service("/webhook/receiveAlarm", (ctx, req) -> HttpResponse.from(req.aggregate().thenApply(r -> {
final String content = r.content().toStringUtf8();
final RequestHeaders headers = r.headers();
List<AlarmMessage> alarmMessages = new Gson().fromJson(content, new TypeToken<ArrayList<AlarmMessage>>() {
}.getType());
if (alarmMessages.size() != 1) {
IS_SUCCESS.set(false);
return HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);
}
if (Objects.equals(alarmMessages.get(0).getId0(), "1")) {
IS_SUCCESS.set(true);
COUNTER.incrementAndGet();
return HttpResponse.of(HttpStatus.OK);
} else if (Objects.equals(alarmMessages.get(0).getId0(), "2")) {
if (Objects.equals(headers.get("Authorization"), "Bearer bearer_token")
&& Objects.equals(headers.get("x-company-header"), "arbitrary-additional-http-headers")) {
IS_SUCCESS.set(true);
COUNTER.incrementAndGet();
return HttpResponse.of(HttpStatus.OK);
}
return HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);
}))
);
}
IS_SUCCESS.set(false);
return HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);
})));
}
};
@ -72,16 +87,19 @@ public class WebhookCallbackTest {
setting2.setUrls(remoteEndpoints);
rules.getWebhookSettingsMap().put(setting1.getFormattedName(), setting1);
rules.getWebhookSettingsMap().put(setting2.getFormattedName(), setting2);
setting2.setHeaders(ImmutableMap.of("Authorization", " Bearer bearer_token", "x-company-header", "arbitrary-additional-http-headers"));
AlarmRulesWatcher alarmRulesWatcher = new AlarmRulesWatcher(rules, null, null);
WebhookCallback webhookCallback = new WebhookCallback(alarmRulesWatcher);
List<AlarmMessage> alarmMessages = new ArrayList<>(2);
AlarmMessage alarmMessage = new AlarmMessage();
alarmMessage.setId0("1");
alarmMessage.setScopeId(DefaultScopeDefine.SERVICE);
alarmMessage.setRuleName("service_resp_time_rule");
alarmMessage.setAlarmMessage("alarmMessage with [DefaultScopeDefine.All]");
alarmMessage.getHooks().add(setting1.getFormattedName());
alarmMessages.add(alarmMessage);
AlarmMessage anotherAlarmMessage = new AlarmMessage();
anotherAlarmMessage.setId0("2");
anotherAlarmMessage.setRuleName("service_resp_time_rule_2");
anotherAlarmMessage.setScopeId(DefaultScopeDefine.ENDPOINT);
anotherAlarmMessage.setAlarmMessage("anotherAlarmMessage with [DefaultScopeDefine.Endpoint]");
@ -92,4 +110,4 @@ public class WebhookCallbackTest {
Assertions.assertTrue(IS_SUCCESS.get());
Assertions.assertEquals(2, COUNTER.get());
}
}
}

View File

@ -66,6 +66,12 @@ hooks:
urls:
- http://127.0.0.1/notify/
- http://127.0.0.1/go-wechat/
custom1:
urls:
- http://127.0.0.1/custom1
headers:
Authorization: Bearer bearer_token
x-company-header: arbitrary-additional-http-headers
gRPC:
default:

View File

@ -47,6 +47,22 @@ hooks:
# urls:
# - http://127.0.0.1/notify/
# - http://127.0.0.1/go-wechat/
# custom1:
# urls:
# - http://127.0.0.1/custom1
# headers:
# Authorization: Bearer bearer_token
# custom2:
# urls:
# - http://127.0.0.1/custom2
# headers:
# Authorization: Basic basic_token
# custom3:
# urls:
# - http://127.0.0.1/internal-hook
# headers:
# x-company-token: whatever-token-defined-internally-within-the-company
# x-company-header: arbitrary-additional-http-headers
#
# gRPC:
# default: