Spring Boot Micrometer metrics for MongoDB - mongodb

I use spring boot 2.2.5 + micrometer 1.3.5 + starter-data-mongodb
Under "io.micrometer.core.instrument.binder.mongodb" I can see 2 classes CommandListener and ConnectionPoolListener. I would like to know what purpose these serve?
In actuator metrics endpoint, mongo metrics are not available.
How do I enable metrics for mongodb in actuator? For example, actuator automatically shows several metrics of RabbitMQ. I was expecting something similar in case of MongoDB as well.
Should I create my own metrics?

In order to enable Spring Boot applying its AutoConfiguration I suggest to use the customizer pattern:
Kotlin:
#Configuration
class MongoConfiguration {
#Bean
fun mongoClientSettingsBuilderCustomizer(meterRegistry: MeterRegistry) =
MongoClientSettingsBuilderCustomizer {
it.addCommandListener(MongoMetricCommandListener(meterRegistry))}
}
Java:
#Configuration
public class MongoConfiguration {
#Bean
public MongoClientSettingsBuilderCustomizer mongoClientSettingsBuilderCustomizer(MeterRegistry meterRegistry) {
return builder -> builder.addCommandListener(new MongoMetricsCommandListener(meterRegistry));
}
}
Please note you currenly will neither see a relation to the spring data repository nor to the mongo collection in the metrics. see open issue
EDIT (07/30/2021):
The issues has been fixed, so you likely get collection metrics in a current release.

Adding those listeners is not as straighforward as i thought and it's quite dependend on which properties you use for configuring Spring Data MongoDB.
The key for the integration is to customize the com.mongodb.MongoClientSettings instance which is used for creating the MongoClient. There are multiple possiblities to do so as documented in Connecting to MongoDB with Spring
The following is a working example (simplified from an application of ours) based on Spring Boot 2.3 which assumes you are using spring.data.mongodb.uri to specify the connection string in your application.properties.
package com.example.demo;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.mongodb.MongoMetricsCommandListener;
import io.micrometer.core.instrument.binder.mongodb.MongoMetricsConnectionPoolListener;
import org.springframework.boot.autoconfigure.mongo.MongoProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoClientFactoryBean;
#Configuration
public class MongoConfiguration {
#Bean
public MongoClientFactoryBean mongoClientFactoryBean(MongoProperties properties, MeterRegistry meterRegistry) {
MongoClientFactoryBean mongoClientFactoryBean = new MongoClientFactoryBean();
mongoClientFactoryBean.setConnectionString(new ConnectionString(properties.getUri()));
MongoClientSettings settings = MongoClientSettings.builder()
.addCommandListener(new MongoMetricsCommandListener(meterRegistry))
.applyToConnectionPoolSettings(builder ->
builder.addConnectionPoolListener(new MongoMetricsConnectionPoolListener(meterRegistry)))
.build();
mongoClientFactoryBean.setMongoClientSettings(settings);
return mongoClientFactoryBean;
}
}
Unfortunately a lot has in configuring MongoDB from Spring Boot 2.2 to 2.3. If you cannot use Spring Boot 2.3 and you are stuck in backporting this to 2.2 please let me know.

