I want to create a new endpoint that extends the existing jhimetrics endpoint (or extend the results of the existing jhimetrics). The application is generated with JHipster.
So what I have done is:
add the new endpoint to the array in application.yml file, specifically:
management:
endpoints:
web:
base-path: /management
exposure:
include: [ ..., "health", "info", "jhimetrics", "roxhens"]
created the ExtendedMetricsEndpoint.java with the following content:
// imports, etc...
#Endpoint(id = "roxhens")
public class ExtendedMetricsEndpoint {
private final JHipsterMetricsEndpoint delegate;
private final SimpUserRegistry simpUserRegistry;
public ExtendedMetricsEndpoint(
JHipsterMetricsEndpoint delegate,
SimpUserRegistry simpUserRegistry
) {
this.delegate = delegate;
this.simpUserRegistry = simpUserRegistry;
}
#ReadOperation
public Map<String, Map> getMetrics() {
Map<String, Map> metrics = this.delegate.allMetrics();
HashMap<String, Integer> activeUsers = new HashMap<>();
activeUsers.put("activeUsers", this.simpUserRegistry.getUserCount());
metrics.put("customMetrics", new HashMap(activeUsers));
return metrics;
}
}
created the configuration file for this endpoint:
// imports etc...
#Configuration
#ConditionalOnClass(Timed.class)
#AutoConfigureAfter(JHipsterMetricsEndpointConfiguration.class)
public class ExtendedMetricsEndpointConfiguration {
#Bean
#ConditionalOnBean({JHipsterMetricsEndpoint.class, SimpUserRegistry.class})
#ConditionalOnMissingBean
#ConditionalOnAvailableEndpoint
public ExtendedMetricsEndpoint extendedMetricsEndpoint(JHipsterMetricsEndpoint jHipsterMetricsEndpoint, SimpUserRegistry simpUserRegistry) {
return new ExtendedMetricsEndpoint(jHipsterMetricsEndpoint, simpUserRegistry);
}
}
What step am I missing here, or what am I doing wrong?
I had the same issue and after 2 days of struggle I was able to find a solution which works for me:
#Component
#WebEndpoint(id = "xxxmetrics")
public class XXXMetricsEndpoint {
private final MeterRegistry meterRegistry;
public SrnMetricsEndpoint(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
#ReadOperation
public Map<String, Map> allMetrics() {
Map<String, Map> stringMapMap = new LinkedHashMap<>();
return stringMapMap;
}
}
Application yml:
management:
endpoints:
web:
base-path: /management
exposure:
include: [... , 'health', 'info', 'jhimetrics', 'xxxmetrics' ,'metrics', 'logfile']
This way the request: /management/xxxmetrics works.
The spring docs: https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-endpoints-custom
Edit:
spring version: 5.1.10, spring-boot-actuator: 2.1.9
Related
My application.yml looks like so
service:
cloud:
piglet:
published-host: http://localhost:29191
webhook:
headers:
gitlab: X-Gitlab-Token
The Config class
#Component
#ConfigurationProperties(prefix = "service.cloud.piglet.webhook")
public class WebhooksConsumerTokenHeadersProperties {
private final Map<String, String> headers;
public WebhooksConsumerTokenHeadersProperties(Map<String, String> headers) {
this.headers = headers;
}
public String getTokenHeaderName(String app) {
return headers.get(app);
}
}
I ran on debug and noticed that the headers map is null when initialising it in the constructor.
I have this test project which I would like to migrate to more recent version:
#Configuration
public class LoadbalancerConfig extends RibbonLoadBalancerClient {
public LoadbalancerConfig(SpringClientFactory clientFactory) {
super(clientFactory);
}
}
Full code example: https://github.com/rcbandit111/Generic_SO_POC/blob/master/src/main/java/org/merchant/database/service/sql/LoadbalancerConfig.java
Do you know how I can migrate this code to latest load balancer version?
I think examining the RibbonAutoConfiguration class gives you a good hint of how you should configure things.
First remove #Configuration from LoadbalancerConfig, I also renamed LoadbalancerConfig to CustomLoadbalancer to prevent confusion.
public class CustomLoadbalancer extends RibbonLoadBalancerClient {
public CustomLoadbalancer(SpringClientFactory clientFactory) {
super(clientFactory);
}
}
add the following dependency to your gradle
com.netflix.ribbon:ribbon:2.7.18
then add a configuration class like:
#Configuration
#ConditionalOnClass({Ribbon.class})
#AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
#ConditionalOnProperty(value = "spring.cloud.loadbalancer.ribbon.enabled",
havingValue = "true", matchIfMissing = true)
#AutoConfigureBefore(LoadBalancerAutoConfiguration.class)
public class LoadBalancerClientConfig {
#Autowired(required = false)
private List<RibbonClientSpecification> configurations = new ArrayList<>();
#Bean
public CustomLoadbalancer customLoadbalancer() {
return new CustomLoadbalancer(springClientFactory());
}
#Bean
public SpringClientFactory springClientFactory() {
SpringClientFactory factory = new SpringClientFactory();
factory.setConfigurations(this.configurations);
return factory;
}
}
If you want to use Spring cloud load balancer instead of above configuration add spring-cloud-starter-loadbalancer dependency to your gradle.build and for configuration you only need this bean:
#LoadBalanced
#Bean
RestTemplate getRestTemplate() {
return new RestTemplate();
}
This RestTemplate pretty works identical to standard RestTemplate class, except instead of using physical location of the service, you need to build the URL using Eureka service ID.
Here is an example of how could you possibly use it in your code
#Component
public class LoadBalancedTemplateClient {
#Autowired
RestTemplate restTemplate;
#Autowired
User user;
public ResponseEntity<Result> getResult() {
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder
.fromHttpUrl("http://application-id/v1/") // application id registered in eureka
.queryParam("id", user.getUserId());
return restTemplate.getForEntity(uriComponentsBuilder.toUriString(),
Result.class);
}
}
Also if you wish to use reactive client the process is the same first define the bean:
#LoadBalanced
#Bean
WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
and then inject and use it when you need:
#Autowired
private WebClient.Builder webClient;
public Mono<String> doSomething() {
return webClient
.build()
.get()
.uri("http://application-id/v1/")
.retrieve()
.bodyToMono(String.class);
}
Also you can check documentation for additional information: documentation
I am new to Spring Boot and trying to implement multi-tenancy architecture using Spring boot 2, hibernate and flyway. I was referring to tutorial https://reflectoring.io/flyway-spring-boot-multitenancy/ to understand the concepts and was able to implement the architecture as mentioned.
However, if I add a new field entity classes, everything breaks because hibernate does not create new fields in tenant databases. From reading theory and stackoverflow questions, I understand that flyway is to solve this problem. However, i am not able to make it work.
Can somebody tell me where I am going wrong. My requirement is - When I add a new field to entity class, all tables in all tenant databases should get updated with that field. Below is the code
Application Properties
spring:
jpa:
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
flyway:
enabled: false
tenants:
datasources:
vw:
jdbcUrl: jdbc:mysql://localhost:3306/vw
driverClassName: com.mysql.jdbc.Driver
username: vikky
password: Test#123
bmw:
jdbcUrl: jdbc:mysql://localhost:3306/bmw
driverClassName: com.mysql.jdbc.Driver
username: vikky
password: Test#123
Datasource Configuration
#Configuration
public class DataSourceConfiguration {
private final DataSourceProperties dataSourceProperties;
public DataSourceConfiguration(DataSourceProperties dataSourceProperties) {
this.dataSourceProperties = dataSourceProperties;
}
#Bean
public DataSource dataSource() {
TenantRoutingDataSource customDataSource = new TenantRoutingDataSource();
customDataSource.setTargetDataSources(dataSourceProperties.getDatasources());
return customDataSource;
}
#PostConstruct
public void migrate() {
dataSourceProperties
.getDatasources()
.values()
.stream()
.map(dataSource -> (DataSource) dataSource)
.forEach(this::migrate);
}
private void migrate(DataSource dataSource) {
Flyway flyway = Flyway.configure().dataSource(dataSource).load();
flyway.migrate();
}
}
DataSource properties
#Component
#ConfigurationProperties(prefix = "tenants")
public class DataSourceProperties {
private Map<Object, Object> datasources = new LinkedHashMap<>();
public Map<Object, Object> getDatasources() {
return datasources;
}
public void setDatasources(Map<String, Map<String, String>> datasources) {
datasources
.forEach((key, value) -> this.datasources.put(key, convert(value)));
}
public DataSource convert(Map<String, String> source) {
return DataSourceBuilder.create()
.url(source.get("jdbcUrl"))
.driverClassName(source.get("driverClassName"))
.username(source.get("username"))
.password(source.get("password"))
.build();
}
}
Tenant Routing Data Source
public class TenantRoutingDataSource extends AbstractRoutingDataSource {
#Override
protected Object determineCurrentLookupKey() {
return ThreadTenantStorage.getTenantId();
}
}
Header Interceptor
#Component
public class HeaderTenantInterceptor implements WebRequestInterceptor {
public static final String TENANT_HEADER = "X-tenant";
#Override
public void preHandle(WebRequest request) throws Exception {
ThreadTenantStorage.setTenantId(request.getHeader(TENANT_HEADER));
}
#Override
public void postHandle(WebRequest request, ModelMap model) throws Exception {
ThreadTenantStorage.clear();
}
#Override
public void afterCompletion(WebRequest request, Exception ex) throws Exception {
}
}
There are other classes as well like Web Configuration, controllers etc. but I don't they are required to be posted here.
After lot of research, I understood that flyway is required only in case of production, where we do not want to update table definition using ddl-auto=true. Since that was not the case with me, I added below configuration to update all schemas according to entity structure
#Configuration
public class AutoDDLConfig
{
#Value("${spring.datasource.username}")
private String username;
#Value("${spring.datasource.password}")
private String password;
#Value("${schemas.list}")
private String schemasList;
#Bean
public void bb()
{
if (StringUtils.isBlank(schemasList))
{
return;
}
String[] tenants = schemasList.split(",");
for (String tenant : tenants)
{
tenant = tenant.trim();
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver"); // Change here to MySql Driver
dataSource.setSchema(tenant);
dataSource.setUrl("jdbc:mysql://localhost/" + tenant
+ "?autoReconnect=true&characterEncoding=utf8&useSSL=false&useTimezone=true&serverTimezone=Asia/Kolkata&useLegacyDatetimeCode=false&allowPublicKeyRetrieval=true");
dataSource.setUsername(username);
dataSource.setPassword(password);
LocalContainerEntityManagerFactoryBean emfBean = new LocalContainerEntityManagerFactoryBean();
emfBean.setDataSource(dataSource);
emfBean.setPackagesToScan("com"); // Here mention JPA entity path / u can leave it scans all packages
emfBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
emfBean.setPersistenceProviderClass(HibernatePersistenceProvider.class);
Map<String, Object> properties = new HashMap<>();
properties.put("hibernate.hbm2ddl.auto", "update");
properties.put("hibernate.default_schema", tenant);
emfBean.setJpaPropertyMap(properties);
emfBean.setPersistenceUnitName(dataSource.toString());
emfBean.afterPropertiesSet();
}
}
}
How can I register a custom converter in my MongoTemplate with Spring Boot? I would like to do this only using annotations if possible.
I just register the bean:
#Bean
public MongoCustomConversions mongoCustomConversions() {
List list = new ArrayList<>();
list.add(myNewConverter());
return new MongoCustomConversions(list);
}
Here is a place in source code where I find it
If you only want to override the custom converters portion of the Spring Boot configuration, you only need to create a configuration class that provides a #Bean for the custom converters. This is handy if you don't want to override all of the other Mongo settings (URI, database name, host, port, etc.) that Spring Boot has wired in for you from your application.properties file.
#Configuration
public class MongoConfig
{
#Bean
public CustomConversions customConversions()
{
List<Converter<?, ?>> converterList = new ArrayList<Converter<?, ?>>();
converterList.add(new MyCustomWriterConverter());
return new CustomConversions(converterList);
}
}
This will also only work if you've enabled AutoConfiguration and excluded the DataSourceAutoConfig:
#SpringBootApplication(scanBasePackages = {"com.mypackage"})
#EnableMongoRepositories(basePackages = {"com.mypackage.repository"})
#EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class MyApplication
{
public static void main(String[] args)
{
SpringApplication.run(MyApplication.class, args);
}
}
In this case, I'm setting a URI in the application.properties file and using Spring data repositories:
#mongodb settings
spring.data.mongodb.uri=mongodb://localhost:27017/mydatabase
spring.data.mongodb.repositories.enabled=true
You need to create a configuration class for converter config.
#Configuration
#EnableAutoConfiguration(exclude = { EmbeddedMongoAutoConfiguration.class })
#Profile("!testing")
public class MongoConfig extends AbstractMongoConfiguration {
#Value("${spring.data.mongodb.host}") //if it is stored in application.yml, else hard code it here
private String host;
#Value("${spring.data.mongodb.port}")
private Integer port;
#Override
protected String getDatabaseName() {
return "test";
}
#Bean
public Mongo mongo() throws Exception {
return new MongoClient(host, port);
}
#Override
public String getMappingBasePackage() {
return "com.base.package";
}
#Override
public CustomConversions customConversions() {
List<Converter<?, ?>> converters = new ArrayList<>();
converters.add(new LongToDateTimeConverter());
return new CustomConversions(converters);
}
}
#ReadingConverter
static class LongToDateTimeConverter implements Converter<Long, Date> {
#Override
public Date convert(Long source) {
if (source == null) {
return null;
}
return new Date(source);
}
}
In the springboot project that I work on there is a transitive maven dependency on spring-data-mongodb. Therefore the MongoHealthIndicator seems to be activated automatically although the project does not actually use mongodb. Is it possible to deactivate specifically this HealthIndicator without deactivating the actuator health endpoint? A workaround that I found is excluding the dependency. But I was wondering if it is possible to do this specific deactivation of the MongoHealthIndicator.
From:
http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
# HEALTH INDICATORS (previously health.*)
...
management.health.mongo.enabled=true
...
You should be able to set that to false to disable the health indicator. From org.springframework.boot.actuate.autoconfigure.HealthIndicatorAutoConfiguration.java
#Configuration
#ConditionalOnBean(MongoTemplate.class)
#ConditionalOnProperty(prefix = "management.health.mongo", name = "enabled", matchIfMissing = true)
public static class MongoHealthIndicatorConfiguration {
Try this in your application.properties
management.health.mongo.enabled=false
application.properties
management.health.mongo.enabled=false
endpoints.mongo.enabled=true
MongoDBHealthCheckEndPoint.java
#ConfigurationProperties(prefix = "endpoints.mongo", ignoreUnknownFields = true)
#Component
public class MongoDBHealthCheckEndPoint extends AbstractEndpoint<Map<String, String>>
{
#Inject
MongoTemplate mongoTemplate;
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final Map<String, String> UP = new HashMap<String, String>() {{
put("mongo.status", "UP");
}};
private static final Map<String, String> DOWN = new HashMap<String, String>() {{
put("mongo.status", "DOWN");
}};
public MongoDBHealthCheckEndPoint() {
super("mongo", false);
}
public MongoDBHealthCheckEndPoint(Map<String, ? extends Object> mongo) {
super("mongo", false);
}
public Map<String, String> invoke() {
try {
return (new MongoHealthIndicator(mongoTemplate).health().getStatus().equals(Status.UP)) ? UP : DOWN;
} catch (Exception e) {
log.error("mongo database is down", e);
return DOWN;
}
}
}