001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.camel.management.mbean; 018 019import java.io.ByteArrayInputStream; 020import java.io.InputStream; 021import java.util.ArrayList; 022import java.util.Collections; 023import java.util.Comparator; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028import java.util.concurrent.TimeUnit; 029import java.util.concurrent.atomic.AtomicBoolean; 030import javax.management.AttributeValueExp; 031import javax.management.MBeanServer; 032import javax.management.ObjectName; 033import javax.management.Query; 034import javax.management.QueryExp; 035import javax.management.StringValueExp; 036import javax.management.openmbean.CompositeData; 037import javax.management.openmbean.CompositeDataSupport; 038import javax.management.openmbean.CompositeType; 039import javax.management.openmbean.TabularData; 040import javax.management.openmbean.TabularDataSupport; 041 042import org.w3c.dom.Document; 043 044import org.apache.camel.CamelContext; 045import org.apache.camel.ManagementStatisticsLevel; 046import org.apache.camel.Route; 047import org.apache.camel.ServiceStatus; 048import org.apache.camel.TimerListener; 049import org.apache.camel.api.management.ManagedResource; 050import org.apache.camel.api.management.mbean.CamelOpenMBeanTypes; 051import org.apache.camel.api.management.mbean.ManagedProcessorMBean; 052import org.apache.camel.api.management.mbean.ManagedRouteMBean; 053import org.apache.camel.model.ModelCamelContext; 054import org.apache.camel.model.ModelHelper; 055import org.apache.camel.model.RouteDefinition; 056import org.apache.camel.spi.InflightRepository; 057import org.apache.camel.spi.ManagementStrategy; 058import org.apache.camel.spi.RouteError; 059import org.apache.camel.spi.RoutePolicy; 060import org.apache.camel.util.ObjectHelper; 061import org.apache.camel.util.XmlLineNumberParser; 062import org.slf4j.Logger; 063import org.slf4j.LoggerFactory; 064 065@ManagedResource(description = "Managed Route") 066public class ManagedRoute extends ManagedPerformanceCounter implements TimerListener, ManagedRouteMBean { 067 068 public static final String VALUE_UNKNOWN = "Unknown"; 069 070 private static final Logger LOG = LoggerFactory.getLogger(ManagedRoute.class); 071 072 protected final Route route; 073 protected final String description; 074 protected final ModelCamelContext context; 075 private final LoadTriplet load = new LoadTriplet(); 076 private final String jmxDomain; 077 078 public ManagedRoute(ModelCamelContext context, Route route) { 079 this.route = route; 080 this.context = context; 081 this.description = route.getDescription(); 082 this.jmxDomain = context.getManagementStrategy().getManagementAgent().getMBeanObjectDomainName(); 083 } 084 085 @Override 086 public void init(ManagementStrategy strategy) { 087 super.init(strategy); 088 boolean enabled = context.getManagementStrategy().getManagementAgent().getStatisticsLevel() != ManagementStatisticsLevel.Off; 089 setStatisticsEnabled(enabled); 090 } 091 092 public Route getRoute() { 093 return route; 094 } 095 096 public CamelContext getContext() { 097 return context; 098 } 099 100 public String getRouteId() { 101 String id = route.getId(); 102 if (id == null) { 103 id = VALUE_UNKNOWN; 104 } 105 return id; 106 } 107 108 public String getRouteGroup() { 109 return route.getGroup(); 110 } 111 112 @Override 113 public TabularData getRouteProperties() { 114 try { 115 final Map<String, Object> properties = route.getProperties(); 116 final TabularData answer = new TabularDataSupport(CamelOpenMBeanTypes.camelRoutePropertiesTabularType()); 117 final CompositeType ct = CamelOpenMBeanTypes.camelRoutePropertiesCompositeType(); 118 119 // gather route properties 120 for (Map.Entry<String, Object> entry : properties.entrySet()) { 121 final String key = entry.getKey(); 122 final String val = context.getTypeConverter().convertTo(String.class, entry.getValue()); 123 124 CompositeData data = new CompositeDataSupport( 125 ct, 126 new String[]{"key", "value"}, 127 new Object[]{key, val} 128 ); 129 130 answer.put(data); 131 } 132 return answer; 133 } catch (Exception e) { 134 throw ObjectHelper.wrapRuntimeCamelException(e); 135 } 136 } 137 138 public String getDescription() { 139 return description; 140 } 141 142 @Override 143 public String getEndpointUri() { 144 if (route.getEndpoint() != null) { 145 return route.getEndpoint().getEndpointUri(); 146 } 147 return VALUE_UNKNOWN; 148 } 149 150 public String getState() { 151 // must use String type to be sure remote JMX can read the attribute without requiring Camel classes. 152 ServiceStatus status = context.getRouteStatus(route.getId()); 153 // if no status exists then its stopped 154 if (status == null) { 155 status = ServiceStatus.Stopped; 156 } 157 return status.name(); 158 } 159 160 public String getUptime() { 161 return route.getUptime(); 162 } 163 164 public long getUptimeMillis() { 165 return route.getUptimeMillis(); 166 } 167 168 public Integer getInflightExchanges() { 169 return (int) super.getExchangesInflight(); 170 } 171 172 public String getCamelId() { 173 return context.getName(); 174 } 175 176 public String getCamelManagementName() { 177 return context.getManagementName(); 178 } 179 180 public Boolean getTracing() { 181 return route.getRouteContext().isTracing(); 182 } 183 184 public void setTracing(Boolean tracing) { 185 route.getRouteContext().setTracing(tracing); 186 } 187 188 public Boolean getMessageHistory() { 189 return route.getRouteContext().isMessageHistory(); 190 } 191 192 public Boolean getLogMask() { 193 return route.getRouteContext().isLogMask(); 194 } 195 196 public String getRoutePolicyList() { 197 List<RoutePolicy> policyList = route.getRouteContext().getRoutePolicyList(); 198 199 if (policyList == null || policyList.isEmpty()) { 200 // return an empty string to have it displayed nicely in JMX consoles 201 return ""; 202 } 203 204 StringBuilder sb = new StringBuilder(); 205 for (int i = 0; i < policyList.size(); i++) { 206 RoutePolicy policy = policyList.get(i); 207 sb.append(policy.getClass().getSimpleName()); 208 sb.append("(").append(ObjectHelper.getIdentityHashCode(policy)).append(")"); 209 if (i < policyList.size() - 1) { 210 sb.append(", "); 211 } 212 } 213 return sb.toString(); 214 } 215 216 public String getLoad01() { 217 double load1 = load.getLoad1(); 218 if (Double.isNaN(load1)) { 219 // empty string if load statistics is disabled 220 return ""; 221 } else { 222 return String.format("%.2f", load1); 223 } 224 } 225 226 public String getLoad05() { 227 double load5 = load.getLoad5(); 228 if (Double.isNaN(load5)) { 229 // empty string if load statistics is disabled 230 return ""; 231 } else { 232 return String.format("%.2f", load5); 233 } 234 } 235 236 public String getLoad15() { 237 double load15 = load.getLoad15(); 238 if (Double.isNaN(load15)) { 239 // empty string if load statistics is disabled 240 return ""; 241 } else { 242 return String.format("%.2f", load15); 243 } 244 } 245 246 @Override 247 public void onTimer() { 248 load.update(getInflightExchanges()); 249 } 250 251 public void start() throws Exception { 252 if (!context.getStatus().isStarted()) { 253 throw new IllegalArgumentException("CamelContext is not started"); 254 } 255 context.getRouteController().startRoute(getRouteId()); 256 } 257 258 public void stop() throws Exception { 259 if (!context.getStatus().isStarted()) { 260 throw new IllegalArgumentException("CamelContext is not started"); 261 } 262 context.getRouteController().stopRoute(getRouteId()); 263 } 264 265 public void stop(long timeout) throws Exception { 266 if (!context.getStatus().isStarted()) { 267 throw new IllegalArgumentException("CamelContext is not started"); 268 } 269 context.getRouteController().stopRoute(getRouteId(), timeout, TimeUnit.SECONDS); 270 } 271 272 public boolean stop(Long timeout, Boolean abortAfterTimeout) throws Exception { 273 if (!context.getStatus().isStarted()) { 274 throw new IllegalArgumentException("CamelContext is not started"); 275 } 276 return context.getRouteController().stopRoute(getRouteId(), timeout, TimeUnit.SECONDS, abortAfterTimeout); 277 } 278 279 public void shutdown() throws Exception { 280 if (!context.getStatus().isStarted()) { 281 throw new IllegalArgumentException("CamelContext is not started"); 282 } 283 String routeId = getRouteId(); 284 context.stopRoute(routeId); 285 context.removeRoute(routeId); 286 } 287 288 public void shutdown(long timeout) throws Exception { 289 if (!context.getStatus().isStarted()) { 290 throw new IllegalArgumentException("CamelContext is not started"); 291 } 292 String routeId = getRouteId(); 293 context.stopRoute(routeId, timeout, TimeUnit.SECONDS); 294 context.removeRoute(routeId); 295 } 296 297 public boolean remove() throws Exception { 298 if (!context.getStatus().isStarted()) { 299 throw new IllegalArgumentException("CamelContext is not started"); 300 } 301 return context.removeRoute(getRouteId()); 302 } 303 304 @Override 305 public void restart() throws Exception { 306 restart(1); 307 } 308 309 @Override 310 public void restart(long delay) throws Exception { 311 stop(); 312 if (delay > 0) { 313 try { 314 LOG.debug("Sleeping {} seconds before starting route: {}", delay, getRouteId()); 315 Thread.sleep(delay * 1000); 316 } catch (InterruptedException e) { 317 // ignore 318 } 319 } 320 start(); 321 } 322 323 public String dumpRouteAsXml() throws Exception { 324 return dumpRouteAsXml(false); 325 } 326 327 @Override 328 public String dumpRouteAsXml(boolean resolvePlaceholders) throws Exception { 329 String id = route.getId(); 330 RouteDefinition def = context.getRouteDefinition(id); 331 if (def != null) { 332 String xml = ModelHelper.dumpModelAsXml(context, def); 333 334 // if resolving placeholders we parse the xml, and resolve the property placeholders during parsing 335 if (resolvePlaceholders) { 336 final AtomicBoolean changed = new AtomicBoolean(); 337 InputStream is = new ByteArrayInputStream(xml.getBytes("UTF-8")); 338 Document dom = XmlLineNumberParser.parseXml(is, new XmlLineNumberParser.XmlTextTransformer() { 339 @Override 340 public String transform(String text) { 341 try { 342 String after = getContext().resolvePropertyPlaceholders(text); 343 if (!changed.get()) { 344 changed.set(!text.equals(after)); 345 } 346 return after; 347 } catch (Exception e) { 348 // ignore 349 return text; 350 } 351 } 352 }); 353 // okay there were some property placeholder replaced so re-create the model 354 if (changed.get()) { 355 xml = context.getTypeConverter().mandatoryConvertTo(String.class, dom); 356 RouteDefinition copy = ModelHelper.createModelFromXml(context, xml, RouteDefinition.class); 357 xml = ModelHelper.dumpModelAsXml(context, copy); 358 } 359 } 360 return xml; 361 } 362 return null; 363 } 364 365 public void updateRouteFromXml(String xml) throws Exception { 366 // convert to model from xml 367 RouteDefinition def = ModelHelper.createModelFromXml(context, xml, RouteDefinition.class); 368 if (def == null) { 369 return; 370 } 371 372 // if the xml does not contain the route-id then we fix this by adding the actual route id 373 // this may be needed if the route-id was auto-generated, as the intend is to update this route 374 // and not add a new route, adding a new route, use the MBean operation on ManagedCamelContext instead. 375 if (ObjectHelper.isEmpty(def.getId())) { 376 def.setId(getRouteId()); 377 } else if (!def.getId().equals(getRouteId())) { 378 throw new IllegalArgumentException("Cannot update route from XML as routeIds does not match. routeId: " 379 + getRouteId() + ", routeId from XML: " + def.getId()); 380 } 381 382 LOG.debug("Updating route: {} from xml: {}", def.getId(), xml); 383 384 try { 385 // add will remove existing route first 386 context.addRouteDefinition(def); 387 } catch (Exception e) { 388 // log the error as warn as the management api may be invoked remotely over JMX which does not propagate such exception 389 String msg = "Error updating route: " + def.getId() + " from xml: " + xml + " due: " + e.getMessage(); 390 LOG.warn(msg, e); 391 throw e; 392 } 393 } 394 395 public String dumpRouteStatsAsXml(boolean fullStats, boolean includeProcessors) throws Exception { 396 // in this logic we need to calculate the accumulated processing time for the processor in the route 397 // and hence why the logic is a bit more complicated to do this, as we need to calculate that from 398 // the bottom -> top of the route but this information is valuable for profiling routes 399 StringBuilder sb = new StringBuilder(); 400 401 // need to calculate this value first, as we need that value for the route stat 402 Long processorAccumulatedTime = 0L; 403 404 // gather all the processors for this route, which requires JMX 405 if (includeProcessors) { 406 sb.append(" <processorStats>\n"); 407 MBeanServer server = getContext().getManagementStrategy().getManagementAgent().getMBeanServer(); 408 if (server != null) { 409 // get all the processor mbeans and sort them accordingly to their index 410 String prefix = getContext().getManagementStrategy().getManagementAgent().getIncludeHostName() ? "*/" : ""; 411 ObjectName query = ObjectName.getInstance(jmxDomain + ":context=" + prefix + getContext().getManagementName() + ",type=processors,*"); 412 Set<ObjectName> names = server.queryNames(query, null); 413 List<ManagedProcessorMBean> mps = new ArrayList<>(); 414 for (ObjectName on : names) { 415 ManagedProcessorMBean processor = context.getManagementStrategy().getManagementAgent().newProxyClient(on, ManagedProcessorMBean.class); 416 417 // the processor must belong to this route 418 if (getRouteId().equals(processor.getRouteId())) { 419 mps.add(processor); 420 } 421 } 422 mps.sort(new OrderProcessorMBeans()); 423 424 // walk the processors in reverse order, and calculate the accumulated total time 425 Map<String, Long> accumulatedTimes = new HashMap<>(); 426 Collections.reverse(mps); 427 for (ManagedProcessorMBean processor : mps) { 428 processorAccumulatedTime += processor.getTotalProcessingTime(); 429 accumulatedTimes.put(processor.getProcessorId(), processorAccumulatedTime); 430 } 431 // and reverse back again 432 Collections.reverse(mps); 433 434 // and now add the sorted list of processors to the xml output 435 for (ManagedProcessorMBean processor : mps) { 436 sb.append(" <processorStat").append(String.format(" id=\"%s\" index=\"%s\" state=\"%s\"", processor.getProcessorId(), processor.getIndex(), processor.getState())); 437 // do we have an accumulated time then append that 438 Long accTime = accumulatedTimes.get(processor.getProcessorId()); 439 if (accTime != null) { 440 sb.append(" accumulatedProcessingTime=\"").append(accTime).append("\""); 441 } 442 // use substring as we only want the attributes 443 sb.append(" ").append(processor.dumpStatsAsXml(fullStats).substring(7)).append("\n"); 444 } 445 } 446 sb.append(" </processorStats>\n"); 447 } 448 449 // route self time is route total - processor accumulated total) 450 long routeSelfTime = getTotalProcessingTime() - processorAccumulatedTime; 451 if (routeSelfTime < 0) { 452 // ensure we don't calculate that as negative 453 routeSelfTime = 0; 454 } 455 456 StringBuilder answer = new StringBuilder(); 457 answer.append("<routeStat").append(String.format(" id=\"%s\"", route.getId())).append(String.format(" state=\"%s\"", getState())); 458 // use substring as we only want the attributes 459 String stat = dumpStatsAsXml(fullStats); 460 answer.append(" exchangesInflight=\"").append(getInflightExchanges()).append("\""); 461 answer.append(" selfProcessingTime=\"").append(routeSelfTime).append("\""); 462 InflightRepository.InflightExchange oldest = getOldestInflightEntry(); 463 if (oldest == null) { 464 answer.append(" oldestInflightExchangeId=\"\""); 465 answer.append(" oldestInflightDuration=\"\""); 466 } else { 467 answer.append(" oldestInflightExchangeId=\"").append(oldest.getExchange().getExchangeId()).append("\""); 468 answer.append(" oldestInflightDuration=\"").append(oldest.getDuration()).append("\""); 469 } 470 answer.append(" ").append(stat.substring(7, stat.length() - 2)).append(">\n"); 471 472 if (includeProcessors) { 473 answer.append(sb); 474 } 475 476 answer.append("</routeStat>"); 477 return answer.toString(); 478 } 479 480 public void reset(boolean includeProcessors) throws Exception { 481 reset(); 482 483 // and now reset all processors for this route 484 if (includeProcessors) { 485 MBeanServer server = getContext().getManagementStrategy().getManagementAgent().getMBeanServer(); 486 if (server != null) { 487 // get all the processor mbeans and sort them accordingly to their index 488 String prefix = getContext().getManagementStrategy().getManagementAgent().getIncludeHostName() ? "*/" : ""; 489 ObjectName query = ObjectName.getInstance(jmxDomain + ":context=" + prefix + getContext().getManagementName() + ",type=processors,*"); 490 QueryExp queryExp = Query.match(new AttributeValueExp("RouteId"), new StringValueExp(getRouteId())); 491 Set<ObjectName> names = server.queryNames(query, queryExp); 492 for (ObjectName name : names) { 493 server.invoke(name, "reset", null, null); 494 } 495 } 496 } 497 } 498 499 public String createRouteStaticEndpointJson() { 500 return getContext().createRouteStaticEndpointJson(getRouteId()); 501 } 502 503 @Override 504 public String createRouteStaticEndpointJson(boolean includeDynamic) { 505 return getContext().createRouteStaticEndpointJson(getRouteId(), includeDynamic); 506 } 507 508 @Override 509 public boolean equals(Object o) { 510 return this == o || (o != null && getClass() == o.getClass() && route.equals(((ManagedRoute) o).route)); 511 } 512 513 @Override 514 public int hashCode() { 515 return route.hashCode(); 516 } 517 518 private InflightRepository.InflightExchange getOldestInflightEntry() { 519 return getContext().getInflightRepository().oldest(getRouteId()); 520 } 521 522 public Long getOldestInflightDuration() { 523 InflightRepository.InflightExchange oldest = getOldestInflightEntry(); 524 if (oldest == null) { 525 return null; 526 } else { 527 return oldest.getDuration(); 528 } 529 } 530 531 public String getOldestInflightExchangeId() { 532 InflightRepository.InflightExchange oldest = getOldestInflightEntry(); 533 if (oldest == null) { 534 return null; 535 } else { 536 return oldest.getExchange().getExchangeId(); 537 } 538 } 539 540 @Override 541 public Boolean getHasRouteController() { 542 return route.getRouteContext().getRouteController() != null; 543 } 544 545 @Override 546 public RouteError getLastError() { 547 return route.getRouteContext().getLastError(); 548 } 549 550 /** 551 * Used for sorting the processor mbeans accordingly to their index. 552 */ 553 private static final class OrderProcessorMBeans implements Comparator<ManagedProcessorMBean> { 554 555 @Override 556 public int compare(ManagedProcessorMBean o1, ManagedProcessorMBean o2) { 557 return o1.getIndex().compareTo(o2.getIndex()); 558 } 559 } 560}