Deleting logging modules

This commit is contained in:
Alberto C. Ríos
2023-04-17 11:03:21 +02:00
parent e7f845127d
commit 09d4736ceb
31 changed files with 1 additions and 1732 deletions

View File

@@ -426,8 +426,7 @@ def getTestProjects() {
def getStarterProjects() {
[project(":spring-cloud-starter-app-broker"),
project(":spring-cloud-starter-app-broker-cloudfoundry"),
project(":spring-cloud-starter-app-broker-logging")] as Set
project(":spring-cloud-starter-app-broker-cloudfoundry")] as Set
}
def getLibraryProjects() {

View File

@@ -32,7 +32,5 @@ include "spring-cloud-app-broker-autoconfigure"
include "spring-cloud-app-broker-integration-tests"
include "spring-cloud-app-broker-acceptance-tests"
include "spring-cloud-app-broker-security-credhub"
include "spring-cloud-app-broker-logging"
include "spring-cloud-starter-app-broker"
include "spring-cloud-starter-app-broker-cloudfoundry"
include "spring-cloud-starter-app-broker-logging"

View File

@@ -22,12 +22,6 @@ plugins {
description = "Spring Cloud App Broker Autoconfiguration"
java {
registerFeature("logging") {
usingSourceSet(sourceSets.main)
}
}
dependencies {
annotationProcessor platform(SpringBootPlugin.BOM_COORDINATES)
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
@@ -43,8 +37,6 @@ dependencies {
api "org.cloudfoundry:cloudfoundry-operations:${cfJavaClientVersion}"
api "org.springframework.credhub:spring-credhub-starter:${springCredhubVersion}"
loggingImplementation project(":spring-cloud-app-broker-logging")
testImplementation "org.springframework.boot:spring-boot-starter-test"
testImplementation "org.springframework.boot:spring-boot-starter-webflux"
testImplementation "io.projectreactor.tools:blockhound-junit-platform:${blockHoundVersion}"

View File

@@ -1,83 +0,0 @@
/*
* Copyright 2016-2020 the original author or authors
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.autoconfigure;
import java.util.HashMap;
import java.util.Map;
import org.cloudfoundry.client.CloudFoundryClient;
import org.cloudfoundry.doppler.DopplerClient;
import org.cloudfoundry.doppler.Envelope;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.appbroker.logging.ApplicationIdsProvider;
import org.springframework.cloud.appbroker.logging.streaming.ApplicationLogStreamPublisher;
import org.springframework.cloud.appbroker.logging.streaming.DopplerLogStreamPublisher;
import org.springframework.cloud.appbroker.logging.streaming.LogStreamPublisher;
import org.springframework.cloud.appbroker.logging.streaming.endpoint.StreamingLogWebSocketHandler;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;
@Configuration
@ConditionalOnClass(ApplicationLogStreamPublisher.class)
@ConditionalOnBean(ApplicationIdsProvider.class)
public class ServiceInstanceLogStreamAutoConfiguration {
@Bean
public StreamingLogWebSocketHandler streamingLogWebSocketHandler(
ApplicationEventPublisher applicationEventPublisher) {
return new StreamingLogWebSocketHandler(applicationEventPublisher);
}
@Bean
@ConditionalOnMissingBean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter();
}
@Bean
public HandlerMapping logsHandlerMapping(StreamingLogWebSocketHandler webSocketHandler) {
Map<String, WebSocketHandler> map = new HashMap<>();
map.put("/logs/**", webSocketHandler);
SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
handlerMapping.setOrder(1);
handlerMapping.setUrlMap(map);
return handlerMapping;
}
@Bean
@ConditionalOnMissingBean
public LogStreamPublisher<Envelope> streamLogsPublisher(CloudFoundryClient cloudFoundryClient,
DopplerClient dopplerClient, ApplicationIdsProvider applicationIdsProvider) {
return new DopplerLogStreamPublisher(cloudFoundryClient, dopplerClient, applicationIdsProvider);
}
@Bean
public ApplicationLogStreamPublisher applicationLogsPublisher(LogStreamPublisher<Envelope> logStreamPublisher,
ApplicationEventPublisher eventPublisher) {
return new ApplicationLogStreamPublisher(logStreamPublisher, eventPublisher);
}
}

View File

@@ -1,49 +0,0 @@
/*
* Copyright 2016-2020 the original author or authors
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.autoconfigure;
import org.cloudfoundry.client.CloudFoundryClient;
import org.cloudfoundry.doppler.DopplerClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.appbroker.logging.ApplicationIdsProvider;
import org.springframework.cloud.appbroker.logging.recent.ApplicationRecentLogsProvider;
import org.springframework.cloud.appbroker.logging.recent.RecentLogsProvider;
import org.springframework.cloud.appbroker.logging.recent.endpoint.RecentLogsController;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnClass(ApplicationRecentLogsProvider.class)
@ConditionalOnBean(ApplicationIdsProvider.class)
public class ServiceInstanceRecentLogsAutoConfiguration {
@Bean
public RecentLogsProvider recentLogsProvider(CloudFoundryClient cloudFoundryClient, DopplerClient dopplerClient,
ApplicationIdsProvider applicationIdsProvider) {
return new ApplicationRecentLogsProvider(cloudFoundryClient, dopplerClient, applicationIdsProvider);
}
@Bean
@ConditionalOnMissingBean
public RecentLogsController recentLogsController(RecentLogsProvider recentLogsProvider) {
return new RecentLogsController(recentLogsProvider);
}
}

View File

@@ -1,5 +1,3 @@
org.springframework.cloud.appbroker.autoconfigure.AppBrokerAutoConfiguration
org.springframework.cloud.appbroker.autoconfigure.CloudFoundryAppDeployerAutoConfiguration
org.springframework.cloud.appbroker.autoconfigure.CredHubAutoConfiguration
org.springframework.cloud.appbroker.autoconfigure.ServiceInstanceRecentLogsAutoConfiguration
org.springframework.cloud.appbroker.autoconfigure.ServiceInstanceLogStreamAutoConfiguration

View File

@@ -1,96 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.autoconfigure;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.appbroker.logging.ApplicationIdsProvider;
import org.springframework.cloud.appbroker.logging.streaming.ApplicationLogStreamPublisher;
import org.springframework.cloud.appbroker.logging.streaming.LogStreamPublisher;
import org.springframework.cloud.appbroker.logging.streaming.endpoint.StreamingLogWebSocketHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;
import static org.assertj.core.api.Assertions.assertThat;
class ServiceInstanceLogStreamAutoConfigurationTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
AppBrokerAutoConfiguration.class,
CloudFoundryAppDeployerAutoConfiguration.class,
ServiceInstanceLogStreamAutoConfiguration.class
))
.withPropertyValues(
"spring.cloud.appbroker.deployer.cloudfoundry.api-host=https://api.example.local",
"spring.cloud.appbroker.deployer.cloudfoundry.username=user",
"spring.cloud.appbroker.deployer.cloudfoundry.password=secret"
);
@Test
void servicesAreNotCreatedWithoutLoggingOnClasspath() {
contextRunner
.withClassLoader(new FilteredClassLoader(ApplicationLogStreamPublisher.class))
.withUserConfiguration(LoggingConfiguration.class)
.run(context -> assertThat(context)
.doesNotHaveBean(StreamingLogWebSocketHandler.class)
.doesNotHaveBean(WebSocketHandlerAdapter.class)
.doesNotHaveBean(HandlerMapping.class)
.doesNotHaveBean(LogStreamPublisher.class)
.doesNotHaveBean(ApplicationLogStreamPublisher.class));
}
@Test
void servicesAreNotCreatedWithoutRequiredBeansOnClasspath() {
contextRunner
.run(context -> assertThat(context)
.doesNotHaveBean(StreamingLogWebSocketHandler.class)
.doesNotHaveBean(WebSocketHandlerAdapter.class)
.doesNotHaveBean(HandlerMapping.class)
.doesNotHaveBean(LogStreamPublisher.class)
.doesNotHaveBean(ApplicationLogStreamPublisher.class));
}
@Test
void servicesAreCreatedWithLoggingConfigured() {
contextRunner
.withUserConfiguration(LoggingConfiguration.class)
.run(context -> assertThat(context)
.hasSingleBean(StreamingLogWebSocketHandler.class)
.hasSingleBean(WebSocketHandlerAdapter.class)
.hasSingleBean(HandlerMapping.class)
.hasSingleBean(LogStreamPublisher.class)
.hasSingleBean(ApplicationLogStreamPublisher.class));
}
@TestConfiguration
public static class LoggingConfiguration {
@Bean
public ApplicationIdsProvider applicationIdsProvider() {
return serviceInstanceId -> Flux.just("app1");
}
}
}

View File

@@ -1,85 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.autoconfigure;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.appbroker.logging.ApplicationIdsProvider;
import org.springframework.cloud.appbroker.logging.recent.ApplicationRecentLogsProvider;
import org.springframework.cloud.appbroker.logging.recent.RecentLogsProvider;
import org.springframework.cloud.appbroker.logging.recent.endpoint.RecentLogsController;
import org.springframework.context.annotation.Bean;
import static org.assertj.core.api.Assertions.assertThat;
class ServiceInstanceRecentLogsAutoConfigurationTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
AppBrokerAutoConfiguration.class,
CloudFoundryAppDeployerAutoConfiguration.class,
ServiceInstanceRecentLogsAutoConfiguration.class
))
.withPropertyValues(
"spring.cloud.appbroker.deployer.cloudfoundry.api-host=https://api.example.local",
"spring.cloud.appbroker.deployer.cloudfoundry.username=user",
"spring.cloud.appbroker.deployer.cloudfoundry.password=secret"
);
@Test
void servicesAreNotCreatedWithoutLoggingOnClasspath() {
contextRunner
.withClassLoader(new FilteredClassLoader(ApplicationRecentLogsProvider.class))
.withUserConfiguration(LoggingConfiguration.class)
.run(context -> assertThat(context)
.doesNotHaveBean(RecentLogsProvider.class)
.doesNotHaveBean(RecentLogsController.class));
}
@Test
void servicesAreNotCreatedWithoutRequiredBeansOnClasspath() {
contextRunner
.run(context -> assertThat(context)
.doesNotHaveBean(RecentLogsProvider.class)
.doesNotHaveBean(RecentLogsController.class));
}
@Test
void servicesAreCreatedWithLoggingConfigured() {
contextRunner
.withUserConfiguration(LoggingConfiguration.class)
.run(context -> assertThat(context)
.hasSingleBean(RecentLogsProvider.class)
.hasSingleBean(RecentLogsController.class));
}
@TestConfiguration
public static class LoggingConfiguration {
@Bean
public ApplicationIdsProvider applicationIdsProvider() {
return serviceInstanceId -> Flux.just("app1");
}
}
}

View File

@@ -1,35 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.
*/
import org.springframework.boot.gradle.plugin.SpringBootPlugin
plugins {
id 'org.springframework.boot' apply false
}
description = "Spring Cloud App Broker Logging"
dependencies {
api platform(SpringBootPlugin.BOM_COORDINATES)
api "org.springframework.boot:spring-boot-starter-webflux"
api "org.cloudfoundry:cloudfoundry-client-reactor:${cfJavaClientVersion}"
api "org.cloudfoundry:cloudfoundry-operations:${cfJavaClientVersion}"
testImplementation project(":spring-cloud-starter-app-broker-logging")
testImplementation "org.springframework.boot:spring-boot-starter-test"
testImplementation "org.junit.jupiter:junit-jupiter-api"
testImplementation "org.awaitility:awaitility"
}

View File

@@ -1,25 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging;
import reactor.core.publisher.Flux;
public interface ApplicationIdsProvider {
Flux<String> getApplicationIds(String serviceInstanceId);
}

View File

@@ -1,111 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging;
import okio.ByteString;
import org.cloudfoundry.doppler.EventType;
import org.cloudfoundry.doppler.MessageType;
import org.cloudfoundry.dropsonde.events.Envelope;
import org.cloudfoundry.dropsonde.events.LogMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class LoggingUtils {
private static final Logger LOG = LoggerFactory.getLogger(LoggingUtils.class);
private LoggingUtils() {
}
public static Envelope convertDopplerEnvelopeToDropsonde(org.cloudfoundry.doppler.Envelope envelope) {
final Envelope.Builder builder = new Envelope.Builder()
.deployment(envelope.getDeployment())
.eventType(toDropsondeEventType(envelope.getEventType()))
.index(envelope.getIndex())
.ip(envelope.getIp())
.job(envelope.getJob())
.origin(envelope.getOrigin())
.tags(envelope.getTags())
.timestamp(envelope.getTimestamp());
if (envelope.getEventType() == EventType.LOG_MESSAGE) {
final org.cloudfoundry.doppler.LogMessage logMessage = envelope.getLogMessage();
if (LOG.isDebugEnabled()) {
LOG.debug("Decoding message [" + logMessage.getTimestamp() + "]: " + logMessage.getMessage());
}
builder.logMessage(new LogMessage.Builder()
.app_id(logMessage.getApplicationId())
.message(ByteString.encodeUtf8(logMessage.getMessage()))
.message_type(toDropsondeMessageType(logMessage.getMessageType()))
.source_instance(logMessage.getSourceInstance())
.source_type(logMessage.getSourceType())
.timestamp(logMessage.getTimestamp())
.build());
}
else {
if (LOG.isDebugEnabled()) {
LOG.debug("Unable to decode message of type " + envelope.getEventType());
}
}
return builder.build();
}
public static org.cloudfoundry.doppler.Envelope injectAppNameIntoLogSourceInstance(String appName,
org.cloudfoundry.doppler.Envelope envelope) {
if (envelope.getEventType() != EventType.LOG_MESSAGE) {
return envelope;
}
return org.cloudfoundry.doppler.Envelope.builder().from(envelope).logMessage(
org.cloudfoundry.doppler.LogMessage.builder().from(envelope.getLogMessage())
.sourceInstance(appName + " " + envelope.getLogMessage().getSourceInstance())
.build()
).build();
}
private static LogMessage.MessageType toDropsondeMessageType(MessageType messageType) {
switch (messageType) {
case ERR:
return LogMessage.MessageType.ERR;
case OUT:
return LogMessage.MessageType.OUT;
default:
throw new IllegalArgumentException("Unknown message type " + messageType);
}
}
private static Envelope.EventType toDropsondeEventType(EventType eventType) {
switch (eventType) {
case ERROR:
return Envelope.EventType.Error;
case CONTAINER_METRIC:
return Envelope.EventType.ContainerMetric;
case COUNTER_EVENT:
return Envelope.EventType.CounterEvent;
case HTTP_START_STOP:
return Envelope.EventType.HttpStartStop;
case LOG_MESSAGE:
return Envelope.EventType.LogMessage;
case VALUE_METRIC:
return Envelope.EventType.ValueMetric;
}
throw new IllegalArgumentException("Unknown event type: " + eventType);
}
}

View File

@@ -1,60 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging.recent;
import org.cloudfoundry.client.CloudFoundryClient;
import org.cloudfoundry.client.v2.applications.GetApplicationRequest;
import org.cloudfoundry.doppler.DopplerClient;
import org.cloudfoundry.doppler.RecentLogsRequest;
import org.cloudfoundry.dropsonde.events.Envelope;
import reactor.core.publisher.Flux;
import org.springframework.cloud.appbroker.logging.ApplicationIdsProvider;
import org.springframework.cloud.appbroker.logging.LoggingUtils;
public class ApplicationRecentLogsProvider implements RecentLogsProvider {
private final CloudFoundryClient client;
private final DopplerClient dopplerClient;
private final ApplicationIdsProvider applicationIdsProvider;
public ApplicationRecentLogsProvider(CloudFoundryClient client, DopplerClient dopplerClient,
ApplicationIdsProvider applicationIdsProvider) {
this.client = client;
this.dopplerClient = dopplerClient;
this.applicationIdsProvider = applicationIdsProvider;
}
@Override
public Flux<Envelope> getLogs(String serviceInstanceId) {
return this.applicationIdsProvider.getApplicationIds(serviceInstanceId)
.flatMap(this::recentLogs)
.map(LoggingUtils::convertDopplerEnvelopeToDropsonde);
}
protected Flux<org.cloudfoundry.doppler.Envelope> recentLogs(String applicationId) {
return client.applicationsV2().get(GetApplicationRequest.builder().applicationId(applicationId).build())
.map(response -> response.getEntity().getName())
.flatMapMany(appName ->
dopplerClient.recentLogs(RecentLogsRequest.builder().applicationId(applicationId).build())
.map(envelope -> LoggingUtils.injectAppNameIntoLogSourceInstance(appName, envelope))
);
}
}

View File

@@ -1,26 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging.recent;
import org.cloudfoundry.dropsonde.events.Envelope;
import reactor.core.publisher.Flux;
public interface RecentLogsProvider {
Flux<Envelope> getLogs(String serviceInstanceId);
}

View File

@@ -1,27 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging.recent.endpoint;
class EncodingException extends RuntimeException {
private static final long serialVersionUID = 1837485200518028161L;
public EncodingException(Throwable throwable) {
super("Failed to encode: " + throwable.getMessage(), throwable);
}
}

View File

@@ -1,42 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging.recent.endpoint;
import java.util.Comparator;
import org.cloudfoundry.dropsonde.events.Envelope;
class LogMessageComparator implements Comparator<Envelope> {
@Override
public int compare(Envelope o1, Envelope o2) {
return Long.compare(getTimestamp(o1), getTimestamp(o2));
}
private long getTimestamp(Envelope e) {
if (e.logMessage != null && e.logMessage.timestamp != null) {
return e.logMessage.timestamp;
}
if (e.timestamp == null) {
return 0;
}
else {
return e.timestamp;
}
}
}

View File

@@ -1,66 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging.recent.endpoint;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
class MultipartEncoder {
private final byte[] bytesCRLF = {'\r', '\n'};
private final byte[] bytesSEP = {'-', '-'};
private final byte[] boundary;
private final ByteArrayOutputStream out = new ByteArrayOutputStream();
public MultipartEncoder(String boundary) {
this.boundary = boundary.getBytes();
}
public void append(byte[] part) {
try {
out.write(bytesCRLF);
out.write(bytesSEP);
out.write(boundary);
out.write(bytesCRLF);
out.write(bytesCRLF);
out.write(part);
}
catch (IOException e) {
throw new EncodingException(e);
}
}
public byte[] terminateAndGetBytes() {
try {
out.write(bytesCRLF);
out.write(bytesSEP);
out.write(boundary);
out.write(bytesSEP);
out.write(bytesCRLF);
final byte[] bytes = out.toByteArray();
out.close();
return bytes;
}
catch (IOException e) {
throw new EncodingException(e);
}
}
}

View File

@@ -1,63 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging.recent.endpoint;
import java.util.UUID;
import org.cloudfoundry.dropsonde.events.Envelope;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.appbroker.logging.recent.RecentLogsProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RecentLogsController {
private static final LogMessageComparator LOG_MESSAGE_COMPARATOR = new LogMessageComparator();
private final RecentLogsProvider recentLogsProviders;
public RecentLogsController(@Autowired(required = false) RecentLogsProvider recentLogsProviders) {
this.recentLogsProviders = recentLogsProviders;
}
@RequestMapping("/logs/{serviceInstanceId}/recentlogs")
public Mono<ResponseEntity<byte[]>> recentLogs(@PathVariable("serviceInstanceId") String serviceInstanceId) {
final String multipartBoundary = UUID.randomUUID().toString();
final HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_TYPE, "multipart/mixed; boundary=" + multipartBoundary);
return recentLogsProviders.getLogs(serviceInstanceId)
.collectList()
.doOnNext(envelopes -> envelopes.sort(LOG_MESSAGE_COMPARATOR))
.map(envelopes -> {
final MultipartEncoder multipart = new MultipartEncoder(multipartBoundary);
for (Envelope message : envelopes) {
multipart.append(Envelope.ADAPTER.encode(message));
}
return new ResponseEntity<>(multipart.terminateAndGetBytes(), headers, HttpStatus.OK);
});
}
}

View File

@@ -1,148 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging.streaming;
import java.util.HashMap;
import java.util.Map;
import org.cloudfoundry.doppler.Envelope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import org.springframework.cloud.appbroker.logging.LoggingUtils;
import org.springframework.cloud.appbroker.logging.streaming.events.ServiceInstanceLogEvent;
import org.springframework.cloud.appbroker.logging.streaming.events.ServiceInstanceLoggingEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationListener;
public class ApplicationLogStreamPublisher implements ApplicationListener<ServiceInstanceLoggingEvent> {
private static final Logger LOG = LoggerFactory.getLogger(ApplicationLogStreamPublisher.class);
private final Map<String, Registration> registry = new HashMap<>();
private final LogStreamPublisher<Envelope> logStreamPublisher;
private final ApplicationEventPublisher publisher;
public ApplicationLogStreamPublisher(LogStreamPublisher<Envelope> logStreamPublisher,
ApplicationEventPublisher publisher) {
this.logStreamPublisher = logStreamPublisher;
this.publisher = publisher;
}
@Override
public void onApplicationEvent(ServiceInstanceLoggingEvent event) {
final String serviceInstanceId = event.getServiceInstanceId();
switch (event.getOperation()) {
case START:
LOG.debug("Received event to begin listening to logs for {}", serviceInstanceId);
this.startPublishing(serviceInstanceId);
return;
case STOP:
LOG.debug("Received event to stop listening to logs for {}", serviceInstanceId);
this.stopPublishing(serviceInstanceId);
return;
default:
throw new IllegalArgumentException("Unknown operation: " + event.getOperation());
}
}
private void startPublishing(String serviceInstanceId) {
synchronized (registry) {
final Registration registration = registry.get(serviceInstanceId);
if (registration != null) {
LOG.debug("Incrementing registration subscription count for {}", serviceInstanceId);
registration.increment();
return;
}
Flux<Envelope> logStream = this.logStreamPublisher
.getLogStream(serviceInstanceId);
final Disposable subscription = logStream
.map(LoggingUtils::convertDopplerEnvelopeToDropsonde)
.doOnNext(
envelope -> publisher.publishEvent(new ServiceInstanceLogEvent(this, serviceInstanceId, envelope)))
.subscribe();
LOG.debug("Creating new registration for {}", serviceInstanceId);
registry.put(serviceInstanceId, new Registration(subscription));
}
}
private void stopPublishing(String serviceInstanceId) {
if (LOG.isDebugEnabled()) {
LOG.debug("Received event to stop listening to logs for {}", serviceInstanceId);
}
synchronized (registry) {
final Registration registration = registry.get(serviceInstanceId);
if (registration == null) {
if (LOG.isWarnEnabled()) {
LOG.warn("Received deregister event for service instance {} but there no event handler registered",
serviceInstanceId);
}
}
else if (registration.decrement() == 0) {
if (LOG.isDebugEnabled()) {
LOG.debug("Disposing of registration since there are no more subscriptions");
}
registration.getSubscription().dispose();
registry.remove(serviceInstanceId);
}
}
}
private final static class Registration {
private final Disposable subscription;
private int count = 1;
private Registration(Disposable subscription) {
this.subscription = subscription;
}
public void increment() {
if (LOG.isDebugEnabled()) {
LOG.debug("Incrementing subscription count from {} to {}", count, count + 1);
}
++count;
}
public int decrement() {
if (LOG.isDebugEnabled()) {
LOG.debug("Decrementing subscription count from {} to {}", count, count - 1);
}
return --count;
}
public Disposable getSubscription() {
return subscription;
}
}
}

View File

@@ -1,68 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging.streaming;
import org.cloudfoundry.client.CloudFoundryClient;
import org.cloudfoundry.client.v2.applications.GetApplicationRequest;
import org.cloudfoundry.doppler.DopplerClient;
import org.cloudfoundry.doppler.Envelope;
import org.cloudfoundry.doppler.StreamRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.cloud.appbroker.logging.ApplicationIdsProvider;
import org.springframework.cloud.appbroker.logging.LoggingUtils;
public class DopplerLogStreamPublisher implements LogStreamPublisher<Envelope> {
private static final Logger LOG = LoggerFactory.getLogger(DopplerLogStreamPublisher.class);
private final CloudFoundryClient client;
private final DopplerClient dopplerClient;
private final ApplicationIdsProvider applicationIdsProvider;
public DopplerLogStreamPublisher(
CloudFoundryClient client,
DopplerClient dopplerClient,
ApplicationIdsProvider applicationIdsProvider
) {
this.client = client;
this.dopplerClient = dopplerClient;
this.applicationIdsProvider = applicationIdsProvider;
}
@Override
public Flux<Envelope> getLogStream(String serviceInstanceId) {
return this.applicationIdsProvider
.getApplicationIds(serviceInstanceId)
.doOnNext(id -> LOG.debug("Starting log streaming for app with ID {}", id))
.flatMap(this::createApplicationStreamer);
}
protected Flux<Envelope> createApplicationStreamer(String applicationId) {
return client.applicationsV2().get(GetApplicationRequest.builder().applicationId(applicationId).build())
.map(response -> response.getEntity().getName())
.flatMapMany(appName ->
dopplerClient.stream(StreamRequest.builder().applicationId(applicationId).build())
.map(envelope -> LoggingUtils.injectAppNameIntoLogSourceInstance(appName, envelope))
);
}
}

View File

@@ -1,25 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging.streaming;
import reactor.core.publisher.Flux;
public interface LogStreamPublisher<T> {
Flux<T> getLogStream(String serviceInstanceId);
}

View File

@@ -1,27 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging.streaming.endpoint;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code = HttpStatus.NOT_FOUND)
class ServiceInstanceNotFoundException extends RuntimeException {
private static final long serialVersionUID = 8672053621926030384L;
}

View File

@@ -1,122 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging.streaming.endpoint;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.cloudfoundry.dropsonde.events.Envelope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import org.springframework.cloud.appbroker.logging.streaming.events.ServiceInstanceLogEvent;
import org.springframework.cloud.appbroker.logging.streaming.events.StartServiceInstanceLoggingEvent;
import org.springframework.cloud.appbroker.logging.streaming.events.StopServiceInstanceLoggingEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationListener;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketSession;
import org.springframework.web.util.UriTemplate;
public class StreamingLogWebSocketHandler implements WebSocketHandler, ApplicationListener<ServiceInstanceLogEvent> {
private static final Logger LOG = LoggerFactory.getLogger(StreamingLogWebSocketHandler.class);
private static final UriTemplate LOGGING_URI_TEMPLATE = new UriTemplate("/logs/{serviceInstanceId}/stream");
private final ApplicationEventPublisher eventPublisher;
private final ConcurrentHashMap<String, Sinks.One<Envelope>> envelopeSinks = new ConcurrentHashMap<>();
public StreamingLogWebSocketHandler(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
@Override
public Mono<Void> handle(WebSocketSession session) {
String serviceInstanceId = getServiceInstanceId(session);
LOG.info("Connection established [{}}], service instance {}",
session.getHandshakeInfo().getRemoteAddress(),
serviceInstanceId);
Sinks.One<Envelope> envelopeSink = envelopeSinks
.computeIfAbsent(serviceInstanceId, s -> Sinks.one());
eventPublisher.publishEvent(new StartServiceInstanceLoggingEvent(this, serviceInstanceId));
LOG.info("Published event to start streaming logs for service instance with ID {}", serviceInstanceId);
return session
.send(envelopeSink.asMono().map(envelope -> session
.binaryMessage(dataBufferFactory -> dataBufferFactory.wrap(Envelope.ADAPTER.encode(envelope)))))
.then()
.doFinally(signalType -> afterConnectionClosed(session))
.doOnError(throwable -> LOG
.error("Error handling logging stream for service instance " + serviceInstanceId, throwable));
}
@Override
public void onApplicationEvent(ServiceInstanceLogEvent event) {
broadcastLogMessage(event);
}
public void broadcastLogMessage(ServiceInstanceLogEvent event) {
if (LOG.isDebugEnabled()) {
LOG.debug("Received event to broadcast log message for " + event.getServiceInstanceId());
}
Sinks.One<Envelope> envelopeSink = this.envelopeSinks.get(event.getServiceInstanceId());
if (envelopeSink == null) {
if (LOG.isWarnEnabled()) {
LOG.warn("No sink found for {}, stopping log streaming", event.getServiceInstanceId());
}
eventPublisher.publishEvent(new StopServiceInstanceLoggingEvent(this, event.getServiceInstanceId()));
return;
}
if (LOG.isDebugEnabled()) {
LOG.debug("Sending message to client for {}", event.getServiceInstanceId());
}
envelopeSink.tryEmitValue(event.getEnvelope());
}
private void afterConnectionClosed(WebSocketSession webSocketSession) {
if (LOG.isInfoEnabled()) {
LOG.info("Connection closed [" + webSocketSession.getHandshakeInfo().getRemoteAddress() + "]");
}
final String serviceInstanceId = getServiceInstanceId(webSocketSession);
eventPublisher.publishEvent(new StopServiceInstanceLoggingEvent(this, serviceInstanceId));
envelopeSinks.computeIfPresent(serviceInstanceId, (s, envelopeSink) -> null);
}
private String getServiceInstanceId(WebSocketSession webSocketSession) {
URI uri = webSocketSession.getHandshakeInfo().getUri();
final Map<String, String> match = LOGGING_URI_TEMPLATE.match(uri.getPath());
if (match.isEmpty()) {
throw new ServiceInstanceNotFoundException();
}
return match.get("serviceInstanceId");
}
}

View File

@@ -1,45 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging.streaming.events;
import org.cloudfoundry.dropsonde.events.Envelope;
import org.springframework.context.ApplicationEvent;
public class ServiceInstanceLogEvent extends ApplicationEvent {
private static final long serialVersionUID = 4391048666734944603L;
private final String serviceInstanceId;
private final Envelope envelope;
public ServiceInstanceLogEvent(Object source, String serviceInstanceId, Envelope envelope) {
super(source);
this.serviceInstanceId = serviceInstanceId;
this.envelope = envelope;
}
public String getServiceInstanceId() {
return serviceInstanceId;
}
public Envelope getEnvelope() {
return envelope;
}
}

View File

@@ -1,56 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging.streaming.events;
import org.springframework.context.ApplicationEvent;
public class ServiceInstanceLoggingEvent extends ApplicationEvent {
private static final long serialVersionUID = 3721553379568462887L;
public enum Operation {
/**
* Start publishing log stream for a given service instance id
*/
START,
/**
* Stop publishing log stream for a given service instance id
*/
STOP
}
private final String serviceInstanceId;
private final Operation operation;
public ServiceInstanceLoggingEvent(Object source, String serviceInstanceId, Operation operation) {
super(source);
this.serviceInstanceId = serviceInstanceId;
this.operation = operation;
}
public String getServiceInstanceId() {
return serviceInstanceId;
}
public Operation getOperation() {
return operation;
}
}

View File

@@ -1,27 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging.streaming.events;
public class StartServiceInstanceLoggingEvent extends ServiceInstanceLoggingEvent {
private static final long serialVersionUID = -5940715663862240039L;
public StartServiceInstanceLoggingEvent(Object source, String serviceInstanceId) {
super(source, serviceInstanceId, Operation.START);
}
}

View File

@@ -1,27 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging.streaming.events;
public class StopServiceInstanceLoggingEvent extends ServiceInstanceLoggingEvent {
private static final long serialVersionUID = 2858399700112202361L;
public StopServiceInstanceLoggingEvent(Object source, String serviceInstanceId) {
super(source, serviceInstanceId, Operation.STOP);
}
}

View File

@@ -1,46 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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 com.example.recentlog;
import java.util.UUID;
import reactor.core.publisher.Flux;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.appbroker.logging.ApplicationIdsProvider;
import org.springframework.cloud.servicebroker.autoconfigure.web.ServiceBrokerAutoConfiguration;
import org.springframework.cloud.servicebroker.autoconfigure.web.reactive.ServiceBrokerWebFluxAutoConfiguration;
import org.springframework.context.annotation.Bean;
@SpringBootApplication(exclude = {
ServiceBrokerAutoConfiguration.class,
ServiceBrokerWebFluxAutoConfiguration.class
})
public class RecentLogsTestApp {
static final String APP_ID = UUID.randomUUID().toString();
public static String getAppId() {
return APP_ID;
}
@Bean
ApplicationIdsProvider applicationIdsProvider() {
return serviceInstanceId -> Flux.just(APP_ID);
}
}

View File

@@ -1,65 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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 com.example.streaming;
import java.util.UUID;
import reactor.core.publisher.Flux;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.appbroker.logging.ApplicationIdsProvider;
import org.springframework.cloud.appbroker.logging.streaming.events.StopServiceInstanceLoggingEvent;
import org.springframework.cloud.servicebroker.autoconfigure.web.ServiceBrokerAutoConfiguration;
import org.springframework.cloud.servicebroker.autoconfigure.web.reactive.ServiceBrokerWebFluxAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.EventListener;
@SpringBootApplication(exclude = {
ServiceBrokerAutoConfiguration.class,
ServiceBrokerWebFluxAutoConfiguration.class
})
public class LogStreamingTestApp {
private static final String APP_ID = UUID.randomUUID().toString();
private static boolean receivedStopEvent;
private static String receivedStopEventServiceInstanceId;
public static String getAppId() {
return APP_ID;
}
public static boolean isReceivedStopEvent() {
return receivedStopEvent;
}
public static String getReceivedStopEventServiceInstanceId() {
return receivedStopEventServiceInstanceId;
}
@Bean
ApplicationIdsProvider applicationIdsProvider() {
return serviceInstanceId -> Flux.just(APP_ID);
}
@EventListener
public void onStop(StopServiceInstanceLoggingEvent stopServiceInstanceLoggingEvent) {
receivedStopEventServiceInstanceId = stopServiceInstanceLoggingEvent.getServiceInstanceId();
receivedStopEvent = true;
}
}

View File

@@ -1,98 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging.recent;
import java.time.Instant;
import java.util.UUID;
import com.example.recentlog.RecentLogsTestApp;
import org.cloudfoundry.client.CloudFoundryClient;
import org.cloudfoundry.client.v2.applications.ApplicationEntity;
import org.cloudfoundry.client.v2.applications.GetApplicationRequest;
import org.cloudfoundry.client.v2.applications.GetApplicationResponse;
import org.cloudfoundry.doppler.DopplerClient;
import org.cloudfoundry.doppler.Envelope;
import org.cloudfoundry.doppler.EventType;
import org.cloudfoundry.doppler.LogMessage;
import org.cloudfoundry.doppler.MessageType;
import org.cloudfoundry.doppler.RecentLogsRequest;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Answers;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.mockito.BDDMockito.given;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = RecentLogsTestApp.class)
class ServiceInstanceRecentLogsTest {
@LocalServerPort
int port;
@MockBean
DopplerClient dopplerClient;
@MockBean(answer = Answers.RETURNS_DEEP_STUBS)
CloudFoundryClient cloudFoundryClient;
private String expectedTestMessage;
@BeforeEach
void setUp() {
expectedTestMessage = "test message " + UUID.randomUUID();
RecentLogsRequest request = RecentLogsRequest.builder().applicationId(RecentLogsTestApp.getAppId()).build();
LogMessage testMessage = LogMessage
.builder()
.message(expectedTestMessage)
.timestamp(Instant.now().toEpochMilli())
.messageType(MessageType.OUT).build();
Envelope testEnvelope = Envelope
.builder()
.eventType(EventType.LOG_MESSAGE).origin("test")
.logMessage(testMessage).build();
given(dopplerClient.recentLogs(request))
.willReturn(Flux.just(testEnvelope));
given(cloudFoundryClient.applicationsV2()
.get(GetApplicationRequest.builder().applicationId(RecentLogsTestApp.getAppId()).build()))
.willReturn(Mono.just(
GetApplicationResponse.builder().entity(ApplicationEntity.builder().name("test-app").build()).build()));
}
@Test
void shouldFetchLogs() {
String serviceInstanceId = UUID.randomUUID().toString();
WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port).build();
client.get().uri("/logs/{serviceInstanceId}/recentlogs", serviceInstanceId)
.exchange()
.expectStatus().isOk()
.expectBody(String.class)
.value(Matchers.containsString(expectedTestMessage));
}
}

View File

@@ -1,174 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.appbroker.logging.streaming;
import java.io.IOException;
import java.net.URI;
import java.time.Instant;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import com.example.streaming.LogStreamingTestApp;
import org.cloudfoundry.client.CloudFoundryClient;
import org.cloudfoundry.client.v2.applications.ApplicationEntity;
import org.cloudfoundry.client.v2.applications.GetApplicationRequest;
import org.cloudfoundry.client.v2.applications.GetApplicationResponse;
import org.cloudfoundry.doppler.DopplerClient;
import org.cloudfoundry.doppler.EventType;
import org.cloudfoundry.doppler.MessageType;
import org.cloudfoundry.doppler.StreamRequest;
import org.cloudfoundry.dropsonde.events.Envelope;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Answers;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.cloud.appbroker.logging.LoggingUtils;
import org.springframework.cloud.appbroker.logging.streaming.events.ServiceInstanceLogEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
import org.springframework.web.reactive.socket.client.WebSocketClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.mockito.BDDMockito.given;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = LogStreamingTestApp.class)
class ServiceInstanceLogStreamingTest {
@LocalServerPort
int port;
@MockBean
DopplerClient dopplerClient;
@MockBean(answer = Answers.RETURNS_DEEP_STUBS)
CloudFoundryClient cloudFoundryClient;
@Autowired
ApplicationEventPublisher applicationEventPublisher;
private Envelope expectedEnvelope;
private final AtomicReference<Envelope> actualEnvelope = new AtomicReference<>();
private String serviceInstanceId;
@BeforeEach
void setUp() {
serviceInstanceId = UUID.randomUUID().toString();
String expectedTestMessage = "test message " + serviceInstanceId;
org.cloudfoundry.doppler.LogMessage testMessage = org.cloudfoundry.doppler.LogMessage
.builder()
.message(expectedTestMessage)
.timestamp(Instant.now().toEpochMilli())
.messageType(MessageType.OUT).build();
org.cloudfoundry.doppler.Envelope testEnvelope = org.cloudfoundry.doppler.Envelope
.builder()
.eventType(EventType.LOG_MESSAGE).origin("test")
.logMessage(testMessage).build();
// expectations are instances of Dropsnode Envelope + LogMessage
Envelope envelope = LoggingUtils.convertDopplerEnvelopeToDropsonde(testEnvelope);
org.cloudfoundry.dropsonde.events.LogMessage expectedMessage = new org.cloudfoundry.dropsonde.events.LogMessage.Builder()
.message_type(envelope.logMessage.message_type)
.source_instance("test-app null")
.timestamp(envelope.logMessage.timestamp)
.message(envelope.logMessage.message)
.build();
expectedEnvelope = new Envelope.Builder()
.eventType(envelope.eventType)
.logMessage(expectedMessage)
.origin("test")
.build();
StreamRequest request = StreamRequest.builder().applicationId(LogStreamingTestApp.getAppId()).build();
given(dopplerClient.stream(request)).willReturn(Flux.just(testEnvelope));
given(cloudFoundryClient.applicationsV2().get(
GetApplicationRequest.builder().applicationId(LogStreamingTestApp.getAppId()).build()))
.willReturn(Mono.just(
GetApplicationResponse.builder().entity(ApplicationEntity.builder().name("test-app").build()).build()));
}
@Test
void shouldPublishWebSocketEndpoint() {
Disposable subscription = connectToLogsStreamEndpoint();
await().untilAsserted(() -> assertThat(actualEnvelope).hasValue(expectedEnvelope));
subscription.dispose();
}
@Test
void shouldPublishEventOnDisconnect() {
Disposable subscription = connectToLogsStreamEndpoint();
await().untilAsserted(() -> assertThat(actualEnvelope.get()).isNotNull());
subscription.dispose();
await().untilAsserted(() -> assertThat(LogStreamingTestApp.isReceivedStopEvent()).isTrue());
}
@Test
void shouldStopStreamingIfNoClient() {
// CLI plugin doesn't always handle disconnect gracefully, so sometimes it is possible that log stream is
// being published but there is no listener. In this case log streaming should be stopped.
Disposable subscription = connectToLogsStreamEndpoint();
subscription.dispose();
applicationEventPublisher.publishEvent(new ServiceInstanceLogEvent(this, serviceInstanceId, expectedEnvelope));
await().untilAsserted(() -> assertThat(LogStreamingTestApp.getReceivedStopEventServiceInstanceId()).isEqualTo(serviceInstanceId));
}
private Disposable connectToLogsStreamEndpoint() {
URI uri = URI.create("ws://localhost:" + port + "/logs/" + serviceInstanceId + "/stream");
WebSocketClient client = new ReactorNettyWebSocketClient();
return client.execute(uri, getWebSocketHandler()).subscribe();
}
private WebSocketHandler getWebSocketHandler() {
return session -> session
.receive()
.doOnNext(message -> {
DataBuffer buffer = message.getPayload();
try {
actualEnvelope.set(Envelope.ADAPTER.decode(
buffer.asInputStream()));
}
catch (IOException e) {
throw new RuntimeException(e);
}
})
.then();
}
}

View File

@@ -1,22 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.
*/
description = "Spring Cloud App Broker Starter for Logging"
dependencies {
api project(":spring-cloud-starter-app-broker")
api project(":spring-cloud-app-broker-logging")
}