Short answer
Create a MongoClientOptions bean with addCommandListener and you are good to go.
#Configuration
public class MongoConfiguration {
#Autowired
private MeterRegistry meterRegistry;
#Bean
public MongoClientOptions myMongoClientOptions() {
return MongoClientOptions.builder()
.addCommandListener(new MongoMetricsCommandListener(meterRegistry)).build();
}
}
#chargue's answer won't work for certain version of spring-data-mongodb. Because org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration initializes MongoClient with MongoClientOptions, not MongoClientSettings. It might work in newer version of spring-data-mongodb cuz MongoClientSettings is the recommended way according to mongodb's documents.
#Bean
#ConditionalOnMissingBean(type = { "com.mongodb.MongoClient", "com.mongodb.client.MongoClient" })
public MongoClient mongo(MongoProperties properties, ObjectProvider<MongoClientOptions> options,
Environment environment) {
return new MongoClientFactory(properties, environment).createMongoClient(options.getIfAvailable());
}
Make sure prometheus and micrometer is correctlly setup. You should see mongo metrics in prometheus endpoint like below:
# HELP process_cpu_usage The "recent cpu usage" for the Java Virtual Machine process
# TYPE process_cpu_usage gauge
process_cpu_usage{application="",} 0.004362672325272289
# HELP mongodb_driver_commands_seconds_max Timer of mongodb commands
# TYPE mongodb_driver_commands_seconds_max gauge
mongodb_driver_commands_seconds_max{application="",cluster_id="60b0d12d73b6df671cb4d882",command="find",server_address="",status="SUCCESS",} 34.684200332
mongodb_driver_commands_seconds_max{application="",cluster_id="60b0d12d73b6df671cb4d882",command="buildInfo",server_address="",status="SUCCESS",} 0.263514375
# HELP mongodb_driver_commands_seconds Timer of mongodb commands
# TYPE mongodb_driver_commands_seconds summary
mongodb_driver_commands_seconds_count{application="",cluster_id="60b0d12d73b6df671cb4d882",command="find",server_address="",status="SUCCESS",} 1.0
mongodb_driver_commands_seconds_sum{application="",cluster_id="60b0d12d73b6df671cb4d882",command="find",server_address="",status="SUCCESS",} 34.684200332
mongodb_driver_commands_seconds_count{application="",cluster_id="60b0d12d73b6df671cb4d882",command="buildInfo",server_address="",status="SUCCESS",} 1.0
mongodb_driver_commands_seconds_sum{application="",cluster_id="60b0d12d73b6df671cb4d882",command="buildInfo",server_address="",status="SUCCESS",} 0.263514375

Related

Spring Cloud Dataflow errorChannel not working

I'm attempting to create a custom exception handler for my Spring Cloud Dataflow stream to route some errors to be requeued and others to be DLQ'd.
To do this I'm utilizing the global Spring Integration "errorChannel" and routing based on exception type.
This is the code for the Spring Integration error router:
package com.acme.error.router;
import com.acme.exceptions.DlqException;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.integration.annotation.MessageEndpoint;
import org.springframework.integration.annotation.Router;
import org.springframework.integration.transformer.MessageTransformationException;
import org.springframework.messaging.Message;
#MessageEndpoint
#EnableBinding({ ErrorMessageChannels.class })
public class ErrorMessageMappingRouter {
private static final Logger LOGGER = LoggerFactory.getLogger(ErrorMessageMappingRouter.class);
public static final String ERROR_CHANNEL = "errorChannel";
#Router(inputChannel = ERROR_CHANNEL)
public String onError(Message<Object> message) {
LOGGER.debug("ERROR ROUTER - onError");
if(message.getPayload() instanceof MessageTransformationException) {
MessageTransformationException exception = (MessageTransformationException) message.getPayload();
Message<?> failedMessage = exception.getFailedMessage();
if(exceptionChainContainsDlq(exception)) {
return ErrorMessageChannels.DLQ_QUEUE_NAME;
}
return ErrorMessageChannels.REQUEUE_CHANNEL;
}
return ErrorMessageChannels.DLQ_QUEUE_NAME;
}
...
}
The error router is picked up by each of the stream apps through a package scan on the Spring Boot App for each:
#ComponentScan(basePackages = { "com.acme.error.router" }
#SpringBootApplication
public class StreamApp {}
When this is deployed and run with the local Spring Cloud Dataflow server (version 1.5.0-RELEASE), and a DlqException is thrown, the message is successfully routed to the onError method in the errorRouter and then placed into the dlq topic.
However, when this is deployed as a docker container with SCDF Kubernetes server (also version 1.5.0-RELEASE), the onError method is never hit. (The log statement at the beginning of the router is never output)
In the startup logs for the stream apps, it looks like the bean is picked up correctly and registers as a listener for the errorChannel, but for some reason, when exceptions are thrown they do not get handled by the onError method in our router.
Startup Logs:
o.s.i.endpoint.EventDrivenConsumer : Adding {router:errorMessageMappingRouter.onError.router} as a subscriber to the 'errorChannel' channel
o.s.i.channel.PublishSubscribeChannel : Channel 'errorChannel' has 1 subscriber(s).
o.s.i.endpoint.EventDrivenConsumer : started errorMessageMappingRouter.onError.router
We are using all default settings for the spring cloud stream and kafka binder configurations:
spring.cloud:
stream:
binders:
kafka:
type: kafka
environment.spring.cloud.stream.kafka.binder.brokers=brokerlist
environment.spring.cloud.stream.kafka.binder.zkNodes=zklist
Edit: Added pod args from kubectl describe <pod>
Args:
--spring.cloud.stream.bindings.input.group=delivery-stream
--spring.cloud.stream.bindings.output.producer.requiredGroups=delivery-stream
--spring.cloud.stream.bindings.output.destination=delivery-stream.enricher
--spring.cloud.stream.binders.xdkafka.environment.spring.cloud.stream.kafka.binder.zkNodes=<zkNodes>
--spring.cloud.stream.binders.xdkafka.type=kafka
--spring.cloud.stream.binders.xdkafka.defaultCandidate=true
--spring.cloud.stream.binders.xdkafka.environment.spring.cloud.stream.kafka.binder.brokers=<brokers>
--spring.cloud.stream.bindings.input.destination=delivery-stream.config-enricher
One other idea we attempted was trying to use the Spring Cloud Stream - spring integration error channel support to send to a broker topic on errors, but since messages don't seem to be landing in the global Spring Integration errorChannel at all, that didn't work either.
Is there anything special we need to do in SCDF Kubernetes to enable the global Spring Integration errorChannel?
What am I missing here?
Update with solution from the comments:
After reviewing your configuration I am now pretty sure I know what
the issue is. You have a multi-binder configuration scenario. Even if
you only deal with a single binder instance the existence of
spring.cloud.stream.binders.... is what's going to make framework
treat it as multi-binder. Basically this a bug -
github.com/spring-cloud/spring-cloud-stream/issues/1384. As you can
see it was fixed but you need to upgrade to Elmhurst.SR2 or grab the
latest snapshot (we're in RC2 and 2.1.0.RELEASE is in few weeks
anyway) – Oleg Zhurakousky
This was indeed the problem with our setup. Instead of upgrading, we just eliminated our multi-binder usage for now and the issue was resolved.
Update with solution from the comments:
After reviewing your configuration I am now pretty sure I know what
the issue is. You have a multi-binder configuration scenario. Even if
you only deal with a single binder instance the existence of
spring.cloud.stream.binders.... is what's going to make framework
treat it as multi-binder. Basically this a bug -
github.com/spring-cloud/spring-cloud-stream/issues/1384. As you can
see it was fixed but you need to upgrade to Elmhurst.SR2 or grab the
latest snapshot (we're in RC2 and 2.1.0.RELEASE is in few weeks
anyway) – Oleg Zhurakousky
This was indeed the problem with our setup. Instead of upgrading, we just eliminated our multi-binder usage for now and the issue was resolved.

