Skip to content

Commit 3440414

Browse files
authored
PowerAutomateActivity support (#59)
* Add PowerAutomateActivityType and unit tests for it * Implement PowerAutomateActivityType to NLFPlugin.syslogMessage * Change the order of throwing exceptions in PowerAutomateActivityType.appName * Check if PowerAutomateActivityType's appName regEx is empty and throw Exception - Adds more unit tests for the PowerAutomateActivityType.appName as well * Add test* prefix to two tests in PowerAutomateActivityTypeTest * Add ValidKey to PowerAutomateActivityType
1 parent 55b606d commit 3440414

8 files changed

Lines changed: 712 additions & 0 deletions

src/main/java/com/teragrep/nlf_01/NLFPlugin.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ else if (jsonObject.getString("Type").equals("FunctionAppLogs")) {
118118
else if (jsonObject.getString("Type").equals("LogicAppWorkflowRuntime")) {
119119
eventTypes.add(new LogicAppWorkflowRuntimeType(parsedEvent, realHostname));
120120
}
121+
else if (jsonObject.getString("Type").equals("PowerAutomateActivity")) {
122+
eventTypes.add(new PowerAutomateActivityType(parsedEvent, realHostname));
123+
}
121124
else if (jsonObject.getString("Type").endsWith("fluent_audit_log_events_CL")) {
122125
eventTypes.add(new CCType(parsedEvent, realHostname));
123126
}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
/*
2+
* Teragrep Neon log format plugin for AKV_01
3+
* Copyright (C) 2025 Suomen Kanuuna Oy
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*
18+
*
19+
* Additional permission under GNU Affero General Public License version 3
20+
* section 7
21+
*
22+
* If you modify this Program, or any covered work, by linking or combining it
23+
* with other code, such other code is not for that reason alone subject to any
24+
* of the requirements of the GNU Affero GPL version 3 as long as this Program
25+
* is the same Program as licensed from Suomen Kanuuna Oy without any additional
26+
* modifications.
27+
*
28+
* Supplemented terms under GNU Affero General Public License version 3
29+
* section 7
30+
*
31+
* Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified
32+
* versions must be marked as "Modified version of" The Program.
33+
*
34+
* Names of the licensors and authors may not be used for publicity purposes.
35+
*
36+
* No rights are granted for use of trade names, trademarks, or service marks
37+
* which are in The Program if any.
38+
*
39+
* Licensee must indemnify licensors and authors for any liability that these
40+
* contractual assumptions impose on licensors and authors.
41+
*
42+
* To the extent this program is licensed as part of the Commercial versions of
43+
* Teragrep, the applicable Commercial License may apply to this file if you as
44+
* a licensee so wish it.
45+
*/
46+
package com.teragrep.nlf_01.types;
47+
48+
import com.teragrep.akv_01.event.ParsedEvent;
49+
import com.teragrep.akv_01.plugin.PluginException;
50+
import com.teragrep.nlf_01.PropertiesJson;
51+
import com.teragrep.nlf_01.util.ASCIIString;
52+
import com.teragrep.nlf_01.util.MD5Hash;
53+
import com.teragrep.nlf_01.util.ResourceId;
54+
import com.teragrep.nlf_01.util.ValidKey;
55+
import com.teragrep.nlf_01.util.ValidRFC5424AppName;
56+
import com.teragrep.nlf_01.util.ValidRFC5424Hostname;
57+
import com.teragrep.nlf_01.util.ValidRFC5424Timestamp;
58+
import com.teragrep.nlf_01.util.ValidStringKey;
59+
import com.teragrep.rlo_14.Facility;
60+
import com.teragrep.rlo_14.SDElement;
61+
import com.teragrep.rlo_14.Severity;
62+
import jakarta.json.JsonObject;
63+
import java.time.Instant;
64+
import java.util.HashSet;
65+
import java.util.Set;
66+
import java.util.UUID;
67+
import java.util.regex.Matcher;
68+
import java.util.regex.Pattern;
69+
70+
public final class PowerAutomateActivityType implements EventType {
71+
72+
private final ParsedEvent parsedEvent;
73+
private final String realHostname;
74+
private final Pattern appNamePattern;
75+
76+
public PowerAutomateActivityType(final ParsedEvent parsedEvent, final String realHostname) {
77+
this(parsedEvent, realHostname, Pattern.compile("/(?<=/environments/)(?<environment>.*?)(?=/flows/)/"));
78+
}
79+
80+
public PowerAutomateActivityType(
81+
final ParsedEvent parsedEvent,
82+
final String realHostname,
83+
final Pattern appNamePattern
84+
) {
85+
this.parsedEvent = parsedEvent;
86+
this.realHostname = realHostname;
87+
this.appNamePattern = appNamePattern;
88+
}
89+
90+
@Override
91+
public Severity severity() {
92+
return Severity.NOTICE;
93+
}
94+
95+
@Override
96+
public Facility facility() {
97+
return Facility.AUDIT;
98+
}
99+
100+
@Override
101+
public String hostname() throws PluginException {
102+
final JsonObject record = parsedEvent.asJsonStructure().asJsonObject();
103+
104+
final ValidKey<String> validKey = new ValidStringKey(record, "_Internal_WorkspaceResourceId");
105+
106+
final String resourceId = validKey.value();
107+
108+
return new ValidRFC5424Hostname(
109+
"md5-".concat(new MD5Hash(resourceId).md5().concat("-").concat(new ASCIIString(new ResourceId(resourceId).resourceName()).withNonAsciiCharsRemoved()))
110+
).hostnameWithInvalidCharsRemoved();
111+
112+
}
113+
114+
@Override
115+
public String appName() throws PluginException {
116+
final JsonObject record = parsedEvent.asJsonStructure().asJsonObject();
117+
118+
final ValidKey<String> validKey = new ValidStringKey(record, "FlowDetailsUrl");
119+
120+
final Matcher matcher = appNamePattern.matcher(validKey.value());
121+
if (!matcher.find()) {
122+
throw new PluginException("Could not parse environment from FlowDetailsUrl");
123+
}
124+
125+
final String environment = matcher.group("environment");
126+
if (environment == null || environment.isEmpty()) {
127+
throw new PluginException("Capture group 'environment' was not found");
128+
}
129+
130+
// Prepend 'PowerAA_' before the actual environment name. AA standing for AutomateActivity
131+
return new ValidRFC5424AppName(new ASCIIString("PowerAA_" + environment).withNonAsciiCharsRemoved()).appName();
132+
}
133+
134+
@Override
135+
public long timestamp() throws PluginException {
136+
final JsonObject record = parsedEvent.asJsonStructure().asJsonObject();
137+
138+
final ValidKey<String> validKey = new ValidStringKey(record, "TimeGenerated");
139+
140+
return new ValidRFC5424Timestamp(validKey.value()).validTimestamp();
141+
}
142+
143+
@Override
144+
public Set<SDElement> sdElements() throws PluginException {
145+
final Set<SDElement> elems = new HashSet<>();
146+
final String time;
147+
if (!parsedEvent.enqueuedTimeUtc().isStub()) {
148+
time = parsedEvent.enqueuedTimeUtc().zonedDateTime().toString();
149+
}
150+
else {
151+
time = "";
152+
}
153+
154+
final String fullyQualifiedNamespace;
155+
final String eventHubName;
156+
final String partitionId;
157+
final String consumerGroup;
158+
if (!parsedEvent.partitionCtx().isStub()) {
159+
fullyQualifiedNamespace = String
160+
.valueOf(parsedEvent.partitionCtx().asMap().getOrDefault("FullyQualifiedNamespace", ""));
161+
eventHubName = String.valueOf(parsedEvent.partitionCtx().asMap().getOrDefault("EventHubName", ""));
162+
partitionId = String.valueOf(parsedEvent.partitionCtx().asMap().getOrDefault("PartitionId", ""));
163+
consumerGroup = String.valueOf(parsedEvent.partitionCtx().asMap().getOrDefault("ConsumerGroup", ""));
164+
}
165+
else {
166+
fullyQualifiedNamespace = "";
167+
eventHubName = "";
168+
partitionId = "";
169+
consumerGroup = "";
170+
}
171+
172+
elems
173+
.add(new SDElement("aer_02_partition@48577").addSDParam("fully_qualified_namespace", fullyQualifiedNamespace).addSDParam("eventhub_name", eventHubName).addSDParam("partition_id", partitionId).addSDParam("consumer_group", consumerGroup));
174+
175+
elems
176+
.add(new SDElement("event_id@48577").addSDParam("uuid", UUID.randomUUID().toString()).addSDParam("hostname", realHostname).addSDParam("unixtime", Instant.now().toString()).addSDParam("id_source", "aer_02"));
177+
178+
final String partitionKey;
179+
if (!parsedEvent.systemProperties().isStub()) {
180+
partitionKey = String.valueOf(parsedEvent.systemProperties().asMap().getOrDefault("PartitionKey", ""));
181+
}
182+
else {
183+
partitionKey = "";
184+
}
185+
186+
final String offset;
187+
if (!parsedEvent.offset().isStub()) {
188+
offset = parsedEvent.offset().value();
189+
}
190+
else {
191+
offset = "";
192+
}
193+
194+
elems
195+
.add(new SDElement("aer_02_event@48577").addSDParam("offset", offset).addSDParam("enqueued_time", time).addSDParam("partition_key", partitionKey).addSDParam("properties", new PropertiesJson(parsedEvent.properties()).toJsonObject().toString()));
196+
197+
elems
198+
.add(new SDElement("aer_02@48577").addSDParam("timestamp_source", time.isEmpty() ? "generated" : "timeEnqueued"));
199+
200+
elems.add(new SDElement("nlf_01@48577").addSDParam("eventType", this.getClass().getSimpleName()));
201+
202+
return elems;
203+
}
204+
205+
@Override
206+
public String msgId() throws PluginException {
207+
final String sequenceNumber;
208+
if (!parsedEvent.systemProperties().isStub()) {
209+
sequenceNumber = String.valueOf(parsedEvent.systemProperties().asMap().getOrDefault("SequenceNumber", ""));
210+
}
211+
else {
212+
sequenceNumber = "";
213+
}
214+
return sequenceNumber;
215+
}
216+
217+
@Override
218+
public String msg() throws PluginException {
219+
return parsedEvent.asString();
220+
}
221+
}

src/test/java/com/teragrep/nlf_01/NLFPluginTest.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,64 @@ void functionAppLogsType() {
575575
Assertions.assertTrue(sdElementMap.get("aer_02_event@48577").containsKey("properties"));
576576
}
577577

578+
@Test
579+
void powerAutomateActivityType() {
580+
final String json = Assertions
581+
.assertDoesNotThrow(() -> Files.readString(Paths.get("src/test/resources/powerautomateactivity.json")));
582+
final ParsedEvent parsedEvent = new ParsedEventFactory(
583+
new UnparsedEventImpl(json, new EventPartitionContextImpl(new HashMap<>()), new EventPropertiesImpl(new HashMap<>()), new EventSystemPropertiesImpl(new HashMap<>()), new EnqueuedTimeImpl("2020-01-01T00:00:00"), new EventOffsetImpl("0"))
584+
).parsedEvent();
585+
586+
final NLFPlugin plugin = new NLFPlugin(new FakeSourceable());
587+
final List<SyslogMessage> syslogMessages = Assertions
588+
.assertDoesNotThrow(() -> plugin.syslogMessage(parsedEvent));
589+
Assertions.assertEquals(1, syslogMessages.size());
590+
591+
final SyslogMessage syslogMessage = syslogMessages.get(0);
592+
Assertions
593+
.assertEquals(
594+
"{\n" + " \"ActorName\": \"[email protected]\",\n"
595+
+ " \"ActorUserId\": \"bb41a487-309b-4d21-9ab8-2a8b948b2d18\",\n"
596+
+ " \"ActorUserType\": \"Admin\",\n" + " \"AdditionalInfo\": \"{}\",\n"
597+
+ " \"EventOriginalType\": \"OriginalType\",\n"
598+
+ " \"EventOriginalUid\": \"bb41a487-309b-4d21-9ab8-2a8b948b2d18\",\n"
599+
+ " \"EventResult\": \"Succeeded\",\n" + " \"FlowConnectorNames\": \"Connector\",\n"
600+
+ " \"FlowDetailsUrl\": \"https://{uri1}/{uri2}/environments/EXAMPLE_FLOW_1/flows/{uri3}\",\n"
601+
+ " \"LicenseDisplayName\": \"License1\",\n"
602+
+ " \"ObjectId\": \"bb41a487-309b-4d21-9ab8-2a8b948b2d18\",\n"
603+
+ " \"OrganizationId\": \"bb41a487-309b-4d21-9ab8-2a8b948b2d18\",\n"
604+
+ " \"RecipientUpn\": \"bb41a487-309b-4d21-9ab8-2a8b948b2d18\",\n"
605+
+ " \"RecordType\": \"exchangeAdmin\",\n" + " \"SharingPermission\": \"2\",\n"
606+
+ " \"SourceSystem\": \"Azure\",\n" + " \"SrcIpAddr\": \"127.0.0.1\",\n"
607+
+ " \"Type\": \"PowerAutomateActivity\",\n"
608+
+ " \"TimeGenerated\": \"2025-10-06T00:00:00.0000000Z\",\n"
609+
+ " \"_ItemId\": \"bb41a487-309b-4d21-9ab8-2a8b948b2d18\",\n"
610+
+ " \"TenantId\": \"bb41a487-309b-4d21-9ab8-2a8b948b2d18\",\n"
611+
+ " \"UserUpn\": \"bb41a487-309b-4d21-9ab8-2a8b948b2d18\",\n"
612+
+ " \"Workload\": \"Service1\",\n"
613+
+ " \"_ResourceId\": \"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}\",\n"
614+
+ " \"_SubscriptionId\": \"bb41a487-309b-4d21-9ab8-2a8b948b2d18\",\n"
615+
+ " \"_TimeReceived\": \"2025-10-06T00:00:00.0000000Z\",\n"
616+
+ " \"_Internal_WorkspaceResourceId\": \"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}\"\n"
617+
+ "}",
618+
syslogMessage.getMsg()
619+
);
620+
Assertions.assertEquals("md5-0ded52ef915af563e25778bf26b0f129-resourceName", syslogMessage.getHostname());
621+
Assertions.assertEquals("PowerAA_EXAMPLE_FLOW_1", syslogMessage.getAppName());
622+
Assertions.assertEquals("2025-10-06T00:00:00Z", syslogMessage.getTimestamp());
623+
624+
final Map<String, Map<String, String>> sdElementMap = syslogMessage
625+
.getSDElements()
626+
.stream()
627+
.collect(Collectors.toMap((SDElement::getSdID), (sdElem) -> sdElem.getSdParams().stream().collect(Collectors.toMap(SDParam::getParamName, SDParam::getParamValue))));
628+
629+
Assertions.assertEquals(1, sdElementMap.get("nlf_01@48577").size());
630+
Assertions
631+
.assertEquals(PowerAutomateActivityType.class.getSimpleName(), sdElementMap.get("nlf_01@48577").get("eventType"));
632+
633+
Assertions.assertTrue(sdElementMap.get("aer_02_event@48577").containsKey("properties"));
634+
}
635+
578636
@Test
579637
void testPostgreSQLType() {
580638
final String json = Assertions

0 commit comments

Comments
 (0)