-
Notifications
You must be signed in to change notification settings - Fork 2
feat: wire EMF file output into NucleusEmitter publish flow #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,19 +9,23 @@ | |
| import com.aws.greengrass.config.Topics; | ||
| import com.aws.greengrass.dependency.ImplementsService; | ||
| import com.aws.greengrass.dependency.State; | ||
| import com.aws.greengrass.deployment.DeviceConfiguration; | ||
| import com.aws.greengrass.lifecyclemanager.PluginService; | ||
| import com.aws.greengrass.telemetry.impl.Metric; | ||
| import com.aws.greengrass.telemetry.nucleus.emitter.emf.EmfFileWriter; | ||
| import com.aws.greengrass.telemetry.nucleus.emitter.metrics.KernelMetricsEmitter; | ||
| import com.aws.greengrass.telemetry.nucleus.emitter.metrics.SystemMetricsEmitter; | ||
| import com.aws.greengrass.telemetry.nucleus.emitter.publisher.MqttPublisher; | ||
| import com.aws.greengrass.telemetry.nucleus.emitter.publisher.PubSubPublisher; | ||
| import com.aws.greengrass.util.Coerce; | ||
| import com.aws.greengrass.util.SerializerFactory; | ||
| import com.aws.greengrass.util.Utils; | ||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import lombok.AccessLevel; | ||
| import lombok.Getter; | ||
|
|
||
| import java.nio.file.Paths; | ||
| import java.util.Collection; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
|
|
@@ -64,6 +68,8 @@ public class NucleusEmitter extends PluginService { | |
| //Metric publishers | ||
| private final PubSubPublisher pubSubPublisher; | ||
| private final MqttPublisher mqttPublisher; | ||
| // volatile ensures visibility across config subscriber and publish threads | ||
| private volatile EmfFileWriter emfFileWriter; | ||
|
|
||
| private final ChildChanged subscribeToConfigChanges = (what, topic) -> | ||
| handleConfiguration(this.config.lookupTopics(CONFIGURATION_CONFIG_KEY)); | ||
|
|
@@ -108,9 +114,14 @@ private void handleConfiguration(Topics configurationTopics) { | |
| .equals(newConfiguration.getExcludeMounts()); | ||
| boolean excludeInterfacesChanged = !configuration.getExcludeInterfaces() | ||
| .equals(newConfiguration.getExcludeInterfaces()); | ||
| boolean outputModeChanged = !configuration.getOutputMode() | ||
| .equals(newConfiguration.getOutputMode()); | ||
| boolean outputDirectoryChanged = !configuration.getOutputDirectory() | ||
| .equals(newConfiguration.getOutputDirectory()); | ||
|
|
||
| if (!pubSubPublishChanged && !mqttTopicChanged && !telemetryPublishIntervalMsChanged | ||
| && !metricsLevelChanged && !excludeMountsChanged && !excludeInterfacesChanged) { | ||
| && !metricsLevelChanged && !excludeMountsChanged && !excludeInterfacesChanged | ||
| && !outputModeChanged && !outputDirectoryChanged) { | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -138,6 +149,14 @@ private void handleConfiguration(Topics configurationTopics) { | |
| newConfiguration.getExcludeMounts(), | ||
| newConfiguration.getExcludeInterfaces()); | ||
| } | ||
| if (outputModeChanged || outputDirectoryChanged) { | ||
| if (newConfiguration.isEmfEnabled()) { | ||
| this.emfFileWriter = new EmfFileWriter(getThingName(), | ||
| Paths.get(newConfiguration.getOutputDirectory())); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recommendation generated by Amazon CodeGuru Reviewer. Leave feedback on this recommendation by replying to the comment or by reacting to the comment using emoji. Path traversal vulnerability detected. User-controlled input in file paths |
||
| } else { | ||
| this.emfFileWriter = null; // NOPMD NullAssignment - disables EMF output | ||
| } | ||
| } | ||
| scheduleTelemetryPublish(); | ||
| } | ||
|
|
||
|
|
@@ -151,16 +170,36 @@ public void startup() { | |
| scheduleTelemetryPublish(); | ||
| } | ||
|
|
||
| private void publishTelemetry(boolean pubSubPublish, String pubSubTopic, boolean mqttPublish, String mqttTopic) { | ||
| String jsonString = retrieveMetricsJson(jsonMapper); | ||
| if (pubSubPublish) { | ||
| this.pubSubPublisher.publishMessage(jsonString, pubSubTopic); | ||
| private void publishTelemetry(boolean pubSubPublish, String pubSubTopic, | ||
| boolean mqttPublish, String mqttTopic) { | ||
| List<Metric> metrics = collectMetrics(); | ||
| if (pubSubPublish || mqttPublish) { | ||
| String jsonString = null; // NOPMD - set in try, checked before use | ||
| try { | ||
| jsonString = jsonMapper.writeValueAsString(metrics); | ||
| } catch (JsonProcessingException e) { | ||
| logger.error(JSON_PARSE_ERROR_LOG, e); | ||
| } | ||
| if (pubSubPublish && jsonString != null) { | ||
| this.pubSubPublisher.publishMessage(jsonString, pubSubTopic); | ||
| } | ||
| if (mqttPublish && jsonString != null) { | ||
| this.mqttPublisher.publishMessage(jsonString, mqttTopic); | ||
| } | ||
| } | ||
| if (mqttPublish) { | ||
| this.mqttPublisher.publishMessage(jsonString, mqttTopic); | ||
| EmfFileWriter localEmf = this.emfFileWriter; | ||
| if (localEmf != null) { | ||
| localEmf.write(metrics); | ||
| } | ||
| } | ||
|
|
||
| private List<Metric> collectMetrics() { | ||
| SystemMetricsEmitter localSme = this.sme; | ||
| return Stream.of(localSme.getMetrics(), kme.getMetrics()) | ||
| .flatMap(Collection::stream) | ||
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
| private void scheduleTelemetryPublish() { | ||
| final NucleusEmitterConfiguration configuration = currentConfiguration.get(); | ||
| final boolean newPubPublish = configuration.isPubsubPublish(); | ||
|
|
@@ -192,13 +231,9 @@ private void scheduleTelemetryPublish() { | |
| } | ||
|
|
||
| protected String retrieveMetricsJson(ObjectMapper jsonMapper) { | ||
|
|
||
| String jsonString = null; | ||
| try { | ||
| SystemMetricsEmitter localSme = this.sme; | ||
| List<Metric> metrics = Stream.of(localSme.getMetrics(), kme.getMetrics()) | ||
| .flatMap(Collection::stream) | ||
| .collect(Collectors.toList()); | ||
| List<Metric> metrics = collectMetrics(); | ||
| jsonString = jsonMapper.writeValueAsString(metrics); | ||
| } catch (JsonProcessingException e) { | ||
| logger.error(JSON_PARSE_ERROR_LOG, e); | ||
|
|
@@ -209,6 +244,13 @@ protected String retrieveMetricsJson(ObjectMapper jsonMapper) { | |
| @Override | ||
| public void shutdown() { | ||
| cancelJob(telemetryPublishFuture, telemetryPublishInProgressLock, true); | ||
| this.emfFileWriter = null; // NOPMD NullAssignment - release on shutdown | ||
| } | ||
|
|
||
| private String getThingName() { | ||
| return Coerce.toString( | ||
| this.context.get(DeviceConfiguration.class) | ||
| .getThingName()); | ||
| } | ||
|
|
||
| private void cancelJob(ScheduledFuture<?> future, Object lock, boolean immediately) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package com.aws.greengrass.telemetry.nucleus.emitter.emf; | ||
|
|
||
| import com.aws.greengrass.logging.api.Logger; | ||
| import com.aws.greengrass.logging.impl.LogManager; | ||
| import com.aws.greengrass.telemetry.impl.Metric; | ||
| import com.aws.greengrass.telemetry.models.TelemetryUnit; | ||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||
| import software.amazon.cloudwatchlogs.emf.model.DimensionSet; | ||
| import software.amazon.cloudwatchlogs.emf.model.MetricsContext; | ||
| import software.amazon.cloudwatchlogs.emf.model.Unit; | ||
|
|
||
| import java.nio.file.Path; | ||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * Serializes metrics in EMF (Embedded Metric Format) and writes them via a | ||
| * raw Greengrass logger (no envelope). The GG Log Manager handles file rotation | ||
| * and upload to CloudWatch Logs, where EMF is auto-parsed into CloudWatch Metrics. | ||
| */ | ||
| public class EmfFileWriter { | ||
|
|
||
| private static final Logger logger = LogManager.getLogger(EmfFileWriter.class); | ||
|
|
||
| private static final Map<TelemetryUnit, Unit> UNIT_MAP; | ||
|
|
||
| static { | ||
| Map<TelemetryUnit, Unit> m = new LinkedHashMap<>(); | ||
| m.put(TelemetryUnit.Percent, Unit.PERCENT); | ||
| m.put(TelemetryUnit.Bytes, Unit.BYTES); | ||
| m.put(TelemetryUnit.Megabytes, Unit.MEGABYTES); | ||
| m.put(TelemetryUnit.Count, Unit.COUNT); | ||
| UNIT_MAP = Collections.unmodifiableMap(m); | ||
| } | ||
|
|
||
| private final String thingName; | ||
| private final Logger emfLogger; | ||
|
|
||
| /** | ||
| * Creates an EmfFileWriter. | ||
| * | ||
| * @param thingName thing name for dimensions | ||
| * @param outputDirectory directory for EMF output files | ||
| */ | ||
| public EmfFileWriter(String thingName, Path outputDirectory) { | ||
| this.thingName = thingName; | ||
| this.emfLogger = LogManager.getRawLogger("emf-metrics", outputDirectory); | ||
| } | ||
|
|
||
| /** | ||
| * Serializes metrics to EMF JSON and writes them via the raw logger. | ||
| * | ||
| * @param metrics list of metrics to write | ||
| */ | ||
| public void write(List<Metric> metrics) { | ||
| if (metrics == null || metrics.isEmpty()) { | ||
| return; | ||
| } | ||
| Map<String, List<Metric>> byNamespace = groupByNamespace(metrics); | ||
| for (Map.Entry<String, List<Metric>> entry : byNamespace.entrySet()) { | ||
| MetricsContext context = new MetricsContext(); | ||
| context.setNamespace(entry.getKey()); | ||
| context.putDimension(DimensionSet.of("ThingName", thingName)); | ||
| for (Metric metric : entry.getValue()) { | ||
| if (!(metric.getValue() instanceof Number)) { | ||
| continue; | ||
| } | ||
| context.putMetric(metric.getName(), | ||
| ((Number) metric.getValue()).doubleValue(), | ||
| toEmfUnit(metric.getUnit())); | ||
| } | ||
| try { | ||
| for (String line : context.serialize()) { | ||
| emfLogger.atInfo().log(line); | ||
| } | ||
| } catch (JsonProcessingException e) { | ||
| logger.error("Failed to serialize EMF metrics", e); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private Map<String, List<Metric>> groupByNamespace(List<Metric> metrics) { | ||
| Map<String, List<Metric>> result = new LinkedHashMap<>(); | ||
| for (Metric m : metrics) { | ||
| result.computeIfAbsent(m.getNamespace(), k -> new ArrayList<>()).add(m); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| private static Unit toEmfUnit(TelemetryUnit unit) { | ||
| return UNIT_MAP.getOrDefault(unit, Unit.NONE); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will this tmp file get cleanup after the test?