Container managed MongoDB Connection in Liberty + Spring Data

We have developed an application in Spring Boot + spring data (backend) + MongoDB and used IBM Websphere Liberty as application Server. We were used "Application Managed DB Connection" in an yml file and enjoyed the benefit of Spring Boot autoconfiguration.
Due to policy changes, we would need to manage our DB Connection in Liberty Server(using mongo feature), in Server.xml. I spent whole day in finding out an good example to do this, but dont find any example in Spring with "Container Managed MongoDB Connection" in IBM Websphere Liberty Server.
Can someone please support here?
Check out this other stackoverflow solution. The following is an extension of how you would use that in your Spring Boot app.
You should be able to inject your datasource the same way. You could even inject it into your configuration and wrap it in a Spring DelegatingDataSource.
#Configuration
public class DataSourceConfiguration {
// This is the last code section from that link above
#Resource(lookup = "jdbc/oracle")
DataSource ds;
#Bean
public DataSource mySpringManagedDS() {
return new DelegatingDataSource(ds);
}
}
Then you should be able to inject the mySpringManagedDS DataSource into your Component, Service, etc.
In the past Liberty had a dedicated mongodb-2.0 feature for the server.xml, however this feature provided pretty minimal benefit, since you still needed to bring your own MongoDB libraries. Also, over time MongoDB made significant breaking changes to their API, including how MongoDB gets configured.
Since the MongoDB API is changing so drastically between releases, we found it better to not provide any new MongoDB features in Liberty and instead suggest that users simply use a CDI producer like this:
CDI producer (holds any configuration too):
#ApplicationScoped
public class MongoProducer {
#Produces
public MongoClient createMongo() {
return new MongoClient(new ServerAddress(), new MongoClientOptions.Builder().build());
}
#Produces
public MongoDatabase createDB(MongoClient client) {
return client.getDatabase("testdb");
}
public void close(#Disposes MongoClient toClose) {
toClose.close();
}
}
Example usage:
#Inject
MongoDatabase db;
#POST
#Path("/add")
#Consumes(MediaType.APPLICATION_JSON)
public void add(CrewMember crewMember) {
MongoCollection<Document> crew = db.getCollection("Crew");
Document newCrewMember = new Document();
newCrewMember.put("Name",crewMember.getName());
newCrewMember.put("Rank",crewMember.getRank());
newCrewMember.put("CrewID",crewMember.getCrewID());
crew.insertOne(newCrewMember);
}
This is just the basics, but the following blog post goes into much greater detail along with code examples:
https://openliberty.io/blog/2019/02/19/mongodb-with-open-liberty.html

