/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 * * http://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.apache.brooklyn.feed.windows; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.entity.EntityLocal; import org.apache.brooklyn.api.mgmt.ExecutionContext; import org.apache.brooklyn.api.sensor.AttributeSensor; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.ConfigKeys; import org.apache.brooklyn.core.effector.EffectorTasks; import org.apache.brooklyn.core.entity.EntityInternal; import org.apache.brooklyn.core.feed.AbstractFeed; import org.apache.brooklyn.core.feed.PollHandler; import org.apache.brooklyn.core.feed.Poller; import org.apache.brooklyn.core.sensor.Sensors; import org.apache.brooklyn.feed.windows.WindowsPerformanceCounterPollConfig; import org.apache.brooklyn.location.winrm.WinRmMachineLocation; import org.apache.brooklyn.util.core.flags.TypeCoercions; import org.apache.brooklyn.util.core.internal.winrm.WinRmToolResponse; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.time.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.reflect.TypeToken; /** * A sensor feed that retrieves performance counters from a Windows host and posts the values to sensors. * * <p>To use this feed, you must provide the entity, and a collection of mappings between Windows performance counter * names and Brooklyn attribute sensors.</p> * * <p>This feed uses SSH to invoke the windows utility <tt>typeperf</tt> to query for a specific set of performance * counters, by name. The values are extracted from the response, and published to the entity's sensors.</p> * * <p>Example:</p> * * {@code * @Override * protected void connectSensors() { * WindowsPerformanceCounterFeed feed = WindowsPerformanceCounterFeed.builder() * .entity(entity) * .addSensor("\\Processor(_total)\\% Idle Time", CPU_IDLE_TIME) * .addSensor("\\Memory\\Available MBytes", AVAILABLE_MEMORY) * .build(); * } * } * * @since 0.6.0 * @author richardcloudsoft */ public class WindowsPerformanceCounterFeed extends AbstractFeed { private static final Logger log = LoggerFactory.getLogger(WindowsPerformanceCounterFeed.class); // This pattern matches CSV line(s) with the date in the first field, and at least one further field. protected static final Pattern lineWithPerfData = Pattern.compile("^\"[\\d:/\\-. ]+\",\".*\"$", Pattern.MULTILINE); private static final Joiner JOINER_ON_SPACE = Joiner.on(' '); private static final Joiner JOINER_ON_COMMA = Joiner.on(','); private static final int OUTPUT_COLUMN_WIDTH = 100; @SuppressWarnings("serial") public static final ConfigKey<Collection<WindowsPerformanceCounterPollConfig<?>>> POLLS = ConfigKeys.newConfigKey( new TypeToken<Collection<WindowsPerformanceCounterPollConfig<?>>>() {}, "polls"); public static Builder builder() { return new Builder(); } public static class Builder { private EntityLocal entity; private Set<WindowsPerformanceCounterPollConfig<?>> polls = Sets.newLinkedHashSet(); private Duration period = Duration.of(30, TimeUnit.SECONDS); private String uniqueTag; private volatile boolean built; public Builder entity(Entity val) { this.entity = (EntityLocal) checkNotNull(val, "entity"); return this; } public Builder addSensor(WindowsPerformanceCounterPollConfig<?> config) { polls.add(config); return this; } public Builder addSensor(String performanceCounterName, AttributeSensor<?> sensor) { return addSensor(new WindowsPerformanceCounterPollConfig(sensor).performanceCounterName(checkNotNull(performanceCounterName, "performanceCounterName"))); } public Builder addSensors(Map<String, AttributeSensor> sensors) { for (Map.Entry<String, AttributeSensor> entry : sensors.entrySet()) { addSensor(entry.getKey(), entry.getValue()); } return this; } public Builder period(Duration period) { this.period = checkNotNull(period, "period"); return this; } public Builder period(long millis) { return period(millis, TimeUnit.MILLISECONDS); } public Builder period(long val, TimeUnit units) { return period(Duration.of(val, units)); } public Builder uniqueTag(String uniqueTag) { this.uniqueTag = uniqueTag; return this; } public WindowsPerformanceCounterFeed build() { built = true; WindowsPerformanceCounterFeed result = new WindowsPerformanceCounterFeed(this); result.setEntity(checkNotNull(entity, "entity")); result.start(); return result; } @Override protected void finalize() { if (!built) log.warn("WindowsPerformanceCounterFeed.Builder created, but build() never called"); } } /** * For rebind; do not call directly; use builder */ public WindowsPerformanceCounterFeed() { } protected WindowsPerformanceCounterFeed(Builder builder) { List<WindowsPerformanceCounterPollConfig<?>> polls = Lists.newArrayList(); for (WindowsPerformanceCounterPollConfig<?> config : builder.polls) { if (!config.isEnabled()) continue; @SuppressWarnings({ "unchecked", "rawtypes" }) WindowsPerformanceCounterPollConfig<?> configCopy = new WindowsPerformanceCounterPollConfig(config); if (configCopy.getPeriod() < 0) configCopy.period(builder.period); polls.add(configCopy); } config().set(POLLS, polls); initUniqueTag(builder.uniqueTag, polls); } @Override protected void preStart() { Collection<WindowsPerformanceCounterPollConfig<?>> polls = getConfig(POLLS); long minPeriod = Integer.MAX_VALUE; List<String> performanceCounterNames = Lists.newArrayList(); for (WindowsPerformanceCounterPollConfig<?> config : polls) { minPeriod = Math.min(minPeriod, config.getPeriod()); performanceCounterNames.add(config.getPerformanceCounterName()); } Iterable<String> allParams = ImmutableList.<String>builder() .add("(Get-Counter") .add("-Counter") .add(JOINER_ON_COMMA.join(Iterables.transform(performanceCounterNames, QuoteStringFunction.INSTANCE))) .add("-SampleInterval") .add("2") // TODO: extract SampleInterval as a config key .add(").CounterSamples") .add("|") .add("Format-Table") .add(String.format("@{Expression={$_.Path};width=%d},@{Expression={$_.CookedValue};width=%<d}", OUTPUT_COLUMN_WIDTH)) .add("-HideTableHeaders") .add("|") .add("Out-String") .add("-Width") .add(String.valueOf(OUTPUT_COLUMN_WIDTH * 2)) .build(); String command = JOINER_ON_SPACE.join(allParams); log.debug("Windows performance counter poll command for {} will be: {}", entity, command); GetPerformanceCountersJob<WinRmToolResponse> job = new GetPerformanceCountersJob(getEntity(), command); getPoller().scheduleAtFixedRate( new CallInEntityExecutionContext(entity, job), new SendPerfCountersToSensors(getEntity(), polls), minPeriod); } private static class GetPerformanceCountersJob<T> implements Callable<T> { private final Entity entity; private final String command; GetPerformanceCountersJob(Entity entity, String command) { this.entity = entity; this.command = command; } @Override @SuppressWarnings("unchecked") public T call() throws Exception { WinRmMachineLocation machine = EffectorTasks.getMachine(entity, WinRmMachineLocation.class); WinRmToolResponse response = machine.executePsScript(command); return (T)response; } } @SuppressWarnings("unchecked") protected Poller<WinRmToolResponse> getPoller() { return (Poller<WinRmToolResponse>) super.getPoller(); } /** * A {@link java.util.concurrent.Callable} that wraps another {@link java.util.concurrent.Callable}, where the * inner {@link java.util.concurrent.Callable} is executed in the context of a * specific entity. * * @param <T> The type of the {@link java.util.concurrent.Callable}. */ private static class CallInEntityExecutionContext<T> implements Callable<T> { private final Callable<T> job; private EntityLocal entity; private CallInEntityExecutionContext(EntityLocal entity, Callable<T> job) { this.job = job; this.entity = entity; } @Override public T call() throws Exception { ExecutionContext executionContext = ((EntityInternal) entity).getManagementSupport().getExecutionContext(); return executionContext.submit(Maps.newHashMap(), job).get(); } } @VisibleForTesting static class SendPerfCountersToSensors implements PollHandler<WinRmToolResponse> { private final EntityLocal entity; private final List<WindowsPerformanceCounterPollConfig<?>> polls; private final Set<AttributeSensor<?>> failedAttributes = Sets.newLinkedHashSet(); private static final Pattern MACHINE_NAME_LOOKBACK_PATTERN = Pattern.compile(String.format("(?<=\\\\\\\\.{0,%d})\\\\.*", OUTPUT_COLUMN_WIDTH)); public SendPerfCountersToSensors(EntityLocal entity, Collection<WindowsPerformanceCounterPollConfig<?>> polls) { this.entity = entity; this.polls = ImmutableList.copyOf(polls); } @Override public boolean checkSuccess(WinRmToolResponse val) { // TODO not just using statusCode; also looking at absence of stderr. // Status code is (empirically) unreliable: it returns 0 sometimes even when failed // (but never returns non-zero on success). if (val.getStatusCode() != 0) return false; String stderr = val.getStdErr(); if (stderr == null || stderr.length() != 0) return false; String out = val.getStdOut(); if (out == null || out.length() == 0) return false; return true; } @Override public void onSuccess(WinRmToolResponse val) { for (String pollResponse : val.getStdOut().split("\r\n")) { if (Strings.isNullOrEmpty(pollResponse)) { continue; } String path = pollResponse.substring(0, OUTPUT_COLUMN_WIDTH - 1); // The performance counter output prepends the sensor name with "\\<machinename>" so we need to remove it Matcher machineNameLookbackMatcher = MACHINE_NAME_LOOKBACK_PATTERN.matcher(path); if (!machineNameLookbackMatcher.find()) { continue; } String name = machineNameLookbackMatcher.group(0).trim(); String rawValue = pollResponse.substring(OUTPUT_COLUMN_WIDTH).replaceAll("^\\s+", ""); WindowsPerformanceCounterPollConfig<?> config = getPollConfig(name); Class<?> clazz = config.getSensor().getType(); AttributeSensor<Object> attribute = (AttributeSensor<Object>) Sensors.newSensor(clazz, config.getSensor().getName(), config.getDescription()); try { Object value = TypeCoercions.coerce(rawValue, TypeToken.of(clazz)); entity.sensors().set(attribute, value); } catch (Exception e) { Exceptions.propagateIfFatal(e); if (failedAttributes.add(attribute)) { log.warn("Failed to coerce value '{}' to {} for {} -> {}", new Object[] {rawValue, clazz, entity, attribute}); } else { if (log.isTraceEnabled()) log.trace("Failed (repeatedly) to coerce value '{}' to {} for {} -> {}", new Object[] {rawValue, clazz, entity, attribute}); } } } } @Override public void onFailure(WinRmToolResponse val) { log.error("Windows Performance Counter query did not respond as expected. exitcode={} stdout={} stderr={}", new Object[]{val.getStatusCode(), val.getStdOut(), val.getStdErr()}); for (WindowsPerformanceCounterPollConfig<?> config : polls) { Class<?> clazz = config.getSensor().getType(); AttributeSensor<?> attribute = Sensors.newSensor(clazz, config.getSensor().getName(), config.getDescription()); entity.sensors().set(attribute, null); } } @Override public void onException(Exception exception) { log.error("Detected exception while retrieving Windows Performance Counters from entity " + entity.getDisplayName(), exception); for (WindowsPerformanceCounterPollConfig<?> config : polls) { entity.sensors().set(Sensors.newSensor(config.getSensor().getClass(), config.getPerformanceCounterName(), config.getDescription()), null); } } @Override public String getDescription() { return "" + polls; } @Override public String toString() { return super.toString()+"["+getDescription()+"]"; } private WindowsPerformanceCounterPollConfig<?> getPollConfig(String sensorName) { for (WindowsPerformanceCounterPollConfig<?> poll : polls) { if (poll.getPerformanceCounterName().equalsIgnoreCase(sensorName)) { return poll; } } throw new IllegalStateException(String.format("%s not found in configured polls: %s", sensorName, polls)); } } static class PerfCounterValueIterator implements Iterator<String> { // This pattern matches the contents of the first field, and optionally matches the rest of the line as // further fields. Feed the second match back into the pattern again to get the next field, and repeat until // all fields are discovered. protected static final Pattern splitPerfData = Pattern.compile("^\"([^\\\"]*)\"((,\"[^\\\"]*\")*)$"); private Matcher matcher; public PerfCounterValueIterator(String input) { matcher = splitPerfData.matcher(input); // Throw away the first element (the timestamp) (and also confirm that we have a pattern match) checkArgument(hasNext(), "input "+input+" does not match expected pattern "+splitPerfData.pattern()); next(); } @Override public boolean hasNext() { return matcher != null && matcher.find(); } @Override public String next() { String next = matcher.group(1); String remainder = matcher.group(2); if (!Strings.isNullOrEmpty(remainder)) { assert remainder.startsWith(","); remainder = remainder.substring(1); matcher = splitPerfData.matcher(remainder); } else { matcher = null; } return next; } @Override public void remove() { throw new UnsupportedOperationException(); } } private static enum QuoteStringFunction implements Function<String, String> { INSTANCE; @Nullable @Override public String apply(@Nullable String input) { return input != null ? "\"" + input + "\"" : null; } } }