עקיבה, רכיב קריטי, מעקבת אחר בקשות דרך מערכות מורכבות. תצוגה זו חושפת בקבוקי צוואר ושגיאות, ומאפשרת פתרונות מהירים יותר. בפוסט קודם של הסדרה שלנו על שירותי אינטרנט ב-Go, חקרנו את החשיבות של נראות. היום, אנחנו מתמקדים בעקיבה. Jaeger אוספת, שומרת ומציגת עקבות ממערכות מבוזרות. היא מספקת תובנות קריטיות לגבי זרימת בקשות בין שירותים. על ידי שילוב Jaeger עם OpenTelemetry, מפתחים יכולים לאחד את גישת העקיבה שלהם, ולבטח נראות עקבית ומקיפה. שילוב זה מפשט אבחון של בעיות ביצועים ומשפר את אמינות המערכת. בפוסט הזה, נערוך הגדרות ל-Jaeger, נשלב אותה עם OpenTelemetry באפליקציה שלנו, ונחקור הצגה של עקבות לתובנות עמוקות יותר.
מוטיבציה
מה שאנחנו עובדים בכיוון אליו הוא לוח בקרה של Jaeger שנראה כך:
כאשר אנחנו הולכים לחלקים שונים של האפליקציה (בחזית של Onehub), העקבות של הבקשות השונות נאספות (מהנקודה של הפגיעה ב-grpc-gateway) עם סיכום של כל אחת מהן. אפילו נוכל לחדור לאחת העקבות לתצוגה מפורטת יותר. תראו את הבקשה הראשונה POST
(ליצירה/שליחת הודעה בנושא):
כאן אנחנו רואים את כל הרכיבים שהבקשה Create
נוגעת בהם יחד עם זמני הכניסה/יציאה שלהם והזמן שנלקח בתוך ומחוץ לשיטות. מאוד חזק אכן.
התחלה
TL;DR: כדי לראות את זה בפעולה ולאמת את שאר הבלוג:
- מקור זה בענף PART11_TRACING.
- בונה את כל הדברים הנדרשים (אחרי שהוצאת את הענף):
make build
- שברנו את docker-compose לשני חלקים (על זה נדבר בהמשך), אז ודא שיש לך שתי חלונות פעולה.
- טרמינל 1:
make updb dblogs
- טרמינל 2:
make up logs
- עבור localhost:7080 וזה כל הדרך.
סקירה גבוהה ברמה
המערכת שלנו ברגע זה:
עם הדגמציה של OpenTelemetry, המערכת שלנו תהפך ל:
כפי ששמענו קודם, לשירות כל אחד להשתמש בלקוחות נפרדים כדי לשלוח לספקים ספציפיים זה די מאתגר. במקום זאת, עם משדר של OTel שמופעל באופן נפרד, אנחנו יכולים לוודא שכל (השירותים המעניינים) יכולים פשוט לשלוח מדדים/מעברים/עקבות למשדר זה, שאז יכול להעביר אותם למערכות הרצאה הנדרשות — במקרה זה, Jaeger עבור עקבות.
בואו נתחיל.
הגדרת משדר הOTel
הצעד הראשון הוא להוסיף את המשדר הOTel שמופעל בסביבת Docker ביחד עם Jaeger כך שהם נגישים.
הערה: שברנו את ההגדרה המקיפה המקורית שלdocker-compose.yml
לשני חלקים:
- db-docker-compose.yml: מכיל את כל הרכיבים הקשורים לבases ולמערכות התשתית (שאינם שירותים יישומיים) כמו בases (Postgres, Typesense) ושירותים הנערכים על מנת השגחה (מ
- docker-compose.yml: מכיל את כל השירותים הקשורים ליישום (Nginx, gRPC Gateway, dbsync, frontend וכו')
שני סביבות docker-compose מחוברות על ידי רשת משותפת (onehubnetwork
) שדרכה שירותים בסביבות אלו יכולים לתקשר אחד עם השני. עם הפרדה זו, אנו רק צריכים לאתחל מחדש תת-קבוצה של שירותים עם שינויים, מה שמאיץ את הפיתוח שלנו.
חזרה להגדרתנו: ב-db-docker-compose.yml
שלנו, הוסף את השירותים הבאים:
services
...
otel-collector
networks
onehubnetwork
image otel/opentelemetry-collector-contrib0.105.0
command"--config=/etc/otel-collector.yaml"
volumes
./configs/otel-collector.yaml:/etc/otel-collector.yaml
environment
POSTGRES_DB $ POSTGRES_DB
POSTGRES_USER $ POSTGRES_USER
POSTGRES_PASSWORD $ POSTGRES_PASSWORD
jaeger
networks
onehubnetwork
image jaegertracing/all-in-one1.59
container_name jaeger
environment
QUERY_BASE_PATH'/jaeger'
COLLECTOR_OTLP_GRPC_HOST_PORT'0.0.0.0:4317'
COLLECTOR_OTLP_HTTP_HOST_PORT'0.0.0.0:4318'
COLLECTOR_OTLP_ENABLEDtrue
prometheus
networks
onehubnetwork
image prom/prometheus v2.53.1
command
'--config.file=/etc/prometheus/prometheus.yml'
'--web.external-url=/prometheus/'
'--web.route-prefix=/prometheus/'
volumes
./configs/prometheus.yaml:/etc/prometheus/prometheus.yml
ports
9090:9090
פשוט מספיק, זה מגדיר שני שירותים בסביבת ה-Docker שלנו:
otel-collector
: הכלי של כל האותות (מדדים/יומנים/עקבות) שנשלחו על ידי השירותים השונים שנפקחים (נמשיך להוסיף לרשימה הזו במשך הזמן), הואיל), הוא משתמש בתמונת OTel הסטנדרטית יחד עם הגדרת OTel המותאמת אישית שלנו (להלן) שמתארת צינורות תצפית שונים (כלומר, איך אותות צריכים להתקבל, לעובד ולייצא בדרכים שונות).jaeger
: המקרה שלנו של Jaeger שיקבל ויאחסן עקבות (מיוצאות על ידיotel-collector
), זה מארח הן את האחסון והן את הלוח המוצג (UI) שנייצא בקידומת הנתיב HTTP/jaeger
כדי להיות נגיש דרך nginx.prometheus
: אף על פי שאינו נדרש לפוסט הזה, נייצא גם מדדים כך שניתן יהיה לגרוף אותם על ידי Prometheus. לא נדון בפרט בפוסט הזה.
כמה דברים לשים לב:
- אף על פי שאינו נדרש לפוסט הזה, אנו מעבירים את פרטי החיבור של
POSTGRES
(כמשתני סביבה) ל-otel-collector
כך שיוכל לגרוף מדדי בריאות של Postgres. - ג'ייגר (מאחר ו1.35) מסupports OTLP באופן טבעי.
- היופי של OTLP הוא שקולטי OTel יכולים להיות חוצים בצורה מעגלית וליצור רשת של קולטים/מעבדים/מעבירים/נתבים של OTel.
- OTLP יכול להיות משורה באמצעות קצת GRPC או HTTP (על נמלים 4317 ו4318 בהעדר).
- בהתבסס, השירותים של OTLP מותחים ב
localhost:4317/4318
. זה נוראי אם Jaeger נרץ על אותה מאשה/פוד בה מופעלים השירותים המנוטרים. אך בגלל שJaeger נרץ על פוד נפרד, הם חייבים להיות קשורים לכתוביות חיצוניות (0.0.0.0). זה לא היה ברור במדריך ההגדרות וזה הוביל להתמודדות משמעותית עם השיער. COLLECTOR_OTLP_ENABLED: true
הוא עכשיו הבריר ולא צריך להיות מסומן בבירור.
הגדרות OTel
OTel גם צריך להיווצר עם קולטים, מעבדים ומספקים ספציפיים. אנחנו נעשה את זה בconfigs/otel-collector.yaml.
הוספת קולטים
אנחנו צריכים לספר לקולט של OTel איזה קולטים צריכים להיות מופעלים. זה מסוגל להיות ספציפי באחד המחלקות receivers
:
receivers
otlp
protocols
http
endpoint 0.0.0.04318
grpc
endpoint 0.0.0.04317
postgresql
endpoint postgres5432
transport tcp
username $ POSTGRES_USER
password $ POSTGRES_PASSWORD
databases
$ POSTGRES_DB
collection_interval 10s
tls
insecuretrue
זוהי פעולה שמעוררת קולטן OTLP על פורטים 4317 ו-4318 (grpc
, http
בעצם). קולטנים רבים בעלי סוגים שונים יכולים להתחילו. כמו דוגמה, גם הוספנו קולטן "postgresql
" שיסקוף פעם פעמית את המדגם של Postgres עבור מדגמים (למרות שזה אינו רלוונטי למשוב הזה). קולטנים יכולים גם להיות בעלי עיסוק בדרך משוך או של הולך-אל, קולטנים בעלי עיסוק בדרך משוך מתקבלים במחזורים וסקוף מטרות ספציפיות (לדוגמה, postgres
), בעוד קולטנים בעלי עיסוק בדרך של הולך-אל מקשיבים ומקבלים מדגמים/לוגים/עוקבות מיישמים בעזרת את הSDK הלקוחותי של OTel.
זה הכל. עכשיו האוסף שלנו מוכן לקבל (או לסקוף) את המדגמים הנראים הנכונים.
הוסף מעבדים
מעבדים בOTel הם דרך לשנות, מפית, להרכיב, לסנן ואולי למלא עוד את האותות שאנחנו מקבלים לפני שליחתם. לדוגמה, מעבדים יכולים לסקוף מדגמים, לסנן לוגים או אפילו להרכיב אותם לביצועים יעילים. במצב ברת המצב, אין מעבדים נוספים (עם זאת, האוסף הוא דרך דרך ברת המצב). אנחנו נתעלם מזה עכשיו.
הוסף משדרים
עכשיו הגיע הזמן לזהות לאן אנחנו רוצים שהאותות ייוצאו: בקצוות אחוריים שהכי מתאימים לאותות השונים. כמו מקלטים, גם מייצאים יכולים להיות בעלי משיכה או דחיפה. מייצאים בעלי דחיפה משמשים לשליחת אותות למקלט אחר שפועל במצב דחיפה. אלה הם יוצאים. מייצאים בעלי משיכה חושפים קצוות שניתן לגרד על ידי מקלטים אחרים בעלי משיכה (לדוגמה, prometheus
). נוסיף מייצא מכל סוג: אחד לעקוב ואחד שפרומתאוס יגרד ממנו (אף על פי שפרומתאוס אינו נושא הפוסט הזה):
exporters
otlp/jaeger
endpoint jaeger4317
tls
insecuretrue
prometheus
endpoint 0.0.0.09090
namespace onehub
debug
כאן יש לנו מייצא ל-Jaeger שרץ את האוסף OTLP, כפי שמצוין על ידי otlp/jaeger
. מייצא זה ידחוף עקבות באופן קבוע ל-Jaeger. אנחנו גם מוסיפים קצה "גורד" בפורט 9090 שפרומתאוס יגרד ממנו באופן קבוע.
המייצא "debug" פשוט משמש לפליטת אותות לזרמי פלט/שגיאה סטנדרטיים.
הגדרת צינורות
החלקים מקלט, מעבד ומייצא פשוט מגדירים את המודולים שיופעלו על ידי האוסף. הם עדיין לא מופעלים. כדי להפעיל/להפעיל אותם באמת, עליהם להיות מופנים כ"צינורות". צינורות מגדירים איך אותות זורמים ומעובדים על ידי האוסף. הגדרות הצינורות שלנו (בחלק services
) יבהירו זאת:
service
extensions health_check
pipelines
traces
receivers otlp
processors
exporters otlp/jaeger debug
metrics
receivers otlp
processors
exporters prometheus debug
כאן אנחנו מגדירים שני צינורות. שימו לב כמה דומים הצינורות אך מאפשרים שתי מצבי ייצוא שונים (Jaeger ו-Prometheus). עכשיו אנחנו רואים את כוחו של OTel וביצירת צינורות בתוכו.
traces
:
- קבלת אותות מ-SDKs של לקוחות
- ללא עיבוד
- עוברת העקבות ל控制台 ו Jaeger
metrics
:
- מקבלת אותן מ SDK הלקוחות
- אין עיבוד
- עוברת המדידות ל控制台 ו Prometheus (על ידי חיצוף קו מסע לזה).
חיצוף לוחות דרך Nginx
Jaeger מספק לוח עבור הדמות נתוני העקבות על כל הבקשות שלנו. זה ניתן להסתכל בדפדפן על ידי הפעלת אחד הדברים הבאים ב ערימת Nginx שלנו. שוב, למרות שזה לא נושא המאמר הזה – אנחנו גם מחיצים את מסך השימוש של Prometheus דרך nginx בתווך הפתח הראשי לנתונים HTTP /prometheus
.
...
location ~ ^/jaeger
if ($request_method = OPTIONS ) return 200;
proxy_pass http://jaeger:16686; # Note that JaegerUI starts on port 16686 by default
proxy_pass_request_headers on;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header Host-With-Port $http_host;
proxy_set_header Connection '';
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-HTTPS on;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Prefix /;
proxy_http_version 1.1;
chunked_transfer_encoding off;
location ~ ^/prometheus
if ($request_method = OPTIONS ) return 200;
proxy_pass http://prometheus:9090;
proxy_pass_request_headers on;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header Host-With-Port $http_host;
proxy_set_header Connection '';
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-HTTPS on;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Prefix /;
proxy_http_version 1.1;
chunked_transfer_encoding off;
...
הדמות עקבות ב Jaeger
מסך העברת העקבות ב Jaeger הוא די ממושך ויש בו מספר תכונות שאתה יכול לחקור. ניווט למסך העברת בדפדפן. תראו מסך מקיף לחיפוש וניתוח עקבות. ניתן לחפש עקבות מסויימים באמצעות קריטריונים ולסנן על פי שירות, תקופות זמן, רכיבים וכו'.
ניתן להיות את הקונספטורים של העקבות בבקשות השונות כדי להבין את סדר העיבודים. כל מסגרת מייצגת יחידת עבודה, מראה זמן ההתחלה והסיום, משך הזמן ומידע קשור. הראשון המפורט הזה מאוד מועיל בזמן זה לזהות את הבלגנים המוגבלים בביצועים והשגיאות בתוך העקבה.
אינטגרציית SDK הלקוחות
עד כה, הקוד שלנו מוגדר עבור הסוגים הבאים של תפקידים: חישוף וצריכה של אותם אותות. אך השירותים שלנו עדיין לא עודכנו עבור פלטת האותות ל-OTel. בדברים האלה אנחנו נשלב עם ה-SDK הלקוחותי (Golang) בחלקים מסויימים של הקוד שלנו. המדריך ל-SDK SDK הוא מקום נהדר בו לראשונה תהיה מוכרחה להתמודד עם חלק מהתפיסות.
התפיסות המפתחים שאנחנו נתמודד עם מופיעות למטה.
משאבים
משאבים הם היצירה שמייצרת את האותות. במקרה שלנו, המגבלה של המשאב היא הייצור הבינארי של השירותים. כרגע, יש לנו משאב יחיד עבור כל השירות Onehub, אך יכול להיות מפורץ מאוחר יותר.
זה מוגדר בcmd/backend/obs.go. שימו לב שהSDK לקוחות לא צריך מאיתנו להיכנס לפרטים של ההגדרה של המשאב באופן מובהק. הסיוע הסטנדרטי (sdktrace.WithResource
) מאפשר לנו ליצור הגדרה למשאב על-ידי הבנה של החלקים הכי מועילים (כמו שם תהליך, שם תיבת פולים וכו') בזמן ההתנהגות.
אנחנו נצטרך רק להחליף דבר אחד: המשתנה הסביבתי OTEL_RESOURCE_ATTRIBUTES: service.name=onehub.backend,service.version=0.0.1
עבור השירות onehub
ב-docker-compose.yml.
המשך…
התעברות ההקשריםהיא נושא חשוב מאוד באובסביליביליות. העמודים השונים מסוגלים להיות חזקים במעלה בהיכולתנו לקשר אותם בין עצמם בזמן זיהוי הבעיות במערכת שלנו. תחשבו על ההקשרים כעל ביטים נוספים שיכולים להיות מוצגים בקשר לאותם אותות: למשל, יכולים להיות "מחברים" בדרך ייחודית לתפקיד הקשר בין האותות השונים לקבוצה מסוימת (לדוגמה, בקשה).
ספקים/מוציאים
עבור כל אחד מהאותות, אוטל מספק משף פועל (למשל, TracerProvider
עבור ייצא החלקים/העקבות, MeterProvider
עבור ייצא המדדים, LoggerProvider
עבור ייצא המעברים, וכך הלאה). עבור כל אחד מהמשפים האלה, יכולים להיות מספר הימושים, למשל, ספק דיבוג לשליחת לזרימות stdout/err, ספק אוטל עבור ייצא לנקודת קשר אוטל נוספת (בשרשרת), או אפילו ישירות דרך מגוון של מוציאים. אך במקרה שלנו, אנחנו רוצים לדחות את הבחירה של כל ספקים מחוץ לשירותים שלנו ובמקום זה לשלוח את כל האותות למשלהב אוטל שמקיים בסביבת ההתנהגויות שלנו.
כדי להסתיר את הקשר הזה ניצור סוג "OTELSetup
" שישמר על המשפים השונים שאולי נרצה להשתמש או להחליף. ב cmd/backend/obs.go, יש לנו:
type OTELSetup[C any] struct {
ctx context.Context
shutdownFuncs []ShutdownFunc
Resource *resource.Resource
Context C
SetupPropagator func(o *OTELSetup[C])
SetupTracerProvider func(o *OTELSetup[C]) (trace.TracerProvider, ShutdownFunc, error)
SetupMeterProvider func(o *OTELSetup[C]) (otelmetric.MeterProvider, ShutdownFunc, error)
SetupLogger func(o *OTELSetup[C]) (logr.Logger, ShutdownFunc, error)
SetupLoggerProvider func(o *OTELSetup[C]) (*log.LoggerProvider, ShutdownFunc, error)
}
זוהי קידמה פשוטה שמקישה אחר היבטים רגולריים בהם צריך לשימוש ב SDK OTel. כאן ישנם ספקנים (סירוף דיבור, סירוף עיסקים, ומדדים) בנוסף לדרכים לספק הקשר (עבור עיסקים). המשאבה הכי גדולה שמשמשת כל הספקנים גם מסוגלה כאן. פונקציות הכיבוי מענינות. הן פונקציות שמוצעות על ידי הספקנים כשהאקספורטר הבסיסי נסגר (באופן מלאך או בגלל יציאה). עצמת הקידמה הזו מקבלת מבנה גנרי כך שמייצרים מותאמים לעצמם יכולים להשתמש במידע המותאם להם.
המאגר מכיל שתי ביצועים של זה:
- הודעות בסיסיות לפירוט/שגיאה – cmd/backend/stdout.go
- עידוד למשק אחר ב-OTel – cmd/backend/otelcol.go
אנחנו ניצור את השני בתוכנה שלנו. לא נדבר על פרטי הביצועים הספציפיים בגלל שהם נלקחו מהדוגמאותב SDK עם תיקון קטן ורפאקציה. בעיקר, תסתכלו על otel-collector הדוגמה להשראה.
מתקין ספקנים אוטל.
המהות של הפעילות המאפשרת לאסף בשירותים שלנו היא שסוג כלשהו של "ההקשר המקורי" של OTel מותחדש בכל נקודת הכניסה. אם ההקשר נוצר בהתחלה, הוא יישלח לכל המטרות המוצגות כאן, שאחר כך יתפשט (כל עוד אנחנו מבצעים את הדבר הנכון).
בואו נחשוב על הפעילות הפשוטה של ListTopics
בקשת API (api/vi/topics
), הבקשה שלנו לוקחת את המסלול הבא וחזרה:
[ Browser ] ---> [ Nginx ] ---> [ gRPC Gateway ] ---> [ gRPC Service ] ---> [ Database ]
במקרה שלנו, הנקודות הכניסה כאן הן בהתחלה כשהגרPC גייטד מקבל בקשת API מ Nginx (אנחנו יכולים להתחיל לעקוב אחריהם מהנקודה בה הבקשת HTTP מגיעה ל Nginx על מנת להדגיש את הטרחה ב Nginx, אך נשאר על זה לרגע).
מה שנדרש:
- הגרPC גייטד מקבל בקשה.
- הוא יוצר מבנה "מותאם" של
context.Context
של OTel. - הוא יוצר קישור מותאם אל השירות הגרPC הנאמר (לדוגמה,
TopicService
) ומעביר את ההקשר הזה במקום הברירת מחדל. - השירות הנאמר ישתמש בהקשר הזה כשהוא מפרץ את המעקבים.
בואו נעבור דרך את הצעדים האלה בדיוק.
התחלת והכנה של ה SDK של OTel לשימוש
ב main.go, בואו נתחיל בהתחלת החיבור לאסף:
func main() {
flag.Parse()
// Handle SIGINT (CTRL+C) gracefully.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
collectorAddr := cmdutils.GetEnvOrDefault("OTEL_COLLECTOR_ADDR", "otel-collector:4317")
conn, err := grpc.NewClient(collectorAddr,
// Note the use of insecure transport here. TLS is recommended in production.
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Println("failed to create gRPC connection to collector: %w", err)
return
}
setup := NewOTELSetupWithCollector(conn)
err = setup.Setup(ctx)
if err != nil {
log.Println("error setting up otel: ", err)
}
defer func() {
err = setup.Shutdown(context.Background())
}()
ohdb := OpenOHDB()
srvErr := make(chan error, 2)
httpSrvChan := make(chan bool)
grpcSrvChan := make(chan bool)
go startGRPCServer(*addr, ohdb, srvErr, httpSrvChan)
go startGatewayServer(ctx, *gw_addr, *addr, srvErr, grpcSrvChan)
// Wait for interruption.
select {
case err = <-srvErr:
log.Println("Server error: ", err)
// Error when starting HTTP server or GRPC server
return
case <-ctx.Done():
// Wait for first CTRL+C.
// Stop receiving signal notifications as soon as possible.
stop()
}
// When Shutdown is called, ListenAndServe immediately returns ErrServerClosed.
httpSrvChan <- true
grpcSrvChan <- true
...
}
- שורות 8-16: אנו יוצרים חיבור ל
otel-collector
שרץ בסביבת Docker שלנו. - שורות 17-21: אנו מתקינים את ההגדרות של OTel עם טרייסר וספקי מטריקות, כך שהקולקטור שלנו יוכל עכשיו לדחוף את כל הטרייסים והמטריקות (נזכרו שהגדרנו קולטים בקונפיג של OTel)).
- שורות 23-25: הגדרנו סיומי חיבורים לניקוי חיבורים וספקים של OTel בעת סגירה.
- שורה 27: הגדרנו את ה-DB והחיבורים כמקודם.
- שורות 29+: בעבר, התחלנו את שירותי GRPC ו-Gateway ברקע ולא היינו מעוניינים במיוחד בסטטוס החזרה או היציאה שלהם. למערכת עמידה יותר, חשוב להיות בעלי הבנה טובה יותר בנוגע למחזור החיים של השירותים שאנו מתחילים. כך, עכשיו אנו מעבירים את ערוץ ה"callback" לכל אחד מהשירותים שאנו מתחילים. כאשר השרתים יוצאים, השיטות הרלוונטיות י�ראו בחזרה על אותם ערוצים הזמינים להם כדי להודיע שהם יצאו בצורה חלקה. הבינארי שלנו יסתיים כאשר אחד מהשירותים הללו יצא.
כדוגמה, בואו נראה כיצד שירות הגייטוויי שלנו מנצל את הערוץ הזה.
במקום להתחיל את שרת HTTP (לגייטוויי-GRPC) כ:
1 http.ListenAndServe(gw_addr, mux)
עכשיו יש לנו:
server := &http.Server{
Addr: gw_addr,
BaseContext: func(_ net.Listener) context.Context { return ctx },
Handler: otelhttp.NewHandler(mux, "gateway", otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
return fmt.Sprintf("%s %s %s", operation, r.Method, r.URL.Path)
})),
}
go func() {
<-stopChan
if err := server.Shutdown(context.Background()); err != nil {
log.Fatalln(err)
}
}()
srvErr <- server.ListenAndServe()
תשומת לב לשורות 9-14 שבהן כיבוי השרת נצפה בגורוטין נפרד ושורה 15 שבה אם הייתה שגיאה כאשר השרת יצא, היא נשלחת בחזרה דרך ערוץ ה"notification" שהועבר כארגומנט לשיטה הזו.
עכשיו, לחלקים השונים של השירותים שלנו יש גישה לקישור "פעיל" OTLP שישמש בעת שהולידות רצופות יוצאות.
OTel Middleware לגרPC Gateway
למעלה, המיקסים http.Server
שמשמשים כדי להתחיל את הגרPC Gateway משתמשים במנגן מותאם: ה http.Handler
בערך מעבר את התוכנית OTel HTTP. מנגן זה לוקח מיקסים http.Handler
קיים, מדורף אותו עם הקונTEXT OTel ומובטח שהוא יתפסף אל כל מיקסים הדרוך הלאומי האחרים שמתקרבים.
server := &http.Server{
Addr: gw_addr,
BaseContext: func(_ net.Listener) context.Context { return ctx },
Handler: otelhttp.NewHandler(mux, "gateway", otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
return fmt.Sprintf("%s %s %s", operation, r.Method, r.URL.Path)
})),
}
המנגן שלנו לHTTP פשוט:
- שורה 4: אנחנו יוצרים מעטף מיוחד OTel כדי לטפל בבקשות HTTP חדשות.
- שורה 5: אנחנו מעלים את האפשרות
SpanFormatter
כך שהעותקים יכולים להיות זוהרים באופן ייחודי על ידי השיטה והמסלולים הHTTP של הבקשות. בלילד המנגןSpanNameFormatter
זה, השמות הבריריים של העותקים שלנו בגאטאווד יהיו פשוט"gateway"
, וכך כל העותקים יראו כך:
קישור מעטף גאטאווד לבקשות gRPC עם OTel
למעשה, ספרי הגרPC Gateway מיציאים "פשוט" קונTEXT כשהם מקימים/מנהלים קישורים לשירותים הבסיסיים GRPC. אחרי הכל, הגאטאווד לא יודע מה על OTel. במצב זה, קישור (מהמשתמש/הדפדפן) אל הגאטאווד והקישור מהגאטאווד אל השירות הGRPC יהיו טיפוסים של שני עותקים שונים.
אז חשוב להסיר את האחריות של
לפני אינטגרציה עם OTel, היינו רושמים ג'ייטווי הנדלר ל-gRPC שלנו עם:
ctx := context.Background()
mux := runtime.NewServeMux() // Not showing the interceptors
opts := []grpc.DialOption{grpc.WithInsecure()}
// grpc_addr = ":9090"
v1.RegisterTopicServiceHandlerFromEndpoint(ctx, mux, grpc_addr, opts)
v1.RegisterMessageServiceHandlerFromEndpoint(ctx, mux, grpc_addr, opts)
// And other servers
עכשיו, מעבר לחיבור אחר הוא פשוט:
mux := // Creat the mux runtime as usual
// Use the OpenTelemetry gRPC client interceptor for tracing
trclient := grpc.WithStatsHandler(otelgrpc.NewClientHandler())
conn, err := grpc.NewClient(grpc_addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
trclient)
if err != nil {
srvErr <- err
}
err = v1.RegisterTopicServiceHandler(ctx, mux, conn)
if err != nil {
srvErr <- err
}
// And the Message and User server too...
מה שעשינו הוא יצירת לקוח (שורה 6) שמשמש כמפעל חיבורים לשרת gRPC שלנו. הלקוח הוא פשוט. רק הנדלר gRPC (otelgrpc.NewClientHandler
) משמש ליצירת החיבור. זה מבטיח שההקשר בעקבה הנוכחית שהחלה בבקשת HTTP חדשה עכשיו מועבר לשרת gRPC דרך הנדלר הזה.
זהו. עכשיו אנחנו צריכים לראות את הבקשה החדשה לג'ייטווי ואת הבקשה gRPC->Gateway בעקבה מאוחדת אחת במקום שתי עקבות שונות.
תחילת וסיום ספאנים
אנחנו כמעט שם. עד כה:
- אנחנו הפעילנו את אוסף OTel ו-Jaeger לקבלת ואחסון נתוני עקבה (span) (ב-docker-compose).
- אנחנו הגדרנו את אוסף OTel הבסיסי (רץ כפוד נפרד) כ"ספק" של מעקבים, מדדים ולוגים (כלומר, אינטגרציית OTel של האפליקציה שלנו תשתמש בנקודת קצה זו כדי להפקיד את כל האותות).
- אנחנו עטפנו את הנדלר HTTP של הג'ייטווי כדי לאפשר OTel כך שעקבות והקשרים שלהן נוצרו והועברו.
- אנחנו החלפנו את הלקוח (gRPC) בג'ייטווי כך שעכשיו הוא עוטף את ההקשר OTel מההגדרות OTel שלנו במקום להשתמש בהקשר הברירת מחדל.
- אנחנו יצרנו מעקבים גלובליים, מדדים ולוגרים כך שאנחנו יכולים לשלוח אותות אמיתיים באמצעותם.
אנו צריכים לשלח ספנים לכל המקומות ה"מעניינים" בקוד שלנו. לדוגמה, ניקח את השיטה `ListTopics` (בקובץ `services/topics.go`):
func (s *TopicService) ListTopics(ctx context.Context, req *protos.ListTopicsRequest) (resp *protos.ListTopicsResponse, err error) {
results, err := s.DB.ListTopics(ctx, "", 100)
if err != nil {
return nil, err
}
resp = &protos.ListTopicsResponse{Topics: gfn.Map(results, TopicToProto)}
return
}
אנו קוראים לבסיס הנתונים להביא את הנושאים ולהחזיר אותם. בדומה לשיטת גישה לבסיס הנתונים (בקובץ `datastore/topicds.go`):
func (tdb *OneHubDB) ListTopics(ctx context.Context, pageKey string, pageSize int) (out []*Topic, err error) {
query := tdb.storage.Model(&Topic{}).Order("name asc")
if pageKey != "" {
count := 0
query = query.Offset(count)
}
if pageSize <= 0 || pageSize > tdb.MaxPageSize {
pageSize = tdb.MaxPageSize
}
query = query.Limit(pageSize)
err = query.Find(&out).Error
return out, err
}
כאן, אנו מעוניינים בעיקר בכמות הזמן שנלקחת בכל אחת משיטות אלה. אנו פשוט יוצרים ספנים בכל אחת מהן וזהו.
התוספות שלנו לשיטות השירות ובסיס הנתונים (בהתאמה) הן:
-
services/topics.go
:
func (s *TopicService) ListTopics(ctx context.Context, req *protos.ListTopicsRequest) (resp *protos.ListTopicsResponse, err error) {
ctx, span := Tracer.Start(ctx, "ListTopics")
defer span.End()
... rest of the code to query the DB and return a proto response
}
-
"`go
func (s *TopicService) ListTopics(ctx context.Context, req *protos.ListTopicsRequest) (resp *protos.ListTopicsResponse, err error) {
ctx, span := Tracer.Start(ctx, "ListTopics")
defer span.End()
// … שאר הקוד לשאיבת הנושאים מהבסיס והחזרת התגובה
}
"`datastore/topicds.go
:
func (tdb *OneHubDB) ListTopics(ctx context.Context, pageKey string, pageSize int) (out []*Topic, err error) {
_, span := Tracer.Start(ctx, "db.ListTopics")
defer span.End()
... rest of the code to fetch rows from the DB and return them
}
"`go
func (tdb *OneHubDB) ListTopics(ctx context.Context, pageKey string, pageSize int) (out []*Topic, err error) {
_, span := Tracer.Start(ctx, "db.ListTopics")
defer span.End()
// … שאר הקוד לשאיבת השורות מהבסיס והחזרתן
}
"`
הדפוס הכללי הוא:
1. **יצירת ספן** :
ctx, span := Tracer.Start(ctx, "<span name>")
כאן, ההקשר הנתון (`ctx`) מורכב והקשר חדש מוחזר. אנו יכולים (וצריך) להעביר את ההקשר החדש הזה לשיטות נוספות. אנו עושים דבר זה כשאנו קוראים לשיטת `ListTopics` של בסיס הנתונים.
2. **סיום הספן** :
defer span.End()
סיום הספן (כאשר השיטה מחזירה) מוודא שהזמנים הסופיים, קודים וכדומה נרשמים נכונה. אנו יכולים גם לעשות דברים אחרים כמו הוספת תגים וסטטוסים לספן אם יש צורך לספק מידע נוסף לסיוע באבחון.
זהו. אתה יכול לראות את העקבות היפות שלך ב-Jaeger ולקבל יותר ויותר מבטים על ביצועי הבקשות, מקצה לקצה.
**מסקנה**
אנחנו עשינו הרבה בפוסט הזה ועדיין רק קצת חתך על כל הפרטים מאחוריהם בOTel ובמעקב. במקום להעלות את המאמץ בפוסט הזה (שכבר מאובטח), אנחנו נציג רעיונות חדשים ופרטים מורכבים יותר בפוסטים הבאים. עכשיו, ניסוי זה בשירותים שלך וננסה לשחק עם מקבלים ואקספורטרים אחרים באוסף otel-contrib.
Source:
https://dzone.com/articles/tracing-with-opentelemetry-and-jaeger