Cloud foundry spring boot data source pool configuration

I am using spring boot and use postgres , rabbit mq and deploying application on CF. We figured that we need to set connection pool, we found that whatever configuration we do, on CF we can its max 4 connections, not sure from where we get that number (probably something with buildpack or service config).
In order to resolve that I had to extend AbstractCloudConfig, and that is pain as it turns off other auto configuration so now I have to manually configure rabbit mq connection factory too:(. I have came up with below configuration, but not sure this is right way.
Spring boot version: 1.4
Please advise.
package com.example;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.config.java.AbstractCloudConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
/**
* If we need to modify some some custom service configuration on cloud foundry
* e.g. setting up of connection pools. If we set normally and expose bean, it
* will work fine on local machine. But as it will go to Cloud foundry it
* somehow creates max 4 connections. (Not sure from where this number comes)
*
* Adding this configuration meaning we no longer want to leverage auto
* configuration. As soon as Spring boot sees this bean in cloud profile it will
* turn of auto configuration. Expectation is application is going to take care
* of all configuration. This normally works for most of the applications.
*
* For more information read: https://github.com/dsyer/cloud-middleware-blog
* https://docs.cloudfoundry.org/buildpacks/java/spring-service-bindings.html
*
* Hopefully future release of spring boot will allow us to hijack only
* configuration that we want to do ourselves and rest will be auto
* configuration specifically in context with CloudFoundry.
*
*/
#Configuration
#Profile("cloud")
public class CloudServicesConfig extends AbstractCloudConfig {
#Value("${vcap.services.postgres.credentials.jdbc_uri}")
private String postgresUrl;
#Value("${vcap.services.postgres.credentials.username}")
private String postgresUsername;
#Value("${vcap.services.postgres.credentials.password}")
private String postgresPassword;
#Value("${spring.datasource.driver-class-name}")
private String dataSourceDriverClassName;
#Primary
#Bean
public DataSource dataSource() {
org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
dataSource.setDriverClassName(dataSourceDriverClassName);
dataSource.setUrl(postgresUrl);
dataSource.setUsername(postgresUsername);
dataSource.setPassword(postgresPassword);
dataSource.setInitialSize(10);
dataSource.setMaxIdle(5);
dataSource.setMinIdle(5);
dataSource.setMaxActive(25);
return dataSource;
}
// You can add rest of services configuration below e.g. rabbit connection
// factory, redis etc to centralize services configuration for cloud.
// This example did not use profile but that is what you should use to
// separate out cloud vs local configuraion to help run on local etc.
}
You don't need all that configuration just to customize the pool size. You should just need this code as shown in the documentation:
#Bean
public DataSource dataSource() {
PoolConfig poolConfig = new PoolConfig(5, 30, 3000);
DataSourceConfig dbConfig = new DataSourceConfig(poolConfig, null);
return connectionFactory().dataSource(dbConfig);
}

annotation #RibbonClient not work together with RestTemplate

I am trying Ribbon configuration with RestTemplate based on bookmark service example but without luck, here is my code:
#SpringBootApplication
#RestController
#RibbonClient(name = "foo", configuration = SampleRibbonConfiguration.class)
public class BookmarkServiceApplication {
public static void main(String[] args) {
SpringApplication.run(BookmarkServiceApplication.class, args);
}
#Autowired
RestTemplate restTemplate;
#RequestMapping("/hello")
public String hello() {
String greeting = this.restTemplate.getForObject("http://foo/hello", String.class);
return String.format("%s, %s!", greeting);
}
}
with error page as below:
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Tue Mar 22 19:59:33 GMT+08:00 2016
There was an unexpected error (type=Internal Server Error, status=500).
No instances available for foo
but if I remove annotation #RibbonClient, everything will be just ok,
#RibbonClient(name = "foo", configuration = SampleRibbonConfiguration.class)
and here is SampleRibbonConfiguration implementation:
public class SampleRibbonConfiguration {
#Autowired
IClientConfig ribbonClientConfig;
#Bean
public IPing ribbonPing(IClientConfig config) {
return new PingUrl();
}
#Bean
public IRule ribbonRule(IClientConfig config) {
return new AvailabilityFilteringRule();
}
}
Is it because RibbonClient can not work with RestTemplate together?
and another question is that does Ribbon configuration like load balancing rule could be configured via application.yml configuration file?
as from Ribbon wiki, seems we can configure Ribbon parameters like NFLoadBalancerClassName, NFLoadBalancerRuleClassName etc in property file, does Spring Cloud also supports this?
I'm going to assume you're using Eureka for Service Discovery.
Your particular error:
No instances available for foo
can happen for a couple of reasons
1.) All services are down
All of the instances of your foo service could legitimately be DOWN.
Solution: Try visiting your Eureka Dashboard and ensure all the services are actually UP.
If you're running locally, the Eureka Dashboard is at http://localhost:8761/
2.) Waiting for heartbeats
When you very first register a service via Eureka, there's a period of time where the service is UP but not available. From the documentation
A service is not available for discovery by clients until the
instance, the server and the client all have the same metadata in
their local cache (so it could take 3 heartbeats)
Solution: Wait a good 30 seconds after starting your foo service before you try calling it via your client.
In your particular case I'm going to guess #2 is likely what's happening to you. You're probably starting the service and trying to call it immediately from the client.
When it doesn't work, you stop the client, make some changes and restart. By that time though, all of the heartbeats have completed and your service is now available.
For your second question. Look at the "Customizing the Ribbon Client using properties" section in the reference documentation. (link)

