Meraki丢包및延迟告警至Microsoft Teams
고급
이것은IT Ops, SecOps분야의자동화 워크플로우로, 23개의 노드를 포함합니다.주로 Set, Code, Merge, Redis, HttpRequest 등의 노드를 사용하며. Meraki丢包과延迟告警推送至Microsoft Teams
사전 요구사항
- •Redis 서버 연결 정보
- •대상 API의 인증 정보가 필요할 수 있음
워크플로우 미리보기
노드 연결 관계를 시각적으로 표시하며, 확대/축소 및 이동을 지원합니다
워크플로우 내보내기
다음 JSON 구성을 복사하여 n8n에 가져오면 이 워크플로우를 사용할 수 있습니다
{
"meta": {
"instanceId": "257476b1ef58bf3cb6a46e65fac7ee34a53a5e1a8492d5c6e4da5f87c9b82833",
"templateId": "2054"
},
"nodes": [
{
"id": "3b18d784-eded-4b74-ac44-b25565049e13",
"name": "워크플로 실행 시",
"type": "n8n-nodes-base.manualTrigger",
"position": [
380,
400
],
"parameters": {},
"typeVersion": 1
},
{
"id": "96eadd32-17c2-44b0-a00b-8e2ddcecaafa",
"name": "Meraki 조직 가져오기",
"type": "n8n-nodes-base.httpRequest",
"notes": "This node uses an API call to Meraki using the URL https://api.meraki.com/api/v1/organizations\n\nthe Authorization header is what is used to authenticate. You also have to set it to accept json",
"position": [
620,
320
],
"parameters": {
"url": "https://api.meraki.com/api/v1/organizations",
"options": {
"redirect": {
"redirect": {}
}
},
"sendHeaders": true,
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
}
},
"credentials": {
"httpHeaderAuth": {
"id": "12",
"name": "Header Auth account"
}
},
"typeVersion": 4.1
},
{
"id": "444071ea-a364-45a6-9430-0338d2752b18",
"name": "네트워크 ID 가져오기",
"type": "n8n-nodes-base.httpRequest",
"notes": "This node uses a URL with an expression in the middle to do an API call for each ORG ID that was pulled to pull all the network's for each org",
"position": [
1020,
240
],
"parameters": {
"url": "=https://api.meraki.com/api/v1/organizations/{{ $json.OrgID }}/networks ",
"options": {
"redirect": {
"redirect": {}
}
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization"
},
{
"name": "Accept",
"value": "application/json"
}
]
}
},
"typeVersion": 4.1
},
{
"id": "42deea02-e2d2-4ba6-97f7-698c219715a5",
"name": "조직 이름 및 ID 가져오기",
"type": "n8n-nodes-base.set",
"notes": "This takes the output data from the previous node and changes the variables to better suit what we'll be using. ",
"position": [
840,
240
],
"parameters": {
"fields": {
"values": [
{
"name": "CompanyName",
"stringValue": "={{ $json.name }}"
},
{
"name": "OrgID",
"stringValue": "={{ $json.id }}"
}
]
},
"include": "selected",
"options": {}
},
"typeVersion": 3.2
},
{
"id": "3d5ce6a8-bf05-4457-88e6-c5ad5a995b1d",
"name": "지연을 해당 네트워크와 결합",
"type": "n8n-nodes-base.merge",
"notes": "This node matches on the NetworkID field, so that the networks we pulled earlier and the Loss / Latency can be combined into one dataset",
"position": [
1500,
400
],
"parameters": {
"mode": "combine",
"options": {},
"joinMode": "enrichInput1",
"mergeByFields": {
"values": [
{
"field1": "NetworkID",
"field2": "networkId"
}
]
}
},
"notesInFlow": false,
"typeVersion": 2.1
},
{
"id": "caafd6dd-a2b2-405d-a078-cea3bb615788",
"name": "지연 및 손실 필터링 가능하게 설정",
"type": "n8n-nodes-base.set",
"notes": "Like before, This takes the output data from the previous node and changes the variables to better suit what we'll be using. ",
"position": [
1680,
400
],
"parameters": {
"fields": {
"values": [
{
"name": "networkId",
"stringValue": "={{ $json.networkId }}"
},
{
"name": "NetworkName",
"stringValue": "={{ $json.NetworkName }}"
},
{
"name": "networkURL",
"stringValue": "={{ $json.networkURL }}"
},
{
"name": "Serial",
"stringValue": "={{ $json.serial }}"
},
{
"name": "TS0-Loss",
"stringValue": "={{ $json.timeSeries[0].lossPercent }}"
},
{
"name": "TS1-Loss",
"stringValue": "={{ $json.timeSeries[1].lossPercent }}"
},
{
"name": "TS2-Loss",
"stringValue": "={{ $json.timeSeries[2].lossPercent }}"
},
{
"name": "TS3-Loss",
"stringValue": "={{ $json.timeSeries[3].lossPercent }}"
},
{
"name": "TS4-Loss",
"stringValue": "={{ $json.timeSeries[4].lossPercent }}"
},
{
"name": "TS0-Latency",
"stringValue": "={{ $json.timeSeries[0].latencyMs }}"
},
{
"name": "TS1-Latency",
"stringValue": "={{ $json.timeSeries[1].latencyMs }}"
},
{
"name": "TS2-Latency",
"stringValue": "={{ $json.timeSeries[2].latencyMs }}"
},
{
"name": "TS3-Latency",
"stringValue": "={{ $json.timeSeries[3].latencyMs }}"
},
{
"name": "TS4-Latency",
"stringValue": "={{ $json.timeSeries[4].latencyMs }}"
}
]
},
"include": "selected",
"options": {}
},
"typeVersion": 3.2
},
{
"id": "c695fd89-c884-4794-b0e5-51b91a71ac5c",
"name": "문제 발생 사이트 필터링",
"type": "n8n-nodes-base.code",
"notes": "This node uses JavaScript to look at the calculated averages and if they pass the threshold for 300ms Latency or 2% Loss it will pass that site info forward",
"position": [
2040,
400
],
"parameters": {
"jsCode": "// Function to filter items based on averageLatency and averageLoss\nfunction filterItems(items) {\n return items.filter(item =>\n item.AverageLatency >300 || item.AverageLoss > 2\n );\n}\n\n// Get the input items from the previous node\nconst inputItems = items.map(item => item.json); // Adjust based on your actual data structure\n\n// Filter the items based on the conditions\nconst filteredItems = filterItems(inputItems);\n\n// Return the filtered items to the workflow\nreturn filteredItems.map(item => {\n return { json: item }; // Format each filtered item as JSON\n});\n"
},
"typeVersion": 2
},
{
"id": "6db219fd-f54d-4bf8-b150-9c4f3069cf92",
"name": "5분 평균 지연 및 손실",
"type": "n8n-nodes-base.code",
"notes": "This node uses JavaScript to calculate the average over the last 5 entries of packet loss and latency",
"position": [
1860,
400
],
"parameters": {
"jsCode": "// Assuming $input.all() is an array of items and each item has a json property\nfunction calculateAverages(inputItems) {\n return inputItems.map(item => {\n // Calculate total and average loss\n const totalLoss = \n parseFloat(item.json['TS0-Loss']) +\n parseFloat(item.json['TS1-Loss']) +\n parseFloat(item.json['TS2-Loss']) +\n parseFloat(item.json['TS3-Loss']) +\n parseFloat(item.json['TS4-Loss']);\n const averageLoss = totalLoss / 5;\n item.json['AverageLoss'] = averageLoss;\n\n // Calculate total and average latency\n const totalLatency = \n parseFloat(item.json['TS0-Latency']) +\n parseFloat(item.json['TS1-Latency']) +\n parseFloat(item.json['TS2-Latency']) +\n parseFloat(item.json['TS3-Latency']) +\n parseFloat(item.json['TS4-Latency']);\n const averageLatency = totalLatency / 5;\n item.json['AverageLatency'] = averageLatency;\n\n // Return the modified item\n return item;\n });\n}\n\nreturn calculateAverages($input.all());\n"
},
"typeVersion": 2
},
{
"id": "f3831843-2596-492d-b800-7d349e443293",
"name": "업링크 손실 및 지연 가져오기",
"type": "n8n-nodes-base.httpRequest",
"notes": "This uses a URL with an expression in the middle so that for each org ID it will pull the Loss and Latency for their uplinks. ",
"position": [
840,
400
],
"parameters": {
"url": "=https://api.meraki.com/api/v1/organizations/{{ $json.id }}/devices/uplinksLossAndLatency?timespan=300",
"options": {
"redirect": {
"redirect": {}
}
},
"sendHeaders": true,
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
}
},
"credentials": {
"httpHeaderAuth": {
"id": "12",
"name": "Header Auth account"
}
},
"typeVersion": 4.1
},
{
"id": "87c3f32c-de4f-48fd-a711-3521a008c245",
"name": "일정 트리거",
"type": "n8n-nodes-base.scheduleTrigger",
"notes": "schedules the workflow to run every 5 minutes mon-fri 8am-5pm",
"position": [
380,
240
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "*/5 8-16 * * 1-5"
}
]
}
},
"typeVersion": 1.1
},
{
"id": "572adb5c-6745-4b4e-89d8-a97835c3d486",
"name": "네트워크 변수 설정",
"type": "n8n-nodes-base.set",
"notes": "Like before, This takes the output data from the previous node and changes the variables to better suit what we'll be using. ",
"position": [
1220,
240
],
"parameters": {
"fields": {
"values": [
{
"name": "NetworkID",
"stringValue": "={{ $json.id }}"
},
{
"name": "NetworkName",
"stringValue": "={{ $json.name }}"
},
{
"name": "networkURL",
"stringValue": "={{ $json.url }}"
}
]
},
"include": "selected",
"options": {}
},
"typeVersion": 3.2
},
{
"id": "01717ed3-1012-4eda-9da9-567038132e06",
"name": "병합",
"type": "n8n-nodes-base.merge",
"notes": "This looks at the problematic sites as well as the info from the database. It will pass on all non-matching as if the site name matches with the database then that means we have an open alert for that site already.",
"position": [
2720,
300
],
"parameters": {
"mode": "combine",
"options": {},
"joinMode": "keepNonMatches",
"mergeByFields": {
"values": [
{
"field1": "NetworkName",
"field2": "NetworkName"
}
]
},
"outputDataFrom": "input2"
},
"typeVersion": 2.1
},
{
"id": "a20f6f7c-0c92-4db9-903b-e322d67b536d",
"name": "알림 존재 여부 확인",
"type": "n8n-nodes-base.redis",
"notes": "This node Looks to see if the alert already exists in the Redis database. If it does exist it won't alert us in Teams",
"position": [
2300,
240
],
"parameters": {
"key": "={{ $json.NetworkName }}",
"options": {
"dotNotation": "={{ true }}"
},
"operation": "get",
"propertyName": "NetworkName"
},
"typeVersion": 1,
"alwaysOutputData": true
},
{
"id": "63590446-2938-443a-adbe-bfc0ba89008c",
"name": "응답 생성",
"type": "n8n-nodes-base.code",
"notes": "If the alert isn't in the database all the Redis will respond with is \"null\" \n\nother n8n nodes see null as (no data) and show blank which is fair. So I made this node to look at the null responses and make a response of \"false\" for a \"alertExists\" variable. that way we have something to filter on",
"position": [
2500,
240
],
"parameters": {
"jsCode": "return items.map(item => {\n // Check if the 'NetworkName' property is not null, indicating an alert exists.\n // If 'NetworkName' is null, no alert exists for this network.\n const alertExists = item.json.NetworkName !== null;\n\n // Set the alertExists property correctly based on the condition.\n item.json.alertExists = alertExists;\n\n return item;\n});\n"
},
"typeVersion": 2
},
{
"id": "c63e2a45-bf5e-455f-8012-9868d55aa3e2",
"name": "기술팀에 메시지 전송",
"type": "n8n-nodes-base.microsoftTeams",
"notes": "sends an alert to Dispatch with info related to the site.",
"position": [
2920,
300
],
"parameters": {
"chatId": "19:bfd41d9621e544c88ae9f2f275e373b5@thread.v2",
"message": "=<strong>Loss & Latency Alert</strong> <br><br>\n<strong>Network Name:</strong> <a href=\"{{ $json.networkURL }}\">{{ $json.NetworkName }}</a> <br>\n<strong>Average Loss:</strong> {{ $json.AverageLoss }}% <br>\n<strong>Average Latency:</strong> {{ $json.AverageLatency }} <br> ",
"options": {},
"resource": "chatMessage"
},
"typeVersion": 1.1
},
{
"id": "b4ae54cd-8345-4611-9fbe-32e7537855a9",
"name": "알림 기록",
"type": "n8n-nodes-base.redis",
"notes": "Logs the alert and sets the TTL to 3h, after 3h Redis will delete the entry and if the site is still having issues, the next run of the workflow will notify us again",
"position": [
3120,
300
],
"parameters": {
"key": "={{ $('Merge').item.json.NetworkName }}",
"ttl": 10800,
"value": "={{ $('Merge').item.json.NetworkName }}",
"expire": true,
"keyType": "string",
"operation": "set"
},
"typeVersion": 1,
"alwaysOutputData": true
},
{
"id": "05416de5-a0c0-4ff8-bfc5-3d26f0d88139",
"name": "스티커 노트",
"type": "n8n-nodes-base.stickyNote",
"position": [
573.394087003982,
143.69977924944794
],
"parameters": {
"width": 791.5865288559442,
"height": 462.84878343542437,
"content": "## Pulling in Info \nThis section pulls in all the data we will need to see any possible errors and generate our alert\n"
},
"typeVersion": 1
},
{
"id": "aeb1c8a6-3cec-4899-83e2-098d0b1a9703",
"name": "스티커 노트1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1464,
211
],
"parameters": {
"width": 688.5000872281419,
"height": 411.1258278145692,
"content": "## Changing data\nThis section pulls together the data we got from the first section and sets everything up to be notified "
},
"typeVersion": 1
},
{
"id": "9ca9e89b-61ce-4b3f-af8e-bf03daff6710",
"name": "스티커 노트2",
"type": "n8n-nodes-base.stickyNote",
"position": [
2260,
60
],
"parameters": {
"width": 1015.6997792494475,
"height": 614.8167770419421,
"content": "## Notify\nThis last section is for the push of the alert as well as storing the alert as to not re-notify every time the workflow runs"
},
"typeVersion": 1
},
{
"id": "801879be-e83b-4c74-b8ac-b20157ca32d1",
"name": "스티커 노트3",
"type": "n8n-nodes-base.stickyNote",
"position": [
600,
640
],
"parameters": {
"width": 673.6064168725538,
"height": 394.26386951839356,
"content": "## Explanation\nusing an HTTP request you will can do a get request for all Organizations your Meraki account has access to. \n\nYou will have to Generate your own API key inside of the Meraki Dashboard explained here https://documentation.meraki.com/General_Administration/Other_Topics/Cisco_Meraki_Dashboard_API\n\nYou will have to add two headers to your HTTPS node Authorization and Accept. the 1st is how you'll authenticate with Meraki and the second is how it will know how to answer the request. \n\nUsing the same methods you'll do a get for Organizations, Network IDs and Uplink stats\n\nUsing the Set nodes to organize the data in a \"neat\" way"
},
"typeVersion": 1
},
{
"id": "2b028326-0776-4b9b-bdf2-d79949809092",
"name": "스티커 노트4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1480,
660
],
"parameters": {
"width": 645.9603701592033,
"height": 389.89870424786454,
"content": "## Explanation\nthe Merge node will combine the Networks with their respective stats by matching on NetworkID and networkid and enriching the input \n\nagain we add a set node to better organize the statistics of the uplinks. \n\nThe first JS node will average the 5 Time stamps of Latency and Packet loss \n\nThe last JS node will send the data forward only for sites that pass the threshold (in this example 300ms latency and 2% packet loss) "
},
"typeVersion": 1
},
{
"id": "f7c1448d-3a03-4196-bde6-a1aea92f28ad",
"name": "스티커 노트5",
"type": "n8n-nodes-base.stickyNote",
"position": [
2300,
700
],
"parameters": {
"width": 913.6905067516504,
"height": 523.763772544089,
"content": "## Explanation \nwe will send the problematic sites to both the Redis node and the Merge node. \n\nThe Redis node does a get request to see if a key exists matching the Network name in Meraki. If so it will respond with the Network name, If not it will respond with \"null\" \n\nthe n8n nodes view \"null\" as no data (which isn't exactly wrong as it literally is no data) but the next node will just say the input is blank so I've added the JS node to look at the output and respond if an alert does or doesn't exist based on the response of the Redis node. \n\nThis time we Merge looking at the NetworkName and keep all non-matching. The reason for this is because if they match, that means the key in the database exists, meaning we've already sent a message that the site is having issues. \n\nWe send the tickets forth that don't match and we will send a teams message that will notify of a Network passing the threshold for issues. I've included the Network URL and re-written the message to include a hyperlink that way the alert can be used to take you straight to the problematic site. \n\nFinally we log the site in the database with a TTL of 3h, this way if the error has not been fixed in 3h we will get another message. \n"
},
"typeVersion": 1
},
{
"id": "4f4d40ef-8e7d-420a-8273-4cfefd3af6e9",
"name": "스티커 노트6",
"type": "n8n-nodes-base.stickyNote",
"position": [
1460,
-320
],
"parameters": {
"width": 670.6963066922013,
"height": 366.61782280504275,
"content": "## other usecases \n If you feel confident enough the Teams nodes can be replaced with a node that can generate a ticket for your PSA such as ConnectWise Mange. That way these are generating tickets rather than just messages. "
},
"typeVersion": 1
}
],
"pinData": {},
"connections": {
"01717ed3-1012-4eda-9da9-567038132e06": {
"main": [
[
{
"node": "c63e2a45-bf5e-455f-8012-9868d55aa3e2",
"type": "main",
"index": 0
}
]
]
},
"c63e2a45-bf5e-455f-8012-9868d55aa3e2": {
"main": [
[
{
"node": "b4ae54cd-8345-4611-9fbe-32e7537855a9",
"type": "main",
"index": 0
}
]
]
},
"63590446-2938-443a-adbe-bfc0ba89008c": {
"main": [
[
{
"node": "01717ed3-1012-4eda-9da9-567038132e06",
"type": "main",
"index": 0
}
]
]
},
"444071ea-a364-45a6-9430-0338d2752b18": {
"main": [
[
{
"node": "572adb5c-6745-4b4e-89d8-a97835c3d486",
"type": "main",
"index": 0
}
]
]
},
"87c3f32c-de4f-48fd-a711-3521a008c245": {
"main": [
[
{
"node": "96eadd32-17c2-44b0-a00b-8e2ddcecaafa",
"type": "main",
"index": 0
}
]
]
},
"42deea02-e2d2-4ba6-97f7-698c219715a5": {
"main": [
[
{
"node": "444071ea-a364-45a6-9430-0338d2752b18",
"type": "main",
"index": 0
}
]
]
},
"a20f6f7c-0c92-4db9-903b-e322d67b536d": {
"main": [
[
{
"node": "63590446-2938-443a-adbe-bfc0ba89008c",
"type": "main",
"index": 0
}
]
]
},
"572adb5c-6745-4b4e-89d8-a97835c3d486": {
"main": [
[
{
"node": "3d5ce6a8-bf05-4457-88e6-c5ad5a995b1d",
"type": "main",
"index": 0
}
]
]
},
"96eadd32-17c2-44b0-a00b-8e2ddcecaafa": {
"main": [
[
{
"node": "42deea02-e2d2-4ba6-97f7-698c219715a5",
"type": "main",
"index": 0
},
{
"node": "f3831843-2596-492d-b800-7d349e443293",
"type": "main",
"index": 0
}
]
]
},
"c695fd89-c884-4794-b0e5-51b91a71ac5c": {
"main": [
[
{
"node": "a20f6f7c-0c92-4db9-903b-e322d67b536d",
"type": "main",
"index": 0
},
{
"node": "01717ed3-1012-4eda-9da9-567038132e06",
"type": "main",
"index": 1
}
]
]
},
"f3831843-2596-492d-b800-7d349e443293": {
"main": [
[
{
"node": "3d5ce6a8-bf05-4457-88e6-c5ad5a995b1d",
"type": "main",
"index": 1
}
]
]
},
"6db219fd-f54d-4bf8-b150-9c4f3069cf92": {
"main": [
[
{
"node": "c695fd89-c884-4794-b0e5-51b91a71ac5c",
"type": "main",
"index": 0
}
]
]
},
"3b18d784-eded-4b74-ac44-b25565049e13": {
"main": [
[
{
"node": "96eadd32-17c2-44b0-a00b-8e2ddcecaafa",
"type": "main",
"index": 0
}
]
]
},
"caafd6dd-a2b2-405d-a078-cea3bb615788": {
"main": [
[
{
"node": "6db219fd-f54d-4bf8-b150-9c4f3069cf92",
"type": "main",
"index": 0
}
]
]
},
"3d5ce6a8-bf05-4457-88e6-c5ad5a995b1d": {
"main": [
[
{
"node": "caafd6dd-a2b2-405d-a078-cea3bb615788",
"type": "main",
"index": 0
}
]
]
}
}
}자주 묻는 질문
이 워크플로우를 어떻게 사용하나요?
위의 JSON 구성 코드를 복사하여 n8n 인스턴스에서 새 워크플로우를 생성하고 "JSON에서 가져오기"를 선택한 후, 구성을 붙여넣고 필요에 따라 인증 설정을 수정하세요.
이 워크플로우는 어떤 시나리오에 적합한가요?
고급 - IT 운영, 보안 운영
유료인가요?
이 워크플로우는 완전히 무료이며 직접 가져와 사용할 수 있습니다. 다만, 워크플로우에서 사용하는 타사 서비스(예: OpenAI API)는 사용자 직접 비용을 지불해야 할 수 있습니다.
관련 워크플로우 추천
Google 스프레드시트에서 n8n 사용자 초대 자동화
Google 스프레드시트에서 n8n 사용자 초대 자동화
Set
Code
Merge
+
Set
Code
Merge
11 노드bangank36
IT 운영
↔️ Airtable 일괄 처리
Airtable批量업데이트/插入行(보내기更快+节省API调用请求)
If
Set
Code
+
If
Set
Code
35 노드Simon Mayerhofer
IT 운영
n8n 업데이트
Telegram 승인 시스템을 사용한 Docker 컨테이너 업데이트 자동화
If
Set
Ssh
+
If
Set
Ssh
27 노드Jaber Zare
데브옵스
GPT-4.1, Outlook 및 Mem.ai를 사용한 Microsoft Teams 회의 분석 자동화
GPT-4.1, Outlook 및 Mem.ai를 사용한 자동화된 Microsoft Teams 회의 분석
If
Set
Code
+
If
Set
Code
61 노드Wayne Simpson
인사
EnumX: 서브도메인 자동 DNS 조회 및 Markdown 내보내기
HackerTarget API 및 Gmail 보고서를 사용한 자동화된 서브도메인 DNS 레코드 조회
Set
Code
Gmail
+
Set
Code
Gmail
13 노드Adnan Tariq
보안 운영
Notion 및 Telegram 기반 자동화 SSL 인증서 모니터링 및 갱신
Notion 및 Telegram을 사용한 SSL 인증서 자동 모니터링 및 갱신
If
Set
Ssh
+
If
Set
Ssh
21 노드Frank Chen
보안 운영