scala cloud foundry mongodb : access to mongodb denied

I have installed eclipse, the cloudfoundry plugin, the scala plugin,the vaadin plugin(for web developments) and the mongodb libraries.
I created a class like this :
import vaadin.scala.Application
import vaadin.scala.VerticalLayout
import com.mongodb.casbah.MongoConnection
import com.mongodb.casbah.commons.MongoDBObject
import vaadin.scala.Label
import vaadin.scala.Button
class Launcher extends Application {
val label=new Label
override def main = new VerticalLayout() {
val coll=MongoConnection()("mybd")("somecollection")
val builder=MongoDBObject.newBuilder
builder+="foo1" -> "bar"
var newobj=builder.result()
coll.save(newobj)
val mongoColl=MongoConnection()("mybd")("somecollection")
val withFoo=mongoColl.findOne()
label.value=withFoo
add(label)
//bouton pour faire joli
add(new Button{
caption_=("click me!")
})
}
}
the error (the access to the mongodb database is denied) comes from the parameters, which are the default ones.
do you know how to set up the good parameters in scala or in java?
Looks like you got some help on the vcap-dev mailing list
package com.example.vaadin_1
import vaadin.scala.Application
import org.cloudfoundry.runtime.env.CloudEnvironment
import org.cloudfoundry.runtime.env.MongoServiceInfo
import com.mongodb.casbah.MongoConnection
class Launcher extends Application {
val cloudEnvironment = new CloudEnvironment()
val mongoServices = cloudEnvironment.getServiceInfos(classOf[MongoServiceInfo])
val mongo = mongoServices.get(0)
val mongodb = MongoConnection(mongo.getHost(), mongo.getPort())("abc")
mongodb.authenticate(mongo.getUserName(),mongo.getPassword())
}
I would suggest to do it using Spring Data for MongoDB there is a sample application for Cloudfoundry in particular put together by the Spring guys. With a bit of xml configuration you have ready to inject the mongoTemplate similar to the familiar Spring xxxTemplate paradigm.
when deploying to CloudFoundry, the information relative to connecting to a service (i.e mongo in your case) is made available to the app through environment variable VCAP_SERVICES. It is a json document with one entry per service. You could of course parse it yourself, but you will find the class http://cf-runtime-api.cloudfoundry.com/org/cloudfoundry/runtime/env/CloudEnvironment.html useful. You will need to add the org.cloudfoundry/cloudfoundry-runtime/0.8.1 jar to your project. You can use it without Spring.
Have a look at http://docs.cloudfoundry.com/services.html for explanation of the VCAP_SERVICES